Abstract
在開發(fā)中,如果某個(gè)實(shí)例的創(chuàng)建需要消耗很多系統(tǒng)資源,那么我們通常會(huì)使用惰性加載機(jī)制,也就是說只有當(dāng)使用到這個(gè)實(shí)例的時(shí)候才會(huì)創(chuàng)建這個(gè)實(shí)例,這個(gè)好處在單例模式中得到了廣泛應(yīng)用。這個(gè)機(jī)制在single-threaded環(huán)境下的實(shí)現(xiàn)非常簡(jiǎn)單,然而在multi-threaded環(huán)境下卻存在隱患。本文重點(diǎn)介紹惰性加載機(jī)制以及其在多線程環(huán)境下的使用方法。(作者numberzero,參考IBM文章《Double-checked locking and the Singleton pattern》,歡迎轉(zhuǎn)載與討論)
1 單例模式的惰性加載
通常當(dāng)我們?cè)O(shè)計(jì)一個(gè)單例類的時(shí)候,會(huì)在類的內(nèi)部構(gòu)造這個(gè)類(通過構(gòu)造函數(shù),或者在定義處直接創(chuàng)建),并對(duì)外提供一個(gè)static getInstance方法提供獲取該單例對(duì)象的途徑。例如:
Java代碼 < type="application/x-shockwave-flash" width="14" height="15" src="http://xupo.javaeye.com/javascripts/syntaxhighlighter/clipboard_new.swf" src="http://xupo.javaeye.com/javascripts/syntaxhighlighter/clipboard_new.swf" flashvars="clipboard=public%20class%20Singleton%20%20%20%20%20%20%0A%7B%20%20%20%20%20%20%0A%20%20%20%20private%20static%20Singleton%20instance%20%3D%20new%20Singleton()%3B%20%20%20%20%20%20%0A%20%20%20%20private%20Singleton()%7B%20%20%20%20%20%20%0A%20%20%20%20%20%20%20%20%E2%80%A6%20%20%20%20%20%20%0A%20%20%20%20%7D%20%20%20%20%20%20%0A%20%20%20%20public%20static%20Singleton%20getInstance()%7B%20%20%20%20%20%20%0A%20%20%20%20%20%20%20%20%20%20%20%20%20return%20instance%3B%20%20%20%20%20%20%20%0A%20%20%20%20%7D%20%20%20%20%20%20%0A%7D%20%20%20%20%20%0A" quality="high" allowscriptaccess="always" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" height="15" width="14">
public class Singleton
{
private static Singleton instance = new Singleton();
private Singleton(){
…
}
public static Singleton getInstance(){
return instance;
}
}
public class Singleton
{
private static Singleton instance = new Singleton();
private Singleton(){
…
}
public static Singleton getInstance(){
return instance;
}
}
這樣的代碼缺點(diǎn)是:第一次加載類的時(shí)候會(huì)連帶著創(chuàng)建Singleton實(shí)例,這樣的結(jié)果與我們所期望的不同,因?yàn)閯?chuàng)建實(shí)例的時(shí)候可能并不是我們需要這個(gè)實(shí)例的時(shí)候。同時(shí)如果這個(gè)Singleton實(shí)例的創(chuàng)建非常消耗系統(tǒng)資源,而應(yīng)用始終都沒有使用Singleton實(shí)例,那么創(chuàng)建Singleton消耗的系統(tǒng)資源就被白白浪費(fèi)了。
為了避免這種情況,我們通常使用惰性加載的機(jī)制,也就是在使用的時(shí)候才去創(chuàng)建。以上代碼的惰性加載代碼如下:
Java代碼 < type="application/x-shockwave-flash" width="14" height="15" src="http://xupo.javaeye.com/javascripts/syntaxhighlighter/clipboard_new.swf" src="http://xupo.javaeye.com/javascripts/syntaxhighlighter/clipboard_new.swf" flashvars="clipboard=public%20class%20Singleton%7B%20%20%20%20%20%20%0A%20%20%20%20private%20static%20Singleton%20instance%20%3D%20null%3B%20%20%20%20%20%20%0A%20%20%20%20private%20Singleton()%7B%20%20%20%20%20%20%0A%20%20%20%20%20%20%20%20%E2%80%A6%20%20%20%20%20%20%0A%20%20%20%20%7D%20%20%20%20%20%20%0A%20%20%20%20public%20static%20Singleton%20getInstance()%7B%20%20%20%20%20%20%0A%20%20%20%20%20%20%20%20if%20(instance%20%3D%3D%20null)%20%20%20%20%20%20%0A%20%20%20%20%20%20%20%20%20%20%20%20instance%20%3D%20new%20Singleton()%3B%20%20%20%20%20%20%20%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20instance%3B%20%20%20%20%20%20%20%0A%20%20%20%20%7D%20%20%20%20%20%20%0A%7D%20%20%20%20%20%0A" quality="high" allowscriptaccess="always" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" height="15" width="14">
public class Singleton{
private static Singleton instance = null ;
private Singleton(){
…
}
public static Singleton getInstance(){
if (instance == null )
instance = new Singleton();
return instance;
}
}
public class Singleton{
private static Singleton instance = null;
private Singleton(){
…
}
public static Singleton getInstance(){
if (instance == null)
instance = new Singleton();
return instance;
}
}
這樣,當(dāng)我們第一次調(diào)用Singleton.getInstance()的時(shí)候,這個(gè)單例才被創(chuàng)建,而以后再次調(diào)用的時(shí)候僅僅返回這個(gè)單例就可以了。
2 惰性加載在多線程中的問題
先將惰性加載的代碼提取出來(lái):
Java代碼 < type="application/x-shockwave-flash" width="14" height="15" src="http://xupo.javaeye.com/javascripts/syntaxhighlighter/clipboard_new.swf" src="http://xupo.javaeye.com/javascripts/syntaxhighlighter/clipboard_new.swf" flashvars="clipboard=public%20static%20Singleton%20getInstance()%7B%20%20%20%20%20%20%0A%20%20%20%20if%20(instance%20%3D%3D%20null)%20%20%20%20%20%20%0A%20%20%20%20instance%20%3D%20new%20Singleton()%3B%20%20%20%20%20%20%20%0A%20%20%20%20return%20instance%3B%20%20%20%20%20%20%20%0A%7D%20%20%20%20" quality="high" allowscriptaccess="always" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" height="15" width="14">
public static Singleton getInstance(){
if (instance == null )
instance = new Singleton();
return instance;
}
public static Singleton getInstance(){
if (instance == null)
instance = new Singleton();
return instance;
} 這是如果兩個(gè)線程A和B同時(shí)執(zhí)行了該方法,然后以如下方式執(zhí)行:
1. A進(jìn)入if判斷,此時(shí)foo為null,因此進(jìn)入if內(nèi)
2. B進(jìn)入if判斷,此時(shí)A還沒有創(chuàng)建foo,因此foo也為null,因此B也進(jìn)入if內(nèi)
3. A創(chuàng)建了一個(gè)Foo并返回
4. B也創(chuàng)建了一個(gè)Foo并返回
此時(shí)問題出現(xiàn)了,我們的單例被創(chuàng)建了兩次,而這并不是我們所期望的。
3 各種解決方案及其存在的問題
3.1 使用Class鎖機(jī)制
以上問題最直觀的解決辦法就是給getInstance方法加上一個(gè)synchronize前綴,這樣每次只允許一個(gè)現(xiàn)成調(diào)用getInstance方法:
Java代碼 < type="application/x-shockwave-flash" width="14" height="15" src="http://xupo.javaeye.com/javascripts/syntaxhighlighter/clipboard_new.swf" src="http://xupo.javaeye.com/javascripts/syntaxhighlighter/clipboard_new.swf" flashvars="clipboard=public%20static%20synchronized%20Singleton%20getInstance()%7B%20%20%20%20%20%20%0A%20%20%20%20if%20(instance%20%3D%3D%20null)%20%20%20%20%20%20%0A%20%20%20%20instance%20%3D%20new%20Singleton()%3B%20%20%20%20%20%20%20%0A%20%20%20%20return%20instance%3B%20%20%20%20%20%20%20%0A%7D%20%20%20%20%20%0A" quality="high" allowscriptaccess="always" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" height="15" width="14">
public static synchronized Singleton getInstance(){
if (instance == null )
instance = new Singleton();
return instance;
}
public static synchronized Singleton getInstance(){
if (instance == null)
instance = new Singleton();
return instance;
}
這種解決辦法的確可以防止錯(cuò)誤的出現(xiàn),但是它卻很影響性能:每次調(diào)用getInstance方法的時(shí)候都必須獲得Singleton的鎖,而實(shí)際上,當(dāng)單例實(shí)例被創(chuàng)建以后,其后的請(qǐng)求沒有必要再使用互斥機(jī)制了
3.2 double-checked locking
曾經(jīng)有人為了解決以上問題,提出了double-checked locking的解決方案
Java代碼 < type="application/x-shockwave-flash" width="14" height="15" src="http://xupo.javaeye.com/javascripts/syntaxhighlighter/clipboard_new.swf" src="http://xupo.javaeye.com/javascripts/syntaxhighlighter/clipboard_new.swf" flashvars="clipboard=public%20static%20Singleton%20getInstance()%7B%20%20%20%20%20%20%0A%20%20%20%20if%20(instance%20%3D%3D%20null)%20%20%20%20%20%20%0A%20%20%20%20%20%20%20%20synchronized(instance)%7B%20%20%20%20%20%20%0A%20%20%20%20%20%20%20%20%20%20%20%20if(instance%20%3D%3D%20null)%20%20%20%20%20%20%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20instance%20%3D%20new%20Singleton()%3B%20%20%20%20%20%20%0A%20%20%20%20%20%20%20%20%7D%20%20%20%20%20%20%0A%20%20%20%20return%20instance%3B%20%20%20%20%20%20%20%0A%7D" quality="high" allowscriptaccess="always" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" height="15" width="14">
public static Singleton getInstance(){
if (instance == null )
synchronized (instance){
if (instance == null )
instance = new Singleton();
}
return instance;
}
public static Singleton getInstance(){
if (instance == null)
synchronized(instance){
if(instance == null)
instance = new Singleton();
}
return instance;
} 讓我們來(lái)看一下這個(gè)代碼是如何工作的:首先當(dāng)一個(gè)線程發(fā)出請(qǐng)求后,會(huì)先檢查instance是否為null,如果不是則直接返回其內(nèi)容,這樣避免了進(jìn)入 synchronized塊所需要花費(fèi)的資源。其次,即使第2節(jié)提到的情況發(fā)生了,兩個(gè)線程同時(shí)進(jìn)入了第一個(gè)if判斷,那么他們也必須按照順序執(zhí)行 synchronized塊中的代碼,第一個(gè)進(jìn)入代碼塊的線程會(huì)創(chuàng)建一個(gè)新的Singleton實(shí)例,而后續(xù)的線程則因?yàn)闊o(wú)法通過if判斷,而不會(huì)創(chuàng)建多余的實(shí)例。
上述描述似乎已經(jīng)解決了我們面臨的所有問題,但實(shí)際上,從JVM的角度講,這些代碼仍然可能發(fā)生錯(cuò)誤。
對(duì)于JVM而言,它執(zhí)行的是一個(gè)個(gè)Java指令。在Java指令中創(chuàng)建對(duì)象和賦值操作是分開進(jìn)行的,也就是說instance = new Singleton();語(yǔ)句是分兩步執(zhí)行的。但是JVM并不保證這兩個(gè)操作的先后順序,也就是說有可能JVM會(huì)為新的Singleton實(shí)例分配空間,然后直接賦值給instance成員,然后再去初始化這個(gè)Singleton實(shí)例。這樣就使出錯(cuò)成為了可能,我們?nèi)匀灰訟、B兩個(gè)線程為例:
1. A、B線程同時(shí)進(jìn)入了第一個(gè)if判斷
2. A首先進(jìn)入synchronized塊,由于instance為null,所以它執(zhí)行instance = new Singleton();
3. 由于JVM內(nèi)部的優(yōu)化機(jī)制,JVM先畫出了一些分配給Singleton實(shí)例的空白內(nèi)存,并賦值給instance成員(注意此時(shí)JVM沒有開始初始化這個(gè)實(shí)例),然后A離開了synchronized塊。
4. B進(jìn)入synchronized塊,由于instance此時(shí)不是null,因此它馬上離開了synchronized塊并將結(jié)果返回給調(diào)用該方法的程序。
5. 此時(shí)B線程打算使用Singleton實(shí)例,卻發(fā)現(xiàn)它沒有被初始化,于是錯(cuò)誤發(fā)生了。
4 通過內(nèi)部類實(shí)現(xiàn)多線程環(huán)境中的單例模式
為了實(shí)現(xiàn)慢加載,并且不希望每次調(diào)用getInstance時(shí)都必須互斥執(zhí)行,最好并且最方便的解決辦法如下:
Java代碼 < type="application/x-shockwave-flash" width="14" height="15" src="http://xupo.javaeye.com/javascripts/syntaxhighlighter/clipboard_new.swf" src="http://xupo.javaeye.com/javascripts/syntaxhighlighter/clipboard_new.swf" flashvars="clipboard=public%20class%20Singleton%7B%20%20%20%20%20%20%0A%20%20%20%20private%20Singleton()%7B%20%20%20%20%20%20%0A%20%20%20%20%20%20%20%20%E2%80%A6%20%20%20%20%20%20%0A%20%20%20%20%7D%20%20%20%20%20%20%0A%20%20%20%20private%20static%20class%20SingletonContainer%7B%20%20%20%20%20%20%0A%20%20%20%20%20%20%20%20private%20static%20Singleton%20instance%20%3D%20new%20Singleton()%3B%20%20%20%20%20%20%0A%20%20%20%20%7D%20%20%20%20%20%20%0A%20%20%20%20public%20static%20Singleton%20getInstance()%7B%20%20%20%20%20%20%0A%20%20%20%20%20%20%20%20return%20SingletonContainer.instance%3B%20%20%20%20%20%20%0A%20%20%20%20%7D%20%20%20%20%20%20%0A%7D%20%20%20%20%20%0A" quality="high" allowscriptaccess="always" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" height="15" width="14">
public class Singleton{
private Singleton(){
…
}
private static class SingletonContainer{
private static Singleton instance = new Singleton();
}
public static Singleton getInstance(){
return SingletonContainer.instance;
}
}
public class Singleton{
private Singleton(){
…
}
private static class SingletonContainer{
private static Singleton instance = new Singleton();
}
public static Singleton getInstance(){
return SingletonContainer.instance;
}
}
JVM內(nèi)部的機(jī)制能夠保證當(dāng)一個(gè)類被加載的時(shí)候,這個(gè)類的加載過程是線程互斥的。這樣當(dāng)我們第一次調(diào)用getInstance的時(shí)候,JVM能夠幫我們保證instance只被創(chuàng)建一次,并且會(huì)保證把賦值給instance的內(nèi)存初始化完畢,這樣我們就不用擔(dān)心3.2中的問題。此外該方法也只會(huì)在第一次調(diào)用的時(shí)候使用互斥機(jī)制,這樣就解決了3.1中的低效問題。最后instance是在第一次加載SingletonContainer類時(shí)被創(chuàng)建的,而 SingletonContainer類則在調(diào)用getInstance方法的時(shí)候才會(huì)被加載,因此也實(shí)現(xiàn)了惰性加載。
本文來(lái)自CSDN博客,轉(zhuǎn)載請(qǐng)標(biāo)明出處:http://blog.csdn.net/fancyerII/archive/2010/03/15/5382349.aspx