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