讀完本文將了解到
- 單例模式的常用雙重檢查鎖寫法,以及如何避免JVM指令重排序?qū)е碌氖?/li>
- 更加高效、安全的內(nèi)部靜態(tài)類延時(shí)初始化的單例模式寫法
- 類的初始化順序(靜態(tài)屬性、靜態(tài)代碼塊、普通屬性、普通代碼塊、子類、父類...)
引子
我們都知道常見的單例模式的雙重檢查鎖的形式,代碼如下:
public class Singleton{
private static volatile Singleton instance; (4)
private Singleton(){}
public static Singleton getInstance(){
if(instance == null){ (1)
synchronized(Singleton.class){ (2)
if(instance == null){ (3)
instance = new Singleton();
}
}
}
return instance;
}
}
其中通過(1)(3)作為雙重檢查,而(2)加鎖保證初始化是線程安全的,
(4)中使用volatile關(guān)鍵字保證了內(nèi)存可見性,防止指令重排序?qū)е码p重檢查鎖失效。
雙重檢查鎖失效的原因是由于
instance = new instance()不是原子性操作,從而留給了JVM重排序的機(jī)會(huì)。在單線程中,不管怎么排,最終結(jié)果都一致;在多線程情況下指令就會(huì)發(fā)生重排序出現(xiàn)問題。具體參考: 雙重檢查鎖失效分析
更高效、安全的單例模式實(shí)現(xiàn)
在了解類初始化機(jī)制后可以采用更加高效、安全的線程安全單例模式,即內(nèi)部靜態(tài)類做延時(shí)初始化,代碼如下:
public class Singleton{
private static class SingletonHolder{
private static final Singleton INSTANCE = new Singleton();(3)
}
private Singleton(){}
public static Singleton getInstance(){ (1)
return SingletonHolder.INSTANCE; (2)
}
}
其執(zhí)行過程如下:
(1)執(zhí)行Singleton.getInstance(),檢測(cè)Singleton.class是否已被載入,若尚未被載入,則開始載入Singleton.class文件,并完成相應(yīng)的靜態(tài)初始化動(dòng)作;
(2)執(zhí)行SingletonHolder.INSTANCE,檢測(cè)發(fā)現(xiàn)SingletonHolder.class尚未被載入,則開始載入SingletonHolder.class文件,并執(zhí)行相應(yīng)的靜態(tài)初始化動(dòng)作;
(3)SingleHolder.class中靜態(tài)成員INSTANCE初始化動(dòng)作會(huì)執(zhí)行new Singleton()方法,完成了對(duì)Singleton實(shí)例INSTANCE的初始化操作后返回改實(shí)例。
類的初始化
類的初始化順序 //todo 流程圖完善
(1)類加載器檢查該類的.class文件是否已被載入,若沒有則載入;
(2)初始化靜態(tài)屬性、靜態(tài)代碼塊(按代碼順序);
(3)初始化普通屬性、普通代碼塊(按代碼順序);
(4)調(diào)用構(gòu)造函數(shù);
(5)在(1)、(2)、(3)、(4)的過程中若發(fā)現(xiàn)該類存在父類,并且其父類的.class尚未被載入如則先載入父類的.class文件,并執(zhí)行父類的(2)、(3)、(4)、(5)步驟。
觸發(fā)類初始化行為
(1)在使用new實(shí)例化對(duì)象,訪問靜態(tài)數(shù)據(jù)和方法時(shí),也就是遇到指令:new,getstatic/putstatic和invokestatic時(shí);
(2)使用反射對(duì)類進(jìn)行調(diào)用時(shí);
(3)當(dāng)初始化一個(gè)類時(shí),父類如果沒有進(jìn)行初始化,先觸發(fā)父類的初始化;
(4)執(zhí)行入口main方法所在的類;
(5)JDK1.7動(dòng)態(tài)語(yǔ)言支持中方法句柄所在的類,如果沒有初始化觸發(fā)起初始化;
測(cè)試代碼
class Parent{
private static String str1 = "父類靜態(tài)屬性1";
private String string = "父類普通屬性";
static {
System.out.println(str1);
//error-illegal forward reference
System.out.println(str2); (1)
//ok-not accessed via a simple name
System.out.println(Parent.str2); (2)
System.out.println("父類靜態(tài)代碼塊");
}
{
System.out.println(string);
System.out.println("父類普通代碼塊");
}
public static String str2 = "父類靜態(tài)屬性2";
public Parent() {
System.out.println("父類構(gòu)造函數(shù)");
}
}
class Child extends Parent{
private static String str = "子類靜態(tài)屬性";
private String string = "子類普通屬性";
static {
System.out.println(str);
System.out.println("子類靜態(tài)代碼塊");
}
{
System.out.println(string);
System.out.println("子類普通代碼塊");
}
public Child() {
System.out.println("子類構(gòu)造函數(shù)");
}
}
class Child2 extends Parent{
private static String str = "子類2靜態(tài)屬性";
private String string = "子類2普通屬性";
static {
System.out.println(str);
System.out.println("子類2靜態(tài)代碼塊");
}
{
System.out.println(string);
System.out.println("子類2普通代碼塊");
}
public Child() {
System.out.println("子類2構(gòu)造函數(shù)");
}
}
public class MainTest {
static {
System.out.println("MainTest靜態(tài)代碼塊");
}
public static void main(String[] args){
System.out.println("main() executed");
new Child();
}
}
輸出結(jié)果
MainTest靜態(tài)代碼塊
main() executed
父類靜態(tài)屬性1
null
父類靜態(tài)代碼塊
子類靜態(tài)屬性
子類靜態(tài)代碼塊
父類普通屬性
父類普通代碼塊
父類構(gòu)造函數(shù)
子類普通屬性
子類普通代碼塊
子類構(gòu)造函數(shù)
子類2靜態(tài)屬性
子類2靜態(tài)代碼塊
父類普通屬性
父類普通代碼塊
父類構(gòu)造函數(shù)
子類2普通屬性
子類2普通代碼塊
子類2構(gòu)造函數(shù)
結(jié)果分析
結(jié)果與前面描述的類初始化順序結(jié)論一致,有幾點(diǎn)需要注意:
1、靜態(tài)屬性和靜態(tài)代碼塊是按照統(tǒng)一的順序執(zhí)行的。
2、在代碼(1)之所以編譯器會(huì)報(bào)“illegal forward reference”是因?yàn)樵诼暶鱯tr2之前就進(jìn)行了read操作,要想強(qiáng)制使用,必須使用完成的類名+變量名。如果使用類名+變量名,這種向前引用就是null,如(2)所示。
3、更多詳細(xì)參考illegal forward reference的介紹在The Java Language Specification的Forward References During Field Initialization
總結(jié)
1、傳統(tǒng)的雙重檢查鎖單例模式有時(shí)會(huì)由于在多線程情況下JVM的指令重排序?qū)е率У那闆r,要避免這種情況,需要給instance加上volatile關(guān)鍵字保證內(nèi)存可見性,防止指令重排序。
2、為了更高效安全的實(shí)現(xiàn)單例模式,采用內(nèi)部靜態(tài)類做延時(shí)初始化方法,由JVM的類加載初始化機(jī)制幫我們實(shí)現(xiàn)線程安全的單例模式。
3、為了加深對(duì)類初始化順序的理解,先介紹了類的初始化順序偽代碼,并在后文編寫代碼進(jìn)行了結(jié)果驗(yàn)證和分析。