摘要:所以,在版本前,雙重檢查鎖形式的單例模式是無法保證線程安全的。
單例模式可能是代碼最少的模式了,但是少不一定意味著簡(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檢查??此贫啻艘慌e,但實(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ì)被加載一次(JVM機(jī)制),所以這種寫法也是線程安全的:
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):
線程安全
延遲加載
序列化與反序列化安全
參考資料
《Effective Java(第二版)》
《深入理解Java虛擬機(jī)——JVM高級(jí)特性與最佳實(shí)踐(第二版)》
轉(zhuǎn)載出自https://www.cnblogs.com/andy-...
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/74294.html
摘要:使用靜態(tài)類體現(xiàn)的是基于對(duì)象,而使用單例設(shè)計(jì)模式體現(xiàn)的是面向?qū)ο?。二編寫單例模式的代碼編寫單例模式的代碼其實(shí)很簡(jiǎn)單,就分了三步將構(gòu)造函數(shù)私有化在類的內(nèi)部創(chuàng)建實(shí)例提供獲取唯一實(shí)例的方法餓漢式根據(jù)上面的步驟,我們就可以輕松完成創(chuàng)建單例對(duì)象了。 前言 只有光頭才能變強(qiáng) 回顧前面: 給女朋友講解什么是代理模式 包裝模式就是這么簡(jiǎn)單啦 本來打算沒那么快更新的,這陣子在刷Spring的書籍。在看...
摘要:如果需要防范這種攻擊,請(qǐng)修改構(gòu)造函數(shù),使其在被要求創(chuàng)建第二個(gè)實(shí)例時(shí)拋出異常。單例模式與單一職責(zé)原則有沖突。源碼地址參考文獻(xiàn)設(shè)計(jì)模式之禪 定義 單例模式是一個(gè)比較簡(jiǎn)單的模式,其定義如下: 保證一個(gè)類僅有一個(gè)實(shí)例,并提供一個(gè)訪問它的全局訪問點(diǎn)。 或者 Ensure a class has only one instance, and provide a global point of ac...
摘要:總結(jié)我們主要介紹到了以下幾種方式實(shí)現(xiàn)單例模式餓漢方式線程安全懶漢式非線程安全和關(guān)鍵字線程安全版本懶漢式雙重檢查加鎖版本枚舉方式參考設(shè)計(jì)模式中文版第二版設(shè)計(jì)模式深入理解單例模式我是一個(gè)以架構(gòu)師為年之內(nèi)目標(biāo)的小小白。 初遇設(shè)計(jì)模式在上個(gè)寒假,當(dāng)時(shí)把每個(gè)設(shè)計(jì)模式過了一遍,對(duì)設(shè)計(jì)模式有了一個(gè)最初級(jí)的了解。這個(gè)學(xué)期借了幾本設(shè)計(jì)模式的書籍看,聽了老師的設(shè)計(jì)模式課,對(duì)設(shè)計(jì)模式算是有個(gè)更進(jìn)一步的認(rèn)識(shí)。...
摘要:關(guān)于對(duì)于重排序的講解,強(qiáng)烈推薦閱讀程曉明寫的深入理解內(nèi)存模型二重排序。語義語義單線程下,為了優(yōu)化可以對(duì)操作進(jìn)行重排序。編譯器和處理器為單個(gè)線程實(shí)現(xiàn)了語義,但對(duì)于多線程并不實(shí)現(xiàn)語義。雙重加載的單例模式分析即雙重檢查加鎖。 版權(quán)聲明:本文由吳仙杰創(chuàng)作整理,轉(zhuǎn)載請(qǐng)注明出處:https://segmentfault.com/a/1190000009231182 1. 引言 在開始分析雙重加鎖單...
摘要:總之,選擇單例模式就是為了避免不一致狀態(tài),避免政出多頭。二餓漢式單例餓漢式單例類在類初始化時(shí),已經(jīng)自行實(shí)例化靜態(tài)工廠方法餓漢式在類創(chuàng)建的同時(shí)就已經(jīng)創(chuàng)建好一個(gè)靜態(tài)的對(duì)象供系統(tǒng)使用,以后不再改變,所以天生是線程安全的。 概念: Java中單例模式是一種常見的設(shè)計(jì)模式,單例模式的寫法有好幾種,這里主要介紹兩種:懶漢式單例、餓漢式單例?! 卫J接幸韵绿攸c(diǎn): 1、單例類只能有一個(gè)實(shí)例?!?..
摘要:一般來說,這種單例實(shí)現(xiàn)有兩種思路,私有構(gòu)造器,枚舉。而這種方式又分了飽漢式,餓漢式。通過關(guān)鍵字防止指令重排序。什么是單例?為什么要用單例? 一個(gè)類被設(shè)計(jì)出來,就代表它表示具有某種行為(方法),屬性(成員變量),而一般情況下,當(dāng)我們想使用這個(gè)類時(shí),會(huì)使用new關(guān)鍵字,這時(shí)候jvm會(huì)幫我們構(gòu)造一個(gè)該類的實(shí)例。而我們知道,對(duì)于new這個(gè)關(guān)鍵字以及該實(shí)例,相對(duì)而言是比較耗費(fèi)資源的。所以如果我們能夠想...
閱讀 823·2021-11-18 10:02
閱讀 2542·2021-11-11 16:54
閱讀 2765·2021-09-02 09:45
閱讀 663·2019-08-30 12:52
閱讀 2791·2019-08-29 14:04
閱讀 2757·2019-08-29 12:39
閱讀 460·2019-08-29 12:27
閱讀 1897·2019-08-26 13:23