1.什么情況下使用單例模式痴腌?
有些對象只有一個,比如配置文件燃领,工具類士聪,線程池,緩存猛蔽,日志對象等等剥悟。單例模式保證應用中有且只有一個實例。
2. 什么是單例曼库?
2.1区岗、單例定義
“單例對象的類必須保證只有一個實例存在” 這是維基百科上對單例的定義,這也可以作為對意圖實現(xiàn)單例模式的代碼進行檢驗的標準毁枯。
2.2慈缔、單例的實現(xiàn)可以分為兩大類
懶漢式:指全局的單例實例在第一次被使用時構建。
餓漢式:指全局的單例實例在類裝載時構建种玛。
注:日常我們使用的較多的應該是懶漢式的單例胀糜,畢竟按需加載才能做到資源的最大化利用。
3. 懶漢式單例
先來看一下懶漢式單例的實現(xiàn)方式蒂誉。
3.1 簡單版本
看最簡單的寫法Version 1:
public class LazySingleton {
//1. Simplest version
private static LazySingleton instance;
private LazySingleton(){}
public static LazySingleton getInstance(){
if(instance == null){
instance = new LazySingleton();
}
return instance;
}
}
把構造器改為私有的,這樣能夠防止被外部的類調用距帅。每次獲取instance之前先進行判斷右锨,如果instance為空就new一個出來,否則就直接返回已存在的instance碌秸。這種寫法在大多數(shù)的時候也是沒問題的绍移。問題在于悄窃,當多線程工作的時候,如果有多個線程同時運行到if (instance == null)蹂窖,都判斷為null,那么兩個線程就各自會創(chuàng)建一個實例——這樣一來,就不是單例了端蛆。
3.2 synchronized版本
那既然可能會因為多線程導致問題床三,那么加上一個同步鎖吧!修改后的代碼如下月趟,相對于Version1灯蝴,只是在方法簽名上多加了一個synchronized:
//2. Sychronized version
private static LazySingleton instance2;
private LazySingleton(){}
public static synchronized LazySingleton getInstance2(){
if(instance2 == null){
instance2 = new LazySingleton();
}
return instance2;
}
OK,加上synchronized關鍵字之后孝宗,getInstance方法就會鎖上了穷躁。如果有兩個線程(T1、T2)同時執(zhí)行到這個方法時因妇,會有其中一個線程T1獲得同步鎖问潭,得以繼續(xù)執(zhí)行,而另一個線程T2則需要等待婚被,當?shù)赥1執(zhí)行完畢getInstance之后(完成了null判斷狡忙、對象創(chuàng)建、獲得返回值之后)摔寨,T2線程才會執(zhí)行執(zhí)行去枷。——所以這端代碼也就避免了Version1中是复,可能出現(xiàn)因為多線程導致多個實例的情況删顶。但是,這種寫法也有一個問題:給getInstance方法加鎖淑廊,雖然會避免了可能會出現(xiàn)的多個實例問題逗余,但是會強制除T1之外的所有線程等待,實際上會對程序的執(zhí)行效率造成負面影響季惩。
3.3 雙重檢查(Double-Check)版本
Version2代碼相對于Version1d代碼的效率問題录粱,其實是為了解決1%幾率的問題,而使用了一個100%出現(xiàn)的防護盾画拾。那有一個優(yōu)化的思路啥繁,就是把100%出現(xiàn)的防護盾,也改為1%的幾率出現(xiàn)青抛,使之只出現(xiàn)在可能會導致多個實例出現(xiàn)的地方旗闽。——有沒有這樣的方法呢?當然是有的适室,改進后的代碼Vsersion3如下:
//3. Double-check version
private static LazySingleton instance3;
private LazySingleton(){}
public static LazySingleton getInstance3(){
if(instance3 == null){
synchronized (LazySingleton.class){
if(instance3 == null){
instance3 = new LazySingleton();
}
}
}
return instance3;
}
第一個if (instance == null)嫡意,其實是為了解決Version2中的效率問題,只有instance為null的時候捣辆,才進入synchronized的代碼段大大減少了幾率蔬螟。
第二個if (instance == null),則是跟Version2一樣汽畴,是為了防止可能出現(xiàn)多個實例的情況旧巾。
這段代碼看起來已經完美無瑕了≌………………—— 當然菠齿,只是『看起來』,還是有小概率出現(xiàn)問題的坐昙。這弄清楚為什么這里可能出現(xiàn)問題绳匀,首先,我們需要弄清楚幾個概念:原子操作炸客、指令重排疾棵。
主要在于singleton = new Singleton()這句,這并非是一個原子操作痹仙,事實上在 JVM 中這句話大概做了下面 3 件事情是尔。
1. 給 singleton 分配內存
2. 調用 Singleton 的構造函數(shù)來初始化成員變量,形成實例
3. 將singleton對象指向分配的內存空間(執(zhí)行完這步 singleton才是非 null 了)但是在 JVM 的即時編譯器中存在指令重排序的優(yōu)化开仰。
也就是說上面的第二步和第三步的順序是不能保證的拟枚,最終的執(zhí)行順序可能是 1-2-3 也可能是 1-3-2。如果是后者众弓,則在 3 執(zhí)行完畢恩溅、2 未執(zhí)行之前,被線程二搶占了谓娃,這時 instance 已經是非 null 了(但卻沒有初始化)脚乡,所以線程二會直接返回 instance,然后使用滨达,然后順理成章地報錯奶稠。
再稍微解釋一下,就是說捡遍,由于有一個『instance已經不為null但是仍沒有完成初始化』的中間狀態(tài)锌订,而這個時候,如果有其他線程剛好運行到第一層if (instance == null)這里画株,這里讀取到的instance已經不為null了瀑志,所以就直接把這個中間狀態(tài)的instance拿去用了涩搓,就會產生問題。這里的關鍵在于——線程T1對instance的寫操作沒有完成劈猪,線程T2就執(zhí)行了讀操作。
3.4 終極版本:volatile
對于Version3中可能出現(xiàn)的問題(當然這種概率已經非常小了良拼,但畢竟還是有的嘛~)战得,解決方案是:只需要給instance的聲明加上volatile關鍵字即可,Version4版本:
//4. Double-check with volatile version
private static volatile LazySingleton instance4;
private LazySingleton(){}
public static LazySingleton getInstance4(){
if(instance4 == null){
synchronized (LazySingleton.class){
if(instance4 == null){
instance4 = new LazySingleton();
}
}
}
return instance4;
}
一旦一個共享變量(類的成員變量庸推、類的靜態(tài)成員變量)被volatile修飾之后常侦,那么就具備了兩層語義:
1)可見性:保證了不同線程對這個變量進行操作時的可見性,即一個線程修改了某個變量的值贬媒,這新值對其他線程來說是立即可見的聋亡。
2)有序性:禁止進行指令重排序。
我的理解是际乘,volatile修飾后坡倔,保證了singleton = new Singleton()這句話的指令執(zhí)行順序,從而不會出現(xiàn)版本3的問題脖含。
4. 餓漢式單例
下面再聊了解一下餓漢式的單例罪塔。
如上所說,餓漢式單例是指:指全局的單例實例在類裝載時構建的實現(xiàn)方式养葵。
由于類裝載的過程是由類加載器(ClassLoader)來執(zhí)行的征堪,這個過程也是由JVM來保證同步的,所以這種方式先天就有一個優(yōu)勢——能夠免疫許多由多線程引起的問題关拒。
4.1 餓漢式單例的實現(xiàn)方式
餓漢式單例的實現(xiàn)如下:
public class HungreySingleton {
private static final HungreySingleton instance = new HungreySingleton();
private HungreySingleton(){}
public static HungreySingleton getInstance() {
return instance;
}
}
對于一個餓漢式單例的寫法來說佃蚜,它基本上是完美的了。所以它的缺點也就只是餓漢式單例本身的缺點所在了——由于INSTANCE的初始化是在類加載時進行的着绊,而類的加載是由ClassLoader來做的谐算,所以開發(fā)者本來對于它初始化的時機就很難去準確把握:可能由于初始化的太早,造成資源的浪費畔柔。
如果初始化本身依賴于一些其他數(shù)據(jù)氯夷,那么也就很難保證其他數(shù)據(jù)會在它初始化之前準備好。
當然靶擦,如果所需的單例占用的資源很少腮考,并且也不依賴于其他數(shù)據(jù),那么這種實現(xiàn)方式也是很好的玄捕。
4.2什么時候是類裝載時踩蔚?
類從被加載到虛擬機內存中開始,直到卸載出內存為止枚粘,它的整個生命周期包括了:加載馅闽、驗證、準備、解析福也、初始化局骤、使用和卸載這7個階段。其中暴凑,驗證峦甩、準備和解析這三個部分統(tǒng)稱為連接(linking)。
什么情況下需要開始類加載過程的第一個階段:"加載"现喳。虛擬機規(guī)范中并沒強行約束凯傲,這點可以交給虛擬機的的具體實現(xiàn)自由把握,但是對于初始化階段虛擬機規(guī)范是嚴格規(guī)定了如下幾種情況嗦篱,如果類未初始化會對類進行初始化冰单。
- 創(chuàng)建類的實例
- 訪問類的靜態(tài)變量(除常量【被final修辭的靜態(tài)變量】原因:常量一種特殊的變量,因為編譯器把他們當作值(value)而不是域(field)來對待灸促。如果你的代碼中用到了常變量(constant variable)诫欠,編譯器并不會生成字節(jié)碼來從對象中載入域的值,而是直接把這個值插入到字節(jié)碼中腿宰。這是一種很有用的優(yōu)化呕诉,但是如果你需要改變final域的值那么每一塊用到那個域的代碼都需要重新編譯。
- 訪問類的靜態(tài)方法
- 反射如(Class.forName("my.xyz.Test"))
- 當初始化一個類時吃度,發(fā)現(xiàn)其父類還未初始化甩挫,則先出發(fā)父類的初始化
- 虛擬機啟動時,定義了main()方法的那個類先初始化
class SingleTon {
private static SingleTon singleTon = new SingleTon();
public static int count1;
public static int count2 = 0;
private SingleTon() {
count1++;
count2++;
}
public static SingleTon getInstance() {
return singleTon;
}
}
public class Test {
public static void main(String[] args) {
SingleTon singleTon = SingleTon.getInstance();
System.out.println("count1=" + singleTon.count1);
System.out.println("count2=" + singleTon.count2);
}
}
輸出:
count1=1
count2=0
分析:
1:SingleTon singleTon = SingleTon.getInstance();調用了類的SingleTon調用了類的靜態(tài)方法椿每,觸發(fā)類的初始化
2:類加載的時候在準備過程中為類的靜態(tài)變量分配內存并初始化默認值 singleton=null count1=0,count2=0
3:類初始化伊者,為類的靜態(tài)變量賦值和執(zhí)行靜態(tài)代碼快。singleton賦值為new SingleTon()調用類的構造方法
4:調用類的構造方法后count=1;count2=1
5:繼續(xù)為count1與count2賦值,此時count1沒有賦值操作,所有count1為1,但是count2執(zhí)行賦值操作就變?yōu)?
5. 一些其他的實現(xiàn)方式
5.1 Effective Java 1 —— 靜態(tài)內部類
《Effective Java》一書的第一版中推薦了一個中寫法:
public class InnerSingleton {
private static class SingletonHolder{
private static final InnerSingleton instance = new InnerSingleton();
}
private InnerSingleton(){}
public static final InnerSingleton getInstance(){
return SingletonHolder.instance;
}
}
這種寫法非常巧妙:對于內部類SingletonHolder间护,它是一個餓漢式的單例實現(xiàn)亦渗,在SingletonHolder初始化的時候會由ClassLoader來保證同步,使INSTANCE是一個真·單例汁尺。
同時法精,由于SingletonHolder是一個內部類,只在外部類的Singleton的getInstance()中被使用痴突,所以它被加載的時機也就是在getInstance()方法第一次被調用的時候搂蜓。
它利用了ClassLoader來保證了同步,同時又能讓開發(fā)者控制類加載的時機辽装。從內部看是一個餓漢式的單例帮碰,但是從外部看來,又的確是懶漢式的實現(xiàn)**拾积。簡直是神乎其技殉挽。
5.2 Effective Java 2 —— 枚舉
《Effective Java》的作者在這本書的第二版又推薦了另外一種方法丰涉,來直接看代碼:
public enum SingleInstance {
INSTANCE;
public void fun1() {
// do something
}
}// 使用SingleInstance.INSTANCE.fun1();
看到了么?這是一個枚舉類型……連class都不用了斯碌,極簡一死。由于創(chuàng)建枚舉實例的過程是線程安全的,所以這種寫法也沒有同步的問題傻唾。
對這個方法的評價:
這種寫法在功能上與共有域方法相近摘符,但是它更簡潔,無償?shù)靥峁┝诵蛄谢瘷C制策吠,絕對防止對此實例化,即使是在面對復雜的序列化或者反射攻擊的時候瘩绒。雖然這中方法還沒有廣泛采用猴抹,但是單元素的枚舉類型已經成為實現(xiàn)Singleton的最佳方法。
枚舉單例這種方法問世一些锁荔,許多分析文章都稱它是實現(xiàn)單例的最完美方法——寫法超級簡單蟀给,而且又能解決大部分的問題。不過我個人認為這種方法雖然很優(yōu)秀阳堕,但是它仍然不是完美的——比如跋理,在需要繼承的場景,它就不適用了恬总。
6. 總結
OK前普,看到這里,你還會覺得單例模式是最簡單的設計模式了么壹堰?再回頭看一下你之前代碼中的單例實現(xiàn)拭卿,覺得是無懈可擊的么?可能我們在實際的開發(fā)中贱纠,對單例的實現(xiàn)并沒有那么嚴格的要求峻厚。比如,我如果能保證所有的getInstance都是在一個線程的話谆焊,那其實第一種最簡單的教科書方式就夠用了惠桃。再比如,有時候辖试,我的單例變成了多例也可能對程序沒什么太大影響……但是辜王,如果我們能了解更多其中的細節(jié),那么如果哪天程序出了些問題剃执,我們起碼能多一個排查問題的點誓禁。早點解決問題,就能早點回家吃飯……:-D
還有肾档,完美的方案是不存在摹恰,任何方式都會有一個『度』的問題辫继。比如,你的覺得代碼已經無懈可擊了俗慈,但是因為你用的是JAVA語言姑宽,可能ClassLoader有些BUG啊……你的代碼誰運行在JVM上的,可能JVM本身有BUG啊……你的代碼運行在手機上闺阱,可能手機系統(tǒng)有問題啊……你生活在這個宇宙里炮车,可能宇宙本身有些BUG啊……o(╯□╰)o所以,盡力做到能做到的最好就行了酣溃。
感謝你花費了不少時間看到這里瘦穆,但愿你沒有覺得虛度。
本文僅對于原文作少許修改赊豌。
原文:
作者:博麟K
鏈接:http://www.reibang.com/p/d2755af464d2