01-單例模式(Singleton)

概述:

使用一個私有構造函數、一個私有靜態變量以及一個公有靜態函數來實現。

私有構造函數保證了不能通過構造函數來創建對象實例,只能通過公有靜態函數返回唯一的私有靜態變量。

 

優點:

在內存裏只有一個實例,減少了內存開銷

可以避免對資源的多重佔用

設置全局訪問點,嚴格控制訪問

缺點:

沒有接口,拓展比較困難

 

餓漢式可以防止反射攻擊,其他的則不能,因爲餓漢式可以在類的鏈接階段就構建好了靜態字段(?)

 

一、懶漢式(線程不安全)

 

私有靜態變量 uniqueInstance 被延遲實例化,這樣做的好處是,如果沒有用到該類,那麼就不會實例化 uniqueInstance,從而節約資源。

 

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

 

二、餓漢式(線程安全)

 

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

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

 

三、懶漢式(線程安全)

 

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

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

 

四、雙重校驗鎖(線程安全)

 

多例-Double Check

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

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

 

如果只使用了一個 if 語句。

if (uniqueInstance == null) {

    synchronized (Singleton.class) {

        uniqueInstance = new Singleton();

    }

}

在 uniqueInstance == null 的情況下,如果兩個線程都執行了 if 語句,那麼兩個線程都會進入 if 語句塊內。雖然在 if 語句塊內有加鎖操作,但是兩個線程都會執行 uniqueInstance = new Singleton(); 這條語句,只是先後的問題,那麼就會進行兩次實例化。因此必須使用雙重校驗鎖,也就是需要使用兩個 if 語句。

 

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 的指令重排,保證在多線程環境下也能正常運行。

 

五、靜態內部類實現

注:私有構造函數非常重要,上面的那幾種方式都要加,有空就加上

當 Singleton 類加載時,靜態內部類 SingletonHolder 沒有被加載進內存。只有當調用 getUniqueInstance()方法從而觸發 SingletonHolder.INSTANCE 時 SingletonHolder 纔會被加載,此時初始化 INSTANCE 實例,並且 JVM 能確保 INSTANCE 只被實例化一次。(相當於內部類也是一個獨立的類文件,但是隻是在需要的時候才調用,具體時間就是調用其靜態屬性的時候)

這種方式不僅具有延遲初始化的好處,而且由 JVM 提供了對線程安全的支持。

爲什麼是 private static呢?爲了避免其他類直接通過獲取這個內部類來獲取實例。

原理:

JVM在類的初始化階段(即Class被加載後,並且被線程使用之前,都是類的初始化階段)在這個階段會執行類的初始化,在執行類初始化期間,JVM會去獲取一個鎖,這個鎖可以同步多個線程對同一個類的初始化

 

六、枚舉類實現

這種方式能夠在類鏈接階段的時候就已經構建好了INSTANCE對象,同時能夠很好的防止反射攻擊和序列化後失效的問題,具體原因看鏈接

關於枚舉類爲什麼這麼寫

 

In JDK:

這兒就是線程安全的餓漢式

(這兒就沒有防止反射攻擊,可以自己創建一個Runtime實例)

 

In Spring:

AbstractFactoryBean 

#getObject()

這個就是非線程安全的懶漢式

 

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