最近看一些Java內(nèi)存模型方面的書,講了一下Java的對象的內(nèi)存分配過程,其中有個例子講解多線程鎖的問題,說了下面的例子:
單例寫法 雙重校驗寫法
//------------------------雙重校驗鎖------------------
private static Singleton singleton2;//-------1
public static Singleton getInstance4() {//------------2
if (singleton2 == null) {//-----------------------3
synchronized (Singleton.class) {//------------4
if (singleton2 == null)//-----------------5
singleton2 = new Singleton();//-------6
}
}
return singleton;
}
問題處在了第6步,Java創(chuàng)建對象的第6步可以分為以下三步:
memory = allocate();//----1
ctorInstance(memory);//-2
instance = memory;//-----3
其中2,3步在JVM編譯優(yōu)化時可能發(fā)生重排序,這和采用的JIT有關(guān),并且該重排序遵循intra-thread semantics法則(重排序后不會影響單線程的執(zhí)行結(jié)果)。
如果發(fā)生重排序,第3步先于第2步執(zhí)行,那么A線程可能只是讓對象指向內(nèi)存地址,并沒有實質(zhì)的初始化對象,那么線程B調(diào)用時就會發(fā)生錯誤。
解決方案
-
采用volatile
在Java1.5以后,volatile關(guān)鍵字被加強,這種重排序不允許在多線程中發(fā)生。
即在對象聲明加上volatile關(guān)鍵字。
實質(zhì):禁止編譯的重排序。
-
采用JVM初始化類時加鎖
JVM的類的初始化階段,會獲取鎖,該鎖可以同步多線程對一個類的初始化。
此時衍生一種稱為:Initialization On Demand Holder idiom的解決方案。
//-----------------------------靜態(tài)內(nèi)部類---------------
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance3() {
return SingletonHolder.INSTANCE;
}

JVM在多線程中初始化對象過程.jpg
** 實質(zhì): **利用JVM的多線程初始化對象的特性,允許重排序,但對其他線程不可見。
另外根據(jù)Java語言規(guī)范,一個類在一下5種情況會發(fā)生初始化:
- T是一個類,并且T的實例被創(chuàng)建。
- T是一個類,且T中的靜態(tài)方法被調(diào)用
- T是一個類,且T中的一個靜態(tài)字段被賦值。
- T是一個類,且T中的非常量字段被使用
- T是一個頂級類(TOP Level Class),有斷言語句嵌套在T內(nèi)部被執(zhí)行。(assert語句,很少用改規(guī)則)