線程安全的單例模式常見寫法是雙重檢查加鎖。代碼如下:
class Singleton{
private volatile static Singleton singleton;
private Singleton(){}
public static Singleton getInstance(){
if(singleton == null){ // 1
synchronized(Singleton.class){ // 2
if(singleton == null){ // 3
singleton = new Singleton(); // 4
}
}
}
return singleton;
}
}
雙重檢查加鎖的單例模式代碼上就比較復雜,尤其體現(xiàn)在getInstance方法上,包括兩次檢查singleton是否是null,一次加鎖,singleton用關(guān)鍵字volatile修飾。為什么寫一個單例如此復雜呢?
首先是懶漢模式,實例的初始化延遲到getInstance方法中,為了保證只會生成一個實例,要先判斷singleton是否已經(jīng)初始化,如果已經(jīng)初始化了,就返回singleton,沒有的話就創(chuàng)建對象。這就是1的作用。如果是單線程的情況,這樣就夠用了。
在多線程的情況下,只有1,沒有2,3,就可能導致創(chuàng)建多個實例。例如,線程A和線程B調(diào)用getInstance方法,線程A先判斷了1,然后時間片結(jié)束了,切換到線程B,線程B判斷1,然后創(chuàng)建了singleton。時間片有切會線程A,線程A創(chuàng)建實例。這樣就線程A和線程B就分別創(chuàng)建了一個實例了。破壞了單例的結(jié)構(gòu)。
為了解決這個問題,加了synchronized保證只有一個線程進入臨界區(qū)。那只有2,沒有3,可以嗎?還是考慮和前面一模一樣的場景,這次線程A和線程B都判斷了1了,進入2,線程A先進入臨界區(qū),線程B發(fā)現(xiàn)線程A進入了臨界區(qū),就掛在了Singleton.class等等待隊列中,等待線程A執(zhí)行完成。線程A繼續(xù)執(zhí)行,創(chuàng)建了一個singleton實例。退出了臨界區(qū)。然后線程B被喚醒,進入臨界區(qū),又創(chuàng)建了一個singleton實例。結(jié)果又創(chuàng)建了兩個singleton實例。
所以3的作用很明顯了。在上面例子中,如果線程B發(fā)現(xiàn)實例已經(jīng)被創(chuàng)建了(singleton不等于null),就直接退出臨界區(qū)了。那1和3的作用似乎有點重合了,1似乎就不是必須了。2,3確實就足夠保證單例了。但是加鎖是比較消耗資源的,1就是為了減少資源的消耗。
最后,這么看來1,2,3,4就足以保證單例了。那為什么需要加volatile呢?volatile就牽扯到指令重排序的問題了。
要理解為什么要加volatile,首先要理解new Singleton()做了什么。new一個對象有幾個步驟。1.看class對象是否加載,如果沒有就先加載class對象,2.分配內(nèi)存空間,初始化實例,3.調(diào)用構(gòu)造函數(shù),4.返回地址給引用。而cpu為了優(yōu)化程序,可能會進行指令重排序,打亂這3,4這幾個步驟,導致實例內(nèi)存還沒分配,就被使用了。
再用線程A和線程B舉例。線程A執(zhí)行到new Singleton(),開始初始化實例對象,由于存在指令重排序,這次new操作,先把引用賦值了,還沒有執(zhí)行構(gòu)造函數(shù)。這時時間片結(jié)束了,切換到線程B執(zhí)行,線程B調(diào)用new Singleton()方法,發(fā)現(xiàn)引用不等于null,就直接返回引用地址了,然后線程B執(zhí)行了一些操作,就可能導致線程B使用了還沒有被初始化的變量。
加了volatile之后,就保證new 不會被指令重排序。
至此,這就是一個完整的懶漢模式—>線程安全的->雙重檢查加鎖單例模式。
推薦另一種單例模式的寫法:
class Singleton{
private Singleton(){}
private static class LazySomethineHolder{
public static Singleton singleton = new Singleton();
}
public static Singleton getInstance(){
return LazySomethineHolder.singleton;
}
}