單例模式可能是代碼最少的模式了箭券,但是少不一定意味著簡(jiǎn)單细溅,想要用好厚掷、用對(duì)單例模式奕筐,還真得費(fèi)一番腦筋型雳。本文對(duì)Java中常見的單例模式寫法做了一個(gè)總結(jié)绢涡,如有錯(cuò)漏之處本谜,懇請(qǐng)讀者指正痕寓。
餓漢法
顧名思義蠢莺,餓漢法就是在第一次引用該類的時(shí)候就創(chuàng)建對(duì)象實(shí)例寒匙,而不管實(shí)際是否需要?jiǎng)?chuàng)建。代碼如下:
public class Singleton {
private static Singleton = new Singleton();
private Singleton() {}
public static getSignleton(){
return singleton;
}
}
這樣做的好處是編寫簡(jiǎn)單躏将,但是無法做到延遲創(chuàng)建對(duì)象锄弱。但是我們很多時(shí)候都希望對(duì)象可以盡可能地延遲加載,從而減小負(fù)載祸憋,所以就需要下面的懶漢法:
單線程寫法
這種寫法是最簡(jiǎn)單的会宪,由私有構(gòu)造器和一個(gè)公有靜態(tài)工廠方法構(gòu)成,在工廠方法中對(duì)singleton進(jìn)行null判斷蚯窥,如果是null就new一個(gè)出來掸鹅,最后返回singleton對(duì)象。這種方法可以實(shí)現(xiàn)延時(shí)加載拦赠,但是有一個(gè)致命弱點(diǎn):線程不安全巍沙。如果有兩條線程同時(shí)調(diào)用getSingleton()方法,就有很大可能導(dǎo)致重復(fù)創(chuàng)建對(duì)象荷鼠。
public class Singleton {
private static Singleton singleton = null;
private Singleton(){}
public static Singleton getSingleton() {
if(singleton == null) singleton = new Singleton();
return singleton;
}
}
考慮線程安全的寫法
這種寫法考慮了線程安全句携,將對(duì)singleton的null判斷以及new的部分使用synchronized進(jìn)行加鎖。同時(shí)颊咬,對(duì)singleton對(duì)象使用volatile關(guān)鍵字進(jìn)行限制务甥,保證其對(duì)所有線程的可見性,并且禁止對(duì)其進(jìn)行指令重排序優(yōu)化喳篇。如此即可從語義上保證這種單例模式寫法是線程安全的敞临。注意,這里說的是語義上麸澜,實(shí)際使用中還是存在小坑的挺尿,會(huì)在后文寫到。
public class Singleton {
private static volatile Singleton singleton = null;
private Singleton(){}
public static Singleton getSingleton(){
synchronized (Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
return singleton;
}
}
兼顧線程安全和效率的寫法
雖然上面這種寫法是可以正確運(yùn)行的,但是其效率低下编矾,還是無法實(shí)際應(yīng)用熟史。因?yàn)槊看握{(diào)用getSingleton()方法,都必須在synchronized這里進(jìn)行排隊(duì)窄俏,而真正遇到需要new的情況是非常少的蹂匹。所以,就誕生了第三種寫法:
public class Singleton {
private static volatile Singleton singleton = null;
private Singleton(){}
public static Singleton getSingleton(){
if(singleton == null){
synchronized (Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
這種寫法被稱為“雙重檢查鎖”凹蜈,顧名思義限寞,就是在getSingleton()方法中,進(jìn)行兩次null檢查仰坦÷闹玻看似多此一舉,但實(shí)際上卻極大提升了并發(fā)度悄晃,進(jìn)而提升了性能玫霎。為什么可以提高并發(fā)度呢?就像上文說的妈橄,在單例中new的情況非常少庶近,絕大多數(shù)都是可以并行的讀操作。因此在加鎖前多進(jìn)行一次null檢查就可以減少絕大多數(shù)的加鎖操作眷蚓,執(zhí)行效率提高的目的也就達(dá)到了拦盹。
坑
那么,這種寫法是不是絕對(duì)安全呢溪椎?前面說了,從語義角度來看恬口,并沒有什么問題校读。但是其實(shí)還是有坑。說這個(gè)坑之前我們要先來看看volatile這個(gè)關(guān)鍵字祖能。其實(shí)這個(gè)關(guān)鍵字有兩層語義歉秫。第一層語義相信大家都比較熟悉,就是可見性养铸⊙丬剑可見性指的是在一個(gè)線程中對(duì)該變量的修改會(huì)馬上由工作內(nèi)存(Work Memory)寫回主內(nèi)存(Main Memory),所以會(huì)馬上反應(yīng)在其它線程的讀取操作中钞螟。順便一提兔甘,工作內(nèi)存和主內(nèi)存可以近似理解為實(shí)際電腦中的高速緩存和主存,工作內(nèi)存是線程獨(dú)享的鳞滨,主存是線程共享的洞焙。volatile的第二層語義是禁止指令重排序優(yōu)化。大家知道我們寫的代碼(尤其是多線程代碼),由于編譯器優(yōu)化澡匪,在實(shí)際執(zhí)行的時(shí)候可能與我們編寫的順序不同熔任。編譯器只保證程序執(zhí)行結(jié)果與源代碼相同,卻不保證實(shí)際指令的順序與源代碼相同唁情。這在單線程看起來沒什么問題疑苔,然而一旦引入多線程,這種亂序就可能導(dǎo)致嚴(yán)重問題甸鸟。volatile關(guān)鍵字就可以從語義上解決這個(gè)問題惦费。
注意,前面反復(fù)提到“從語義上講是沒有問題的”哀墓,但是很不幸趁餐,禁止指令重排優(yōu)化這條語義直到j(luò)dk1.5以后才能正確工作。此前的JDK中即使將變量聲明為volatile也無法完全避免重排序所導(dǎo)致的問題篮绰。所以后雷,在jdk1.5版本前,雙重檢查鎖形式的單例模式是無法保證線程安全的吠各。
靜態(tài)內(nèi)部類法
那么臀突,有沒有一種延時(shí)加載,并且能保證線程安全的簡(jiǎn)單寫法呢贾漏?我們可以把Singleton實(shí)例放到一個(gè)靜態(tài)內(nèi)部類中候学,這樣就避免了靜態(tài)實(shí)例在Singleton類加載的時(shí)候就創(chuàng)建對(duì)象,并且由于靜態(tài)內(nèi)部類只會(huì)被加載一次纵散,所以這種寫法也是線程安全的:
public class Singleton {
private static class Holder {
private static Singleton singleton = new Singleton();
}
private Singleton(){}
public static Singleton getSingleton(){
return Holder.singleton;
}
}
但是梳码,上面提到的所有實(shí)現(xiàn)方式都有兩個(gè)共同的缺點(diǎn):
都需要額外的工作(Serializable、transient伍掀、readResolve())來實(shí)現(xiàn)序列化掰茶,否則每次反序列化一個(gè)序列化的對(duì)象實(shí)例時(shí)都會(huì)創(chuàng)建一個(gè)新的實(shí)例。
可能會(huì)有人使用反射強(qiáng)行調(diào)用我們的私有構(gòu)造器(如果要避免這種情況蜜笤,可以修改構(gòu)造器濒蒋,讓它在創(chuàng)建第二個(gè)實(shí)例的時(shí)候拋異常)。
枚舉寫法
當(dāng)然把兔,還有一種更加優(yōu)雅的方法來實(shí)現(xiàn)單例模式沪伙,那就是枚舉寫法:
public enum Singleton {
INSTANCE;
private String name;
public String getName(){
return name;
}
public void setName(String name){
this.name = name;
}
}
使用枚舉除了線程安全和防止反射強(qiáng)行調(diào)用構(gòu)造器之外,還提供了自動(dòng)序列化機(jī)制县好,防止反序列化的時(shí)候創(chuàng)建新的對(duì)象围橡。因此,Effective Java推薦盡可能地使用枚舉來實(shí)現(xiàn)單例聘惦。
總結(jié)
這篇文章發(fā)出去以后得到許多反饋某饰,這讓我受寵若驚儒恋,覺得應(yīng)該再寫一點(diǎn)小結(jié)。代碼沒有一勞永逸的寫法黔漂,只有在特定條件下最合適的寫法诫尽。在不同的平臺(tái)、不同的開發(fā)環(huán)境(尤其是jdk版本)下炬守,自然有不同的最優(yōu)解(或者說較優(yōu)解)牧嫉。
比如枚舉,雖然Effective Java中推薦使用减途,但是在Android平臺(tái)上卻是不被推薦的酣藻。在這篇Android Training中明確指出:
Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.
再比如雙重檢查鎖法,不能在jdk1.5之前使用鳍置,而在Android平臺(tái)上使用就比較放心了(一般Android都是jdk1.6以上了辽剧,不僅修正了volatile的語義問題,還加入了不少鎖優(yōu)化税产,使得多線程同步的開銷降低不少)怕轿。
最后,不管采取何種方案辟拷,請(qǐng)時(shí)刻牢記單例的三大要點(diǎn):
- 線程安全
- 延遲加載
- 序列化與反序列化安全