1. 程式人生 > >java單例模式雙重檢驗鎖的優缺點?還有哪些實現方式?列舉一些使用場景

java單例模式雙重檢驗鎖的優缺點?還有哪些實現方式?列舉一些使用場景

2018年7月18日,在專案程式碼中看到單例模式,總結一下單例模式的知識點.

單例模式的概念:

在應用程式的生命週期中,在任意時刻,引用某個類的例項都是同一個.在一個系統中有些類只需要有一個全域性物件,統一管理系統行為和執行某些操作.例如在使用hibernate時,sessionFactory介面負責初始化hibernate,它充當資料儲存源的代理,並負責初始化session物件,通常一個專案只需要一個sessionFactory物件即可(多資料庫時每個資料庫對應一個sessionFactory),那麼就可把sessionFactory單例化,提高系統性能.

實現單例模式的思路:

思考1:如果我們通過new關鍵字建立某個類的物件,那麼new出來的物件在記憶體中佔有不同的地址,肯定不是單例.所以我們要保證單例,首先要保證不能通過new關鍵字來建立該類物件.

思考2:如何保證不能new出該類物件呢?顯然只需要私有化構造方法即可.

思考3:那我們需要的物件從哪來呢?只需要在該類內部建立一個該類物件,建立一個公共方法返回該物件即可,為了保證單例,用static關鍵字保證記憶體地址唯一.這樣該類物件引用始終指向同一個記憶體地址.

單例模式的實現方式:

懶漢式方式一:單執行緒下

  1. public class Singleton {

  2. private static Singleton instance = null;

  3. //構造私有化,外界不能new物件

  4. private Singleton (){}

  5. //通過公共的方式對外提供一個例項

  6. public static Singleton getInstance() {

  7. if (instance == null) {

  8. //如果沒有例項化,就建立一個物件

  9. instance = new Singleton();

  10. }

  11. return instance;

  12. }

  13. }

優點:不呼叫getInstance()就不會例項化,提高效率.

缺點:在單個執行緒中沒有問題,但多個執行緒同時訪問的時候就可能同時建立多個例項,而且這多個例項不是同一個物件,雖然後面建立的例項會覆蓋先建立的例項,但是還是會存在拿到不同物件的情況.

懶漢式方式二:synchronized同步方法

  1. public class Singleton{

  2. private static final Singleton instance = null;

  3. private Singleton(){}

  4. //同步方法

  5. public static synchronized Singleton getInstance(){

  6. if(instance==null){

  7. instance = new Singleton();

  8. }

  9. return instance;

  10. }

  11. }

雖然做到了執行緒安全,並且解決了多例項的問題,但是它並不高效.因為在任何時候只能有一個執行緒呼叫 getInstance()方法.但是同步操作只需要在第一次呼叫時才被需要,即第一次建立單例例項物件時.這就引出了雙重檢驗鎖.

懶漢式方式三:雙重檢驗鎖

  1. public class Singleton {

  2. private volatile static Singleton instance = null;

  3. private Singleton (){}

  4. public static Singleton getSingleton() {

  5. if (instance == null) { //判斷是否為null

  6. synchronized (Singleton.class) {

  7. if (instance == null) { //判斷是否為null

  8. instance = new Singleton();

  9. }

  10. }

  11. }

  12. return instance;

  13. }

  14. }

這段程式碼看起來很完美,但還是有問題,主要在於 instance = new Singleton()這句程式碼.因為這並非一個原子性操作,實際上在JVM裡大概做了3件事:

1.給instance分配記憶體

2.呼叫Singleton構造完成初始化

3.使instance物件的引用指向分配的記憶體空間(完成這一步instance就不是null了)

但是在 JVM 的即時編譯器中存在指令重排序的優化.也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序可能是 1-2-3 也可能是 1-3-2.如果是後者,則在 3 執行完畢、2 未執行之前,被執行緒二搶佔了,這時 instance 已經是非 null 了(但卻沒有初始化),所以執行緒二會直接返回 instance,然後使用,然後順理成章地報錯.(為什麼使用了synchronized同步還會被其他執行緒搶佔?)

我們只需要將 instance 變數宣告成 volatile 就可以了

  1. public class Singleton {

  2. private volatile static Singleton instance; //宣告成 volatile

  3. private Singleton (){}

  4. public static Singleton getSingleton() {

  5. if (instance == null) {

  6. synchronized (Singleton.class) {

  7. if (instance == null) {

  8. instance = new Singleton();

  9. }

  10. }

  11. }

  12. return instance;

  13. }

  14. }

使用 volatile 的主要原因是其一個特性:禁止指令重排序優化。也就是說,在 volatile 變數的賦值操作後面會有一個記憶體屏障(生成的彙編程式碼上),讀操作不會被重排序到記憶體屏障之前。比如上面的例子,取操作必須在執行完 1-2-3 之後或者 1-3-2 之後,不存在執行到 1-3 然後取到值的情況。從「先行發生原則」的角度理解的話,就是對於一個 volatile 變數的寫操作都先行發生於後面對這個變數的讀操作(這裡的“後面”是時間上的先後順序)。

但是特別注意在 Java 5 以前的版本使用了 volatile 的雙檢鎖還是有問題的。其原因是 Java 5 以前的 JMM (Java 記憶體模型)是存在缺陷的,即時將變數宣告成 volatile 也不能完全避免重排序,主要是 volatile 變數前後的程式碼仍然存在重排序問題。這個 volatile 遮蔽重排序的問題在 Java 5 中才得以修復,所以在這之後才可以放心使用 volatile。

相信你不會喜歡這種複雜又隱含問題的方式,當然我們有更好的實現執行緒安全的單例模式的辦法。

餓漢式:載入類時初始化例項

  1. public class Singleton{

  2. //類載入時就初始化

  3. private static final Singleton instance = new Singleton();

  4. private Singleton(){}

  5. public static Singleton getInstance(){

  6. return instance;

  7. }

  8. }

優點:這種方法非常簡單,因為單例的例項被宣告成 static 和 final 變量了,在第一次載入類到記憶體中時就會初始化,所以建立例項本身是執行緒安全的.

缺點: 資源利用率不高,可能getInstance()永遠不會執行到,但執行該類的其他靜態方法或者載入了該類(class.forName),那麼這個例項仍然初始化.餓漢式的建立方式在一些場景中將無法使用:譬如 Singleton 例項的建立是依賴引數或者配置檔案的,在 getInstance()之前必須呼叫某個方法設定引數給它,那樣這種單例寫法就無法使用了

懶漢式方式四:靜態內部類

  1. public class Singleton {

  2. private static class SingletonHolder {

  3. private static final Singleton INSTANCE = new Singleton();

  4. }

  5. private Singleton (){}

  6. public static final Singleton getInstance() {

  7. return SingletonHolder.INSTANCE;

  8. }

  9. }

這種寫法仍然使用JVM本身機制保證了執行緒安全問題;由於 SingletonHolder 是私有的,除了 getInstance() 之外沒有辦法訪問它,因此它是懶漢式的;同時讀取例項的時候不會進行同步,沒有效能缺陷;也不依賴 JDK 版本。

列舉方式:Enum

用列舉寫單例實在太簡單了!這也是它最大的優點。下面這段程式碼就是宣告列舉例項的通常做法。

  1. public enum EasySingleton{

  2. INSTANCE;

  3. //變數

  4. //方法

  5. }

我們可以通過EasySingleton.INSTANCE來訪問例項,這比呼叫getInstance()方法簡單多了。建立列舉預設就是執行緒安全的,所以不需要擔心double checked locking,而且還能防止反序列化導致重新建立新的物件。在《Effective Java》中說列舉是實現單例的最佳方式.建議在實際專案中單例以列舉方式實現.

總結:

一般來說,單例模式有五種寫法:懶漢、餓漢、雙重檢驗鎖、靜態內部類、列舉。

一般情況下直接使用餓漢式就好了,如果明確要求要懶載入(lazy initialization)會傾向於使用靜態內部類,如果涉及到反序列化建立物件時會試著使用列舉的方式來實現單例。

文章參考自: