單例模式詳解(懶漢式、餓漢式、線程安全、雙重檢驗鎖)

單例(Singleton)概述

使單例模式是爲了:保證一個類只有一個實例,並且提供這個實例的全局訪問點

一般做法是把該單例實例作爲該類的一個靜態變量、構造方法私有化,至於如何取得和創建該對象,有兩種種方法實現:懶漢式和餓漢式
對於不太瞭解靜態變量和靜態方法的可以點這裏

Ⅰ 懶漢式-線程不安全

這個實現在多線程環境下是不安全的,如果多個線程能夠同時進入 if (uniqueInstance == null) ,並且此時 uniqueInstance 爲 null,那麼會有多個線程執行 uniqueInstance = new Singleton(); 語句,這將導致實例化多次 uniqueInstance。

public class Singleton {

    private static Singleton uniqueInstance;

    private Singleton() {
    }

    public static Singleton getUniqueInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
}

Ⅱ 餓漢式-線程安全

線程不安全問題主要是由於 uniqueInstance 被實例化多次,採取直接實例化 uniqueInstance 的方式就不會產生線程不安全問題。

但是直接實例化的方式也丟失了延遲實例化帶來的節約資源的好處。


public class Singleton {

	private static Singleton uniqueInstance = new Singleton();

    private Singleton() {
    }

    public static Singleton getUniqueInstance() {
        return uniqueInstance;
    }
}

Ⅲ 懶漢式與餓漢式橫評

懶漢式😂很懶,拖延症,不到必要時候不創建單例實例 餓漢式😂餓了,一上來(類加載時)就創建單例
線程安全(多線程環境下能否確保對象僅被創建一次) 不安全 安全
單例實例創建時機 當Singleton.getUniqueInstance()方法被調用時纔會創建實例,如果程序由始至終都沒有調用該方法,則單例實例永遠不會被創建,節省了內存空間和資源 在Singleton被類加載器加載時創建單例對象,並且會把單例變量一直存放在內存的方法區中,直到程序結束纔會把單例對象銷燬,如果程序由始至終都沒有使用該實例,將會造成資源浪費

由於懶漢式有更好的性能,所以接下介紹兩種通過加鎖使得懶漢式在多線程環境中也能線程安全的方法:

Ⅳ 懶漢式-線程安全改進(通過加鎖的方法)

單鎖 getUniqueInstance() 方法

只需要對 getUniqueInstance() 方法加鎖,那麼在一個時間點只能有一個線程能夠進入該方法,從而避免了實例化多次 uniqueInstance。

但是當一個線程進入該方法之後,其它試圖進入該方法的線程都必須等待,即使 uniqueInstance 已經被實例化了。這會讓線程阻塞時間過長,因此該方法有性能問題,不推薦使用。

public static synchronized Singleton getUniqueInstance() {
    if (uniqueInstance == null) {
        uniqueInstance = new Singleton();
    }
    return uniqueInstance;
}

雙重校驗鎖-線程安全

uniqueInstance 只需要被實例化一次,之後就可以直接使用了。加鎖操作只需要對實例化那部分的代碼進行,只有當 uniqueInstance 沒有被實例化時,才需要進行加鎖。

雙重校驗鎖先判斷 uniqueInstance 是否已經被實例化,如果沒有被實例化,那麼纔對實例化語句進行加鎖。

public class Singleton {

    private volatile static Singleton uniqueInstance;

    private Singleton() {
    }

    public static Singleton getUniqueInstance() {
        if (uniqueInstance == null) {
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

考慮下面的實現,也就是隻使用了一個 if 語句。在 uniqueInstance == null 的情況下,如果兩個線程都執行了 if 語句,那麼兩個線程都會進入 if 語句塊內。雖然在 if 語句塊內有加鎖操作,但是兩個線程都會執行 uniqueInstance = new Singleton(); 這條語句,只是先後的問題,那麼就會進行兩次實例化。因此必須使用雙重校驗鎖,也就是需要使用兩個 if 語句:第一個 if 語句用來避免 uniqueInstance 已經被實例化之後的加鎖操作,而第二個 if 語句進行了加鎖,所以只能有一個線程進入,就不會出現 uniqueInstance == null 時兩個線程同時進行實例化操作。

if (uniqueInstance == null) {
    synchronized (Singleton.class) {
        uniqueInstance = new Singleton();
    }
}

相信看到這裏的同學的專研精神已經到了很高的地步了。但如果有同學想深入瞭解volatilesynchronized的可以點進去看看。

uniqueInstance 採用 volatile 關鍵字修飾也是很有必要的, uniqueInstance = new Singleton(); 這段代碼其實是分爲三步執行:

  1. 爲 uniqueInstance 分配內存空間
  2. 初始化 uniqueInstance
  3. 將 uniqueInstance 指向分配的內存地址

但是由於 JVM 具有指令重排的特性,執行順序有可能變成 1>3>2。指令重排在單線程環境下不會出現問題,但是在多線程環境下會導致一個線程獲得還沒有初始化的實例。例如,線程 T1 執行了 1 和 3,此時 T2 調用 getUniqueInstance() 後發現 uniqueInstance 不爲空,因此返回 uniqueInstance,但此時 uniqueInstance 還未被初始化。

使用 volatile 可以禁止 JVM 的指令重排,保證在多線程環境下也能正常運行。

後記(對於volatile作用的疑惑)

筆者最近發現有些讀者有這樣的疑惑:在雙重所檢驗uniqueInstance使用volatile關鍵字是因爲volatile關鍵字能夠讓對象被修改時 第一時間通知其他線程。

但其實並非這樣。如果T1 線程在執行的 new 的 1、3 步之後遲遲不跑第 2 步,那麼線程 T2 仍然會用還沒有初始化uniqueInstance執行業務邏輯,最後就會報錯。

所以使用 volatile 的作用是禁止 JVM 的指令重排,沒毛病。👍

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章