單例設計模式


一、概述

1、什么是單例設計模式?

在某些特殊場合中,一個類只能夠產生一個實例對象,並且這個實例對象要可以對外提供訪問。這樣的類叫做單例類, 而設計單例的流程和思想叫做單例設計模式

單例模式屬於設計模式三大類中的創建型模式

2、單例設計模式的特點

單例模式具有典型的三個特點:

  • 只有一個實例。
  • 自我實例化。
  • 提供全局訪問點。

注意:

注:注意單例模式所屬類的 構造方法是私有的,所以單例類是 不能被繼承的。 (這句話表述的有點問題,單例類一般情況只想內部保留一個實例對象,所以會選擇將構造函數聲明為私有的,這才使得單例類無法被繼承。單例類與繼承沒有強關聯關系。)

3、單例設計模式的UML類圖

單例模式的UML結構圖非常簡單,就只有一個類,如下圖:

clipboard

Singleton類,定義一個靜態方法,getInstance(),可以通過類名來調用,主要負責替代構造方法,創建Singleton類唯一的實例對象。

這個類可以對外提供訪問,允許用戶通過getInstance()方法訪問它唯一的實例。

4、單例設計模式的優缺點:

優點:

1)、由於單例模式只生成了一個實例,所以能夠節約系統資源,減少性能開銷,提高系統效率,

2)、避免頻繁的創建銷毀對象,可以提高性能;

3)、避免對共享資源的多重占用,簡化訪問;

4)、為整個系統提供唯一一個全局訪問點,能夠嚴格控制客戶對它的訪問。

缺點:

1)、不適用於變化頻繁的對象;

2)、也正是因為系統中只有一個實例,這樣就導致了單例類的職責過重,違背了“單一職責原則”,

濫用單例將帶來一些負面問題,如為了節省資源將數據庫連接池對象設計為的單例類,可能會導致共享連接池對象的程序過多而出現連接池溢出;

3)、同時也沒有抽象類,這樣擴展起來有一定的困難。

4)、如果實例化的對象長時間不被利用,系統會認為該對象是垃圾而被回收,這可能會導致對象狀態的丟失;(這個所有的對象都會,跟單例無關。)

5、單例模式的應用場景:

場景一:

windows的任務管理器,無論你點擊多少次,始終都只有一個管理器窗口存在,系統並不會為你創建新的窗口,也就是說,整個系統運行的過程中,系統只維護了一個進程管理器的實例。這就是一個典型的單例模式運用。

場景二:

線程池、數據庫連接池的設計一般也是采用單例模式,因為數據庫連接是一種數據庫資源。數據庫軟件系統中使用數據庫連接池,主要是節省打開或者關閉數據庫連接所引起的效率損耗,這種效率上的損耗還是非常昂貴的,用單例模式來維護,就可以大大降低這種損耗。

場景三:

程序的日志模塊。一般也是采用單例模式實現。由於共享的日志文件一直處於打開狀態,只能有一個實例去操作,否則內容不好追加。 采用單例模式就可以。

場景四:

Web應用的配置對象的讀取,一般也應用單例模式,這個是由於配置文件是共享的資源。

這些配置信息存放在一個文件中,由一個單例對象統一讀取,然后服務進程中的其他對象再通過這個單例對象獲取這些配置信息。這種方式簡化了在復雜環境下的配置管理。

場景五:

在我們的實際項目開發中,可以使用單例模式來封裝一些常用的工具類,保證整個應用常用的數據統一。或者保存一些共享數據在內存中,其他類隨時可以讀取。

二、單例模式的實現步驟

可以使用如下的步驟實現一個單例類:

單例設計模式的實現流程

1、將構造方法私有化,使用private關鍵字修飾。使其不能在類的外部通過new關鍵字實例化該類對象。

2、在該類內部產生一個唯一的實例化對象,並且將其封裝為private static類型。

3、對外提供一個靜態方法getInstance()負責將對象返回出去,使用public static修飾

三、單例模式的實現方式 (推薦枚舉類方式)

1、餓漢式——立即加載

線程安全,調用效率高。但是不能延時加載。

立即加載就是加載類的時候就已經將對象創建完畢(不管以后會不會使用到該實例化對象,先創建了再說。很着急的樣子,故又被稱為“餓漢模式”),常見的實現辦法就是直接new實例化。

所以加載類的速度比較慢,但是獲取對象的速度比較快,且是線程安全的。

/**
 * 餓漢式 
 */
public class Singleton {

    // 創建全局唯一的實例化對象,在類初始化時,就會立即加載這個對象
    private static Singleton instance = new Singleton();

    // 私有化構造方法
    private Singleton() {}

    // 提供公有靜態方法返回對象
    public static Singleton getInstance() {
        return instance;
    }
}

我們知道,類加載的方式是按需加載,且加載一次。因此,在上述單例類被加載時,就會實例化一個對象並交給自己的引用,供系統使用;而且,由於這個類在整個生命周期中只會被加載一次,因此只會創建一個實例,即能夠充分保證單例。

優缺點:

優點:這種寫法比較簡單,就是在類裝載的時候就完成實例化。避免了線程同步問題。

缺點:在類裝載的時候就完成實例化,沒有達到Lazy Loading的效果。如果從始至終從未使用過這個實例,則會造成內存的浪費。(因為這個static的instance對象會一直占着這段內存,直到卸載類(即便你還沒有用到這個實例))

2、懶漢式——延遲加載

延遲加載就是調用get()方法時實例才被創建(先不急着實例化出對象,等要用的時候才給你創建出來。不着急,故又稱為“懶漢模式”),常見的實現方法就是在get方法中進行new實例化。

/**
 * 懶漢式
  */
public class Singleton {
    
    // 聲明一個自身實例對象的引用
    private static Singleton instance;
    
    // 私有化構造方法
    private Singleton(){}
    
    // 提供公有靜態方法返回對象
    public static Singleton getInstance() {
        // 判斷如果為空,就創建,如果已經有了,就直接返回該實例,避免重復創建,保證全局唯一
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

由於該模式是在運行時加載對象的,所以加載類比較快,但是對象的獲取速度相對較慢,且線程不安全。如果想要線程安全的話可以加上synchronized關鍵字,但是這樣會付出慘重的效率代價。

我們從懶漢式單例可以看到,單例實例被延遲加載,即只有在真正使用的時候才會實例化一個對象並交給自己的引用。

這種寫法起到了Lazy Loading的效果,但是只能在單線程下使用。如果在多線程下,一個線程進入了if (singleton == null)判斷語句塊,還未來得及往下執行,另一個線程也通過了這個判斷語句,這時便會產生多個實例。所以在多線程環境下不可使用這種方式。

“懶漢模式”的優缺點:

優點:實現起來比較簡單,當類SingletonTest被加載的時候,靜態變量static的instance未被創建,只是聲明了一個引用,並未分配內存空間。要當getInstance方法第一次被調用時,初始化instance變量,才會真正創建對象,開始分配內存,因此在某些特定條件下會節約了內存。(需要時才創建)

缺點:在多線程環境中,這種實現方法是完全錯誤的,根本不能保證單例的狀態。

3、線程安全的“懶漢模式”——  synchronized

在懶漢模式的基礎上,增加了synchronized鎖同步機制,保證全局唯一。

/**
 * 3、線程安全的懶漢式 —— synchronized
 */
public class Singleton {

    // 聲明一個自身實例對象的引用
    private static Singleton instance;

    // 私有化構造方法
    private Singleton(){}

    // 提供公有靜態方法返回對象,加上synchronized關鍵字實現同步
    public static synchronized Singleton getInstance() {
        // 判斷如果為空,就創建,如果已經有了,就直接返回該實例,避免重復創建,保證全局唯一
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

優點:在多線程情形下,保證了“懶漢模式”的線程安全。

缺點:眾所周知在多線程情形下,synchronized方法通常效率低,顯然這不是最佳的實現方案。

4、懶漢式(DCL雙重檢測鎖)

DCL雙檢查鎖機制(DCL:double checked locking)

/**
 * 4、懶漢式 —— DCL雙重檢查鎖機制(類鎖)
 * 再一次縮小了鎖的范圍,提供了性能
 */
public class Singleton {

    // 聲明一個自身實例對象的引用,使用volatile保證多線程下引用的一致性
    private static volatile Singleton instance;

    // 私有化構造方法
    private Singleton(){}

    // 提供公有靜態方法返回對象
    public static Singleton getInstance() {
        // 第一次檢查instance是否被實例化出來,如果沒有,再加鎖處理
        if (instance == null) {
            synchronized (Singleton.class) {
                // 某個線程取得了類鎖,實例化對象前第二次檢查instance是否已經被實例化出來,如果沒有,才最終實例出對象
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

Double-Check概念對於多線程開發者來說不會陌生。

我們這里相比3直接對靜態方法getInstance加上synchronized鎖的方式,縮小了鎖的范圍。

將第一個if判斷塊釋放出來了,如果實例存在,則根本不會鎖住,大大加快了返回實例的效率。

只有當第一次if檢查后,確定實例是真的不存在,需要創建時,此時才會開始加鎖,注意此時加的是類鎖,不是對象鎖。不過這里是static靜態方法,對象也是靜態的,所以實際上它們效果是一樣的。


為什么在鎖里面還要再次判定是否為空呢?

因為高並發,后面的線程在第一次判定實例時也為空,也可以獲得鎖,只是要排隊,只是在等待前面的線程釋放鎖。所以,當輪到它拿到鎖之后,可能前面的線程已經創建了實例,所以要再次判定是否為空。這樣才能保證實例唯一。

(重點在於,多個線程可以同時通過第一個if,然后都可以按順序執行鎖里的代碼。)


Java指令重排的問題

注意:單純使用上面這種方式,仍然是線程不安全的。

因為存在java指令重排的問題。

在java創建對象的時候,cpu按照以下三個步驟來執行:

1、memory = allocate() 在堆內存中開辟對象的內存空間,並指定地址

2、根據類加載的順序,初始化對象。

3、instance = memory 設置instance指向剛分配的內存地址。instance是變量,存在棧中。


單純執行以上三步沒啥問題,但是在多線程情況下,可能會發生指令重排序。

指令重排序對單線程沒有影響,單線程下CPU可以按照順序執行以上三個步驟,但是在多線程下,如果發生了指令重排序,則會打亂上面的三個步驟。

如果發生了JVM和CPU優化,發生重排序時,可能會按照下面的順序執行:

1、memory = allocate() 在堆內存中開辟對象的內存空間,並指定地址

3、instance = memory 設置instance指向剛分配的內存地址。instance是變量,存在棧中。

2、根據類加載的順序,初始化對象。


假設目前有兩個線程A和B同時執行getInstance()方法,

  • A線程執行到instance = new Singleton(); B線程剛執行到第一個 if (instance == null) 處,
  • 如果按照1.3.2的順序,假設線程A執行到第三步3.instance = memory 設置instance指向剛分配的內存,此時,線程B判斷instance已經有值,就會直接return instance;
  • 而實際上,線程A還未執行第二步 初始化對象,也就是說線程B拿到的instance對象還未進行初始化,這個未初始化的instance對象一旦被線程B使用,就會出現問題。

5、懶漢式(DCL雙重檢測鎖機制+volatile禁止指令重排)—— 推薦

相比4,這里對引用加入了volatile機制,禁止java的指令重排

懶漢式的單例模式的最佳實現方式。內存消耗少,效率高,線程安全,多線程操作原子性。

/**
 * 5、懶漢式 —— DCL雙重檢查鎖機制(類鎖) + volatile禁止指令重排
 * 再一次縮小了鎖的范圍,提供了性能。(推薦)
 */
public class Singleton {

    // 聲明一個自身實例對象的引用,使用volatile禁止指令重排,保證多線程下引用的一致性
    private static volatile Singleton instance;

    // 私有化構造方法
    private Singleton(){}

    // 提供公有靜態方法返回對象
    public static Singleton getInstance() {
        // 第一次檢查instance是否被實例化出來,如果沒有,再加鎖處理
        if (instance == null) {
            synchronized (Singleton.class) {
                // 某個線程取得了類鎖,實例化對象前第二次檢查instance是否已經被實例化出來,如果沒有,才最終實例出對象
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

線程安全;延遲加載;效率較高。

6、靜態代碼塊——立即加載

靜態代碼塊方式跟餓漢式的方式幾乎是一樣的,只是把初始化代碼放到了static塊中了。

因為我們知道,類加載的時候,這些屬性和靜態代碼塊都是會跟隨類一起加載的,所以它的實現方式和餓漢式一樣。也是線程安全的。

/**
 * 6、靜態代碼塊方式
 * 方式類似餓漢式,也是立即加載,是線程安全的
  */
public class Singleton {

    // 在外部聲明一個對象的引用,注意不能放到靜態代碼塊中
    private static Singleton instance;

    // 靜態代碼塊中,創建唯一實例對象,賦值給引用。
    static {
        instance = new Singleton();
    }

    // 私有化構造方法
    private Singleton() {}

    // 提供公有靜態方法返回實例對象
    public static Singleton getInstance() {
        return instance;
    }
}

優缺點:

優缺點都同餓漢式一樣,也是立即加載,線程安全的。

這里定義靜態變量時要注意:

靜態變量只能定義在類的內部,不可以定義在靜態塊或方法中。可以在類內部定義靜態變量,在靜態塊中進行初始化操作,因為類的內部是不允許有操作語句存在的,比如JDBC操作,所以可以在靜態塊static{} 中進行初始化操作,如:JDBC 定義靜態變量主要是為了供外部訪問,定義在一個局部中外部沒有權限訪問,為什么要定義呢,而且不能定義。

7、靜態內部類

懶漢模式需要考慮線程安全,所以我們多寫了好多的代碼,餓漢模式利用了類加載的特性為我們省去了線程安全的考慮,那么,既能享受類加載確保線程安全帶來的便利,又能延遲加載的方式,就是靜態內部類。Java靜態內部類的特性是,加載的時候不會加載內部靜態類,使用的時候才會進行加載。而使用到的時候類加載又是線程安全的,這就完美的達到了我們的預期效果~

/**
 * 7、靜態內部類
 * 融合餓漢式和懶漢式的優點,推薦
  */
public class Singleton {

    // 私有靜態內部類中創建並初始化實例對象,注意要private私有化,不能被外部調用了
    private static class SingletonInner{
        private static Singleton instance = new Singleton();
    }

    // 私有化構造方法
    private Singleton() {}

    // 提供公有靜態方法,返回實例對象
    public static Singleton getInstance() {
        return SingletonInner.instance;
    }
}

似乎靜態內部類看起來已經是最完美的方法了,其實不是,可能還存在反射攻擊或者反序列化攻擊。


8、枚舉類 —— 線程最安全(最佳方式)

單元素的枚舉類型已經成為實現Singleton的最佳方法

                      -- 出自 《effective java》

在effective java(這本書真的很棒)中說道,最佳的單例實現模式就是枚舉模式。利用枚舉的特性,讓JVM來幫我們保證線程安全和單一實例的問題。除此之外,寫法還特別簡單。

/**
 * 8、枚舉類
 * 最佳實現方式
  */
public enum Singleton {
    INSTANCE;
}
注意:

因為INSTANCE實例是public公有的,可以直接通過類名的方式調用,即Singleton.INSTANCE,

就不再需要提供公有靜態方法getInstance()來返回對象了。


這是最簡潔、最安全的方式,不過它不能實現lazy loading延遲加載。


其實枚舉類它本身就具備單例的特性:

比如:都會私有化構造方法。枚舉類會對屬性值加上public static final的屬性,保障這個屬性值都是全局唯一的。這些操作都和單例很像

所以把這個屬性變成對象,它就是一個單例類。

類似於這種內部類的形式:

public class Singleton {
    public static final Singleton INSTANCE = new Singleton();
}

枚舉類繼承自ENUM,內部實現了Serializable接口,所以不用考慮序列化的問題(其實序列化反序列化也能導致單例失敗的,但是我們這里不過多研究)。

對於線程安全,同樣的,加載的時候JVM能確保只加載一個實例。避免暴力反射創建多個實例,絕對防止多次實例化。


枚舉類最佳實踐:

參考:https://www.jianshu.com/p/d35f244f3770

枚舉單例示例:

public enum  EnumSingleton {
    INSTANCE;
    public EnumSingleton getInstance(){
        return INSTANCE;
    }
}

實際應用場景中,很多人會這么使用枚舉單例:

public class User {
    //私有化構造函數
    private User(){ }
 
    //定義一個靜態枚舉類
    static enum SingletonEnum{
        //創建一個枚舉對象,該對象天生為單例
        INSTANCE;
        private User user;
        //私有化枚舉的構造函數
        private SingletonEnum(){
            user=new User();
        }
        public User getInstnce(){
            return user;
        }
    }
 
    //對外暴露一個獲取User對象的靜態方法
    public static User getInstance(){
        return SingletonEnum.INSTANCE.getInstnce();
    }
}

public class Test {
    public static void main(String [] args){
        System.out.println(User.getInstance());
        System.out.println(User.getInstance());
        System.out.println(User.getInstance()==User.getInstance());
    }
}
結果為true

以上代碼看起來已經是ok了,其實不是,可能還存在反射攻擊或者反序列化攻擊

最終版

public enum Singleton {

    INSTANCE;

    public void doSomething() {
        System.out.println("doSomething");
    }

}

// 調用方法:

public class Main {

    public static void main(String[] args) {
        Singleton.INSTANCE.doSomething();
    }

}

// 直接通過Singleton.INSTANCE.doSomething()的方式調用即可。方便、簡潔又安全。

推薦大家使用枚舉類實現單例模式。

四、各種實現方式的選擇

一般情況下,懶漢式(包含線程安全和線程不安全兩種方式)都比較少用;

餓漢式和DCL雙重檢測鎖都可以使用,可根據具體情況自主選擇;

在要明確實現 lazy loading 效果時,可以考慮靜態內部類的實現方式;

若涉及到反序列化創建對象時,大家也可以嘗試使用枚舉方式。

在選擇時,請參考下面這張圖:

圖片來源:https://www.cnblogs.com/rainbowbridge/p/12902359.html

clipboard

五、破壞單例模式的方法及解決辦法

參考:https://blog.csdn.net/b_just/article/details/104061314

1、除枚舉方式外, 其他方法都會通過反射的方式破壞單例,反射是通過調用構造方法生成新的對象,所以如果我們想要阻止單例破壞,可以在構造方法中進行判斷,若已有實例, 則阻止生成新的實例,解決辦法如下:

private SingletonObject1(){
    if (instance !=null){
        throw new RuntimeException("實例已經存在,請通過 getInstance()方法獲取");
    }
}

2、如果單例類實現了序列化接口Serializable, 就可以通過反序列化破壞單例,所以我們可以不實現序列化接口,如果非得實現序列化接口,可以重寫反序列化方法readResolve(), 反序列化時直接返回相關單例對象。

public Object readResolve() throws ObjectStreamException {
    return instance;
}


引用轉載:

https://www.jianshu.com/p/3f5eb3e0b050 (爆贊)

https://www.cnblogs.com/xuwendong/p/9633985.html (爆贊)

https://segmentfault.com/a/1190000010755849 (贊)

https://www.cnblogs.com/binaway/p/8889184.html

https://www.jianshu.com/p/d35f244f3770 (贊)

https://blog.csdn.net/b_just/article/details/104061314


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM