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

說起單例模式大家基本上已經(jīng)很熟了。最為經(jīng)典的例子double-check-locking (DCL)想想大家都寫了很多次了。然而隨著越來越透傳的對JMM的理解,我們漸漸發(fā)現(xiàn)傳統(tǒng)的DCL在理論上或許存在問題。

引用:

  1. Initialization-on-demand_holder_idiom
  2. 單例模式、雙檢測鎖定DCL、volatile(轉(zhuǎn))

1.傳統(tǒng)的例子

非常經(jīng)典的例子,基本上對java有了解的同學(xué)都可以寫出來,我們的例子,可能存在一個(gè)BUG,這個(gè)BUG的原因是,JMM出于對效率的考慮,是在happens-before原則內(nèi)(out-of-order)亂序執(zhí)行。

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. 簡單的原理性介紹。

我們初始化一個(gè)類,會(huì)產(chǎn)生多條匯編指令,然而總結(jié)下來,是執(zhí)行下面三個(gè)事情:

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

Java編譯器允許處理器亂序執(zhí)行(out-of-order),我們有可能是1->2->3也有可能是1->3->2。即我們有可能在先返回instance實(shí)例,然后執(zhí)行構(gòu)造方法。

即:double-check-locking可能存在線程拿到一個(gè)沒有執(zhí)行構(gòu)造方法的對象。

3.一個(gè)簡單可能出錯(cuò)的執(zhí)行順序。

線程A、B執(zhí)行g(shù)etInstance().getId()

在某一時(shí)刻,線程A執(zhí)行到(5),并且初始化順序?yàn)椋?->3->2,當(dāng)執(zhí)行完將instance對象指向分配空間時(shí)。此時(shí)線程B執(zhí)行(1),發(fā)現(xiàn)instance!=null,繼續(xù)執(zhí)行,最后調(diào)用getId()返回0。此時(shí)切換到線程B對構(gòu)造方法初始化。

4. 解決方案

方案一:

利用類第一次使用才加載,加載時(shí)同步的特性。
優(yōu)點(diǎn)是:官方推薦,可以可以保證實(shí)現(xiàn)懶漢模式。代碼少。
缺點(diǎn)是:第一次加載比較慢,而且多了一個(gè)類多了一個(gè)文件,總覺得不爽。

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

方案二:利用volatile關(guān)鍵字

volatile禁止了指令重排序,所以確保了初始化順序一定是1->2->3,所以也就不存在拿到未初始化的對象引用的情況。
優(yōu)點(diǎn):保持了DCL,比較簡單
確定:volatile這個(gè)關(guān)鍵字多少會(huì)帶來一些性能影響吧。

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;  
    }  
}  

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

通過一個(gè)temp,來確定初始化結(jié)束后其他線程才能獲得引用。
同時(shí)注意,JIT可能對這一部分優(yōu)化,我們必須阻止JTL這部分的"優(yōu)化"。

缺點(diǎn)是有點(diǎn)難理解,優(yōu)點(diǎn)是:可以不用volatile關(guān)鍵字,又可以用DLC,豈不妙哉。

public class Singleton {    
    
    private static Singleton singleton; // 這類沒有volatile關(guān)鍵字    
    
    private Singleton() {    
    }    
    
    public static Singleton getInstance() {    
        // 雙重檢查加鎖    
        if (singleton == null) {    
            synchronized (Singleton.class) {    
                // 延遲實(shí)例化,需要時(shí)才創(chuàng)建    
                if (singleton == null) {    
                        
                    Singleton temp = null;  
                    try {  
                        temp = new Singleton();    
                    } catch (Exception e) {  
                    }  
                    if (temp != null)    //為什么要做這個(gè)看似無用的操作,因?yàn)檫@一步是為了讓虛擬機(jī)執(zhí)行到這一步的時(shí)會(huì)才對singleton賦值,虛擬機(jī)執(zhí)行到這里的時(shí)候,必然已經(jīng)完成類實(shí)例的初始化。所以這種寫法的DCL是安全的。由于try的存在,虛擬機(jī)無法優(yōu)化temp是否為null  
                        singleton = temp; 
                }    
            }    
        }    
        return singleton;    
    }  
}  
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容