1. 程式人生 > >你真的會用單例模式?

你真的會用單例模式?

單例模式可以說只要是一個合格的開發都會寫,但是如果要深究,小小的單例模式可以牽扯到很多東西,比如:多執行緒是否安全?是否懶載入?效能等等。還有你知道幾種單例模式的寫法呢?如何防止反射破壞單例模式?

一、 單例模式

1.1 定義

單例模式就是在程式執行中只例項化一次,建立一個全域性唯一物件。有點像 Java 的靜態變數,但是單例模式要優於靜態變數:

  1. 靜態變數在程式啟動的時候JVM就會進行載入,如果不使用,會造成大量的資源浪費;
  2. 單例模式能夠實現懶載入,能夠在使用例項的時候才去建立例項。

開發工具類庫中的很多工具類都應用了單例模式,比例執行緒池、快取、日誌物件等,它們都只需要建立一個物件,如果建立多份例項,可能會帶來不可預知的問題,比如資源的浪費、結果處理不一致等問題。

1.2 單例的實現思路

  1. 靜態化例項物件;
  2. 私有化構造方法,禁止通過構造方法建立例項;
  3. 提供一個公共的靜態方法,用來返回唯一例項。

1.3 單例的好處

  1. 只有一個物件,記憶體開支少、效能好;
  2. 避免對資源的多重佔用;
  3. 在系統設定全域性訪問點,優化和共享資源訪問。

二、 單例模式的實現

  1. 餓漢模式
  2. 懶漢模式
  3. 雙重檢查鎖模式
  4. 靜態內部類單例模式
  5. 列舉類實現單例模式

2.1 餓漢模式

在定義靜態屬性時,直接例項化了物件

public class HungryMode {

    /**
     * 利用靜態變數來儲存唯一例項
     */
    private static final HungryMode instance = new HungryMode();

    /**
     * 私有化建構函式
     */
    private HungryMode(){
        // 裡面可以有很多操作
    }

    /**
     * 提供公開獲取例項介面
     * @return
     */
    public static HungryMode getInstance(){
        return instance;
    }
}

2.1.1 優點

由於使用了static關鍵字,保證了在引用這個變數時,關於這個變數的所以寫入操作都完成,所以保證了JVM層面的執行緒安全

2.1.2 缺點

不能實現懶載入,造成空間浪費:如果一個類比較大,我們在初始化的時就載入了這個類,但是我們長時間沒有使用這個類,這就導致了記憶體空間的浪費。

所以,能不能只有用到 getInstance()方法,才會去初始化單例類,才會載入單例類中的資料。所以就有了:懶漢式。

2.2 懶漢模式

懶漢模式是一種偷懶的模式,在程式初始化時不會建立例項,只有在使用例項的時候才會建立例項,所以懶漢模式解決了餓漢模式帶來的空間浪費問題。

2.2.1 懶漢模式的一般實現

public class LazyMode {
    /**
     * 定義靜態變數時,未初始化例項
     */
    private static LazyMode instance;

    /**
     * 私有化建構函式
     */
    private LazyMode(){
        // 裡面可以有很多操作
    }
    /**
     * 提供公開獲取例項介面
     * @return
     */
    public static LazyMode getInstance(){
        // 使用時,先判斷例項是否為空,如果例項為空,則例項化物件
        if (instance == null) {
            instance = new LazyMode();
        }
        return instance;
    }
}

但是這種實現在多執行緒的情況下是不安全的,有可能會出現多份例項的情況:

if (instance == null) {
    instance = new LazyMode();
}

假設有兩個執行緒同時進入到上面這段程式碼,因為沒有任何資源保護措施,所以兩個執行緒可以同時判斷的 instance 都為空,都將去初始化例項,所以就會出現多份例項的情況。

2.2.2 懶漢模式的優化

我們給getInstance()方法加上synchronized關鍵字,使得getInstance()方法成為受保護的資源就能夠解決多份例項的問題。

public class LazyModeSynchronized {
    /**
     * 定義靜態變數時,未初始化例項
     */
    private static LazyModeSynchronized instance;
    /**
     * 私有化建構函式
     */
    private LazyModeSynchronized(){
        // 裡面可以有很多操作
    }
    /**
     * 提供公開獲取例項介面
     * @return
     */
    public synchronized static LazyModeSynchronized getInstance(){
        /**
         * 新增class類鎖,影響了效能,加鎖之後將程式碼進行了序列化,
         * 我們的程式碼塊絕大部分是讀操作,在讀操作的情況下,程式碼執行緒是安全的
         *
         */
        if (instance == null) {
            instance = new LazyModeSynchronized();
        }
        return instance;
    }
}

2.2.3 懶漢模式的優點

實現了懶載入,節約了記憶體空間。

2.2.4 懶漢模式的缺點

  1. 在不加鎖的情況下,執行緒不安全,可能出現多份例項;
  2. 在加鎖的情況下,會使程式序列化,使系統有嚴重的效能問題。

懶漢模式中加鎖的問題,對於getInstance()方法來說,絕大部分的操作都是讀操作,讀操作是執行緒安全的,所以我們沒必讓每個執行緒必須持有鎖才能呼叫該方法,我們需要調整加鎖的問題。由此也產生了一種新的實現模式:雙重檢查鎖模式。

2.3 雙重檢查鎖模式

2.3.1 雙重檢查鎖模式的一般實現

public class DoubleCheckLockMode {

    private static DoubleCheckLockMode instance;

    /**
     * 私有化建構函式
     */
    private DoubleCheckLockMode(){

    }
    /**
     * 提供公開獲取例項介面
     * @return
     */
    public static DoubleCheckLockMode getInstance(){
        // 第一次判斷,如果這裡為空,不進入搶鎖階段,直接返回例項
        if (instance == null) {
            synchronized (DoubleCheckLockMode.class) {
                // 搶到鎖之後再次判斷是否為空
                if (instance == null) {
                    instance = new DoubleCheckLockMode();
                }
            }
        }
        return instance;
    }
}

雙重檢查鎖模式解決了單例、效能、執行緒安全問題,但是這種寫法同樣存在問題:在多執行緒的情況下,可能會出現空指標問題,出現問題的原因是JVM在例項化物件的時候會進行優化和指令重排序操作。

2.3.2 什麼是指令重排?

private SingletonObject(){
      // 第一步
     int x = 10;
      // 第二步
     int y = 30;
     // 第三步
     Object o = new Object(); 
}

上面的建構函式SingletonObject()JVM 會對它進行指令重排序,所以執行順序可能會亂掉,但是不管是那種執行順序,JVM 最後都會保證所以例項都完成例項化。 如果建構函式中操作比較多時,為了提升效率,JVM 會在建構函式裡面的屬性未全部完成例項化時,就返回物件。雙重檢測鎖出現空指標問題的原因就是出現在這裡,當某個執行緒獲取鎖進行例項化時,其他執行緒就直接獲取例項使用,由於JVM指令重排序的原因,其他執行緒獲取的物件也許不是一個完整的物件,所以在使用例項的時候就會出現空指標異常問題。

2.3.3 雙重檢查鎖模式優化

要解決雙重檢查鎖模式帶來空指標異常的問題,只需要使用volatile關鍵字,volatile關鍵字嚴格遵循happens-before原則,即:在讀操作前,寫操作必須全部完成。

public class DoubleCheckLockModelVolatile {
    /**
     * 新增volatile關鍵字,保證在讀操作前,寫操作必須全部完成
     */
    private static volatile DoubleCheckLockModelVolatile instance;
    /**
     * 私有化建構函式
     */
    private DoubleCheckLockModelVolatile(){

    }
    /**
     * 提供公開獲取例項介面
     * @return
     */
    public static DoubleCheckLockModelVolatile getInstance(){

        if (instance == null) {
            synchronized (DoubleCheckLockModelVolatile.class) {
                if (instance == null) {
                    instance = new DoubleCheckLockModelVolatile();
                }
            }
        }
        return instance;
    }
}

2.4 靜態內部類模式

靜態內部類模式也稱單例持有者模式,例項由內部類建立,由於 JVM 在載入外部類的過程中, 是不會載入靜態內部類的, 只有內部類的屬性/方法被呼叫時才會被載入, 並初始化其靜態屬性。靜態屬性由static修飾,保證只被例項化一次,並且嚴格保證例項化順序。

public class StaticInnerClassMode {

    private StaticInnerClassMode(){

    }

    /**
     * 單例持有者
     */
    private static class InstanceHolder{
        private  final static StaticInnerClassMode instance = new StaticInnerClassMode();

    }

    /**
     * 提供公開獲取例項介面
     * @return
     */
    public static StaticInnerClassMode getInstance(){
        // 呼叫內部類屬性
        return InstanceHolder.instance;
    }
}

這種方式跟餓漢式方式採用的機制類似,但又有不同。兩者都是採用了類裝載的機制來保證初始化例項時只有一個執行緒。不同的地方:

  1. 餓漢式方式是隻要Singleton類被裝載就會例項化,沒有Lazy-Loading的作用;
  2. 靜態內部類方式在Singleton類被裝載時並不會立即例項化,而是在需要例項化時,呼叫getInstance()方法,才會裝載SingletonInstance類,從而完成Singleton的例項化。

類的靜態屬性只會在第一次載入類的時候初始化,所以在這裡,JVM幫助我們保證了執行緒的安全性,在類進行初始化時,別的執行緒是無法進入的。

所以這種方式在沒有加任何鎖的情況下,保證了多執行緒下的安全,並且沒有任何效能影響和空間的浪費。

2.5 列舉類實現單例模式

因為列舉型別是執行緒安全的,並且只會裝載一次,設計者充分的利用了列舉的這個特性來實現單例模式,列舉的寫法非常簡單,而且列舉型別是所用單例實現中唯一一種不會被破壞的單例實現模式。

public class EnumerationMode {
    
    private EnumerationMode(){
        
    }

    /**
     * 列舉型別是執行緒安全的,並且只會裝載一次
     */
    private enum Singleton{
        INSTANCE;

        private final EnumerationMode instance;

        Singleton(){
            instance = new EnumerationMode();
        }

        private EnumerationMode getInstance(){
            return instance;
        }
    }

    public static EnumerationMode getInstance(){

        return Singleton.INSTANCE.getInstance();
    }
}

適用場合:

  1. 需要頻繁的進行建立和銷燬的物件;
  2. 建立物件時耗時過多或耗費資源過多,但又經常用到的物件;
  3. 工具類物件;
  4. 頻繁訪問資料庫或檔案的物件。

三、單例模式的問題及解決辦法

除列舉方式外, 其他方法都會通過反射的方式破壞單例

3.1 單例模式的破壞

/**
 * 以靜態內部類實現為例
 * @throws Exception
 */
@Test
public void singletonTest() throws Exception {
    Constructor constructor = StaticInnerClassMode.class.getDeclaredConstructor();
    constructor.setAccessible(true);

    StaticInnerClassMode obj1 = StaticInnerClassMode.getInstance();
    StaticInnerClassMode obj2 = StaticInnerClassMode.getInstance();
    StaticInnerClassMode obj3 = (StaticInnerClassMode) constructor.newInstance();

    System.out.println("輸出結果為:"+obj1.hashCode()+"," +obj2.hashCode()+","+obj3.hashCode());
}

控制檯列印:

輸出結果為:1454171136,1454171136,1195396074

從輸出的結果我們就可以看出obj1obj2為同一物件,obj3為新物件。obj3是我們通過反射機制,進而呼叫了私有的建構函式,然後產生了一個新的物件。

3.2 如何阻止單例破壞

可以在構造方法中進行判斷,若已有例項, 則阻止生成新的例項,解決辦法如下:

public class StaticInnerClassModeProtection {

    private static boolean flag = false;

    private StaticInnerClassModeProtection(){
        synchronized(StaticInnerClassModeProtection.class){
            if(flag == false){
                flag = true;
            }else {
                throw new RuntimeException("例項已經存在,請通過 getInstance()方法獲取!");
            }
        }
    }

    /**
     * 單例持有者
     */
    private static class InstanceHolder{
        private  final static StaticInnerClassModeProtection instance = new StaticInnerClassModeProtection();
    }

    /**
     * 提供公開獲取例項介面
     * @return
     */
    public static StaticInnerClassModeProtection getInstance(){
        // 呼叫內部類屬性
        return InstanceHolder.instance;
    }
}

測試:

/**
 * 在構造方法中進行判斷,若存在則丟擲RuntimeException
 * @throws Exception
 */
@Test
public void destroyTest() throws Exception {
    Constructor constructor = StaticInnerClassModeProtection.class.getDeclaredConstructor();
    constructor.setAccessible(true);

    StaticInnerClassModeProtection obj1 = StaticInnerClassModeProtection.getInstance();
    StaticInnerClassModeProtection obj2 = StaticInnerClassModeProtection.getInstance();
    StaticInnerClassModeProtection obj3 = (StaticInnerClassModeProtection) constructor.newInstance();

    System.out.println("輸出結果為:"+obj1.hashCode()+"," +obj2.hashCode()+","+obj3.hashCode());
}

控制檯列印:

Caused by: java.lang.RuntimeException: 例項已經存在,請通過 getInstance()方法獲取!
    at cn.van.singleton.demo.mode.StaticInnerClassModeProtection.<init>(StaticInnerClassModeProtection.java:22)
    ... 35 more

四、總結

4.1 各種實現的對比

名稱 餓漢模式 懶漢模式 雙重檢查鎖模式 靜態內部類實現 列舉類實現
可用性 可用 不推薦使用 推薦使用 推薦使用 推薦使用
特點 不能實現懶載入,可能造成空間浪費 不加鎖執行緒不安全;加鎖效能差 執行緒安全;延遲載入;效率較高 避免了執行緒不安全,延遲載入,效率高。 寫法簡單;執行緒安全;只裝載一次

4.2 示例程式碼地址

Github 示例代