【單例模式】DCL的問題和解決方法

1.傳統的例子

很是經典的例子,基本上對java有了解的同窗均可以寫出來,咱們的例子,可能存在一個BUG,這個BUG的緣由是,JMM出於對效率的考慮,是在happens-before原則內(out-of-order)亂序執行。java

public class LazySingleton {
    private int id;
    private static LazySingleton instance;
    private LazySingleton() {
        this.id= new Random().nextInt(200)+1;                 // (1)
    }
    public static LazySingleton getInstance() {
        if (instance == null) {                               // (2)
            synchronized(LazySingleton.class) {               // (3)
                if (instance == null) {                       // (4)
                    instance = new LazySingleton();           // (5)
                }
            }
        }
        return instance;                                      // (6)
    }
    public int getId() {
        return id;                                            // (7)
    }
}

2. 簡單的原理性介紹。

咱們初始化一個類,會產生多條彙編指令,然而總結下來,是執行下面三個事情:安全

1.給LazySingleton 的實例分配內存。
2.初始化LazySingleton 的構造器
3.將instance對象指向分配的內存空間(注意到這步instance就非null了)app

Java編譯器容許處理器亂序執行(out-of-order),咱們有多是1->2->3也有多是1->3->2。即咱們有可能在先返回instance實例,而後執行構造方法。dom

即:double-check-locking可能存在線程拿到一個沒有執行構造方法的對象。性能

3.一個簡單可能出錯的執行順序。

線程A、B執行getInstance().getId()優化

在某一時刻,線程A執行到(5),而且初始化順序爲:1->3->2,當執行完將instance對象指向分配空間時。此時線程B執行(1),發現instance!=null,繼續執行,最後調用getId()返回0。此時切換到線程B對構造方法初始化。this

4. 解決方案

方案一:

利用類第一次使用才加載,加載時同步的特性。
優勢是:官方推薦,能夠能夠保證明現懶漢模式。代碼少。
缺點是:第一次加載比較慢,並且多了一個類多了一個文件,總以爲不爽。線程

public class SingletonKerriganF {     
      
    private static class SingletonHolder {     
        static final SingletonKerriganF INSTANCE = new SingletonKerriganF();     
    }     
      
    public static SingletonKerriganF getInstance() {     
        return SingletonHolder.INSTANCE;     
    }     
}

方案二:利用volatile關鍵字

volatile禁止了指令重排序,因此確保了初始化順序必定是1->2->3,因此也就不存在拿到未初始化的對象引用的狀況。
優勢:保持了DCL,比較簡單
肯定:volatile這個關鍵字多少會帶來一些性能影響吧。code

public class Singleton(){  
    private volatile static Singleton singleton;  
    private Sington(){};  
    public static Singleton getInstance(){  
        if(singleton == null){  
            synchronized (Singleton.class){  
                if(singleton == null){  
                     singleton = new Singleton();    
                }  
            }
        }           
        return singleton;  
    }  
}

方案三:初始化完後賦值。

經過一個temp,來肯定初始化結束後其餘線程才能得到引用。
同時注意,JIT可能對這一部分優化,咱們必須阻止JTL這部分的"優化"。對象

缺點是有點難理解,優勢是:能夠不用volatile關鍵字,又能夠用DLC,豈不妙哉。

public class Singleton {    
    
    private static Singleton singleton; // 這類沒有volatile關鍵字    
    
    private Singleton() {    
    }    
    
    public static Singleton getInstance() {    
        // 雙重檢查加鎖    
        if (singleton == null) {    
            synchronized (Singleton.class) {    
                // 延遲實例化,須要時才建立    
                if (singleton == null) {    
                        
                    Singleton temp = null;  
                    try {  
                        temp = new Singleton();    
                    } catch (Exception e) {  
                    }  
                    if (temp != null)    //爲何要作這個看似無用的操做,由於這一步是爲了讓虛擬機執行到這一步的時會纔對singleton賦值,虛擬機執行到這裏的時候,必然已經完成類實例的初始化。因此這種寫法的DCL是安全的。因爲try的存在,虛擬機沒法優化temp是否爲null  
                        singleton = temp; 
                }    
            }    
        }    
        return singleton;    
    }  
}
相關文章
相關標籤/搜索