
閱讀原文
在介紹單例模式之前,我們先了解一下,什么是設(shè)計模式?
設(shè)計模式(Design Pattern):是一套被反復(fù)使用,多數(shù)人知曉的,經(jīng)過分類編目的,代碼設(shè)計經(jīng)驗的總結(jié)。
目的:使用設(shè)計模式是為了可重用性代碼,讓代碼更容易被他人理解,保證代碼可靠性。
本文將會用到的關(guān)鍵詞:
- 單例:Singleton
- 實例:instance
- 同步:synchronized
- 類裝載器:ClassLoader
單例模式
單例,顧名思義就是只能有一個、不能再出現(xiàn)第二個。就如同地球上沒有兩片一模一樣的樹葉一樣。
在這里就是說:一個類只能有一個實例,并且整個項目系統(tǒng)都能訪問該實例。
單例模式共分為兩大類:
- 懶漢模式:實例在第一次使用時創(chuàng)建
- 餓漢模式:實例在類裝載時創(chuàng)建
單例模式UML圖

餓漢模式
按照定義我們可以寫出一個基本代碼:
public class Singleton {
// 使用private將構(gòu)造方法私有化,以防外界通過該構(gòu)造方法創(chuàng)建多個實例
private Singleton() {
}
// 由于不能使用構(gòu)造方法創(chuàng)建實例,所以需要在類的內(nèi)部創(chuàng)建該類的唯一實例
// 使用static修飾singleton 在外界可以通過類名調(diào)用該實例 類名.成員名
static Singleton singleton = new Singleton(); // 1
// 如果使用private封裝該實例,則需要添加get方法實現(xiàn)對外界的開放
private static Singleton instance = new Singleton(); // 2
// 添加static,將該方法變成類所有 通過類名訪問
public static Singleton getInstance(){
return instance;
}
//1和2選一種即可,推薦2
}
對于餓漢模式來說,這種寫法已經(jīng)很‘perfect’了,唯一的缺點就是,由于instance的初始化是在類加載時進行的,類加載是由ClassLoader來實現(xiàn)的,如果初始化太早,就會造成資源浪費。
當(dāng)然,如果所需的單例占用的資源很少,并且也不依賴于其他數(shù)據(jù),那么這種實現(xiàn)方式也是很好的。
類裝載的時機:
- new一個對象時
- 使用反射創(chuàng)建他的實例時
- 子類被加載時,如果父類還沒有加載,就先加載父類
- JVM啟動時執(zhí)行主類 會先被加載
懶漢模式
懶漢模式的代碼如下
// 代碼一
public class Singleton {
private static Singleton instance;
private Singleton(){
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
每次獲取instance之前先進行判斷,如果instance為空就new一個出來,否則就直接返回已存在的instance。這種寫法在大多數(shù)的時候也是沒問題的。問題在于,當(dāng)多線程工作的時候,如果有多個線程同時運行到if (instance == null),都判斷為null,那么兩個線程就各自會創(chuàng)建一個實例——這樣一來,就不是單例了。
這時我們需要使用synchronized,加上一個同步鎖
// 代碼二
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
加上synchronized關(guān)鍵字之后,getInstance方法就會鎖上了。如果有兩個線程(T1、T2)同時執(zhí)行到這個方法時,會有其中一個線程T1獲得同步鎖,得以繼續(xù)執(zhí)行,而另一個線程T2則需要等待,當(dāng)?shù)赥1執(zhí)行完畢getInstance之后(完成了null判斷、對象創(chuàng)建、獲得返回值之后),T2線程才會執(zhí)行執(zhí)行。
所以這端代碼也就避免了代碼一中,可能出現(xiàn)因為多線程導(dǎo)致多個實例的情況。但是,這種寫法也有一個問題:給getInstance方法加鎖,雖然會避免了可能會出現(xiàn)的多個實例問題,但是會強制除T1之外的所有線程等待,實際上會對程序的執(zhí)行效率造成負面影響。
雙重檢查(Double-Check)
代碼二相對于代碼一的效率問題,其實是為了解決1%幾率的問題,而使用了一個100%出現(xiàn)的防護盾。那有一個優(yōu)化的思路,就是把100%出現(xiàn)的防護盾,也改為1%的幾率出現(xiàn),使之只出現(xiàn)在可能會導(dǎo)致多個實例出現(xiàn)的地方。
代碼如下:
// 代碼三
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
這段代碼看起來有點復(fù)雜,注意其中有兩次if(instance==null)的判斷,這個叫做『雙重檢查 Double-Check』。
- 第一個if(instance==null),其實是為了解決Version2中的效率問題,只有instance為null的時候,才進入synchronized的代碼段大大減少了幾率。
- 第二個if(instance==null),則是跟代碼二一樣,是為了防止可能出現(xiàn)多個實例的情況。
這段代碼看起來已經(jīng)完美無瑕了?!?當(dāng)然,只是『看起來』,還是有小概率出現(xiàn)問題的。這弄清楚為什么這里可能出現(xiàn)問題,首先,我們需要弄清楚幾個概念:原子操作、指令重排。
原子操作
簡單來說,原子操作(atomic)就是不可分割的操作,在計算機中,就是指不會因為線程調(diào)度被打斷的操作。比如,簡單的賦值是一個原子操作:m = 6; // 這是個原子操作
假如m原先的值為0,那么對于這個操作,要么執(zhí)行成功m變成了6,要么是沒執(zhí)行m還是0,而不會出現(xiàn)諸如m=3這種中間態(tài)——即使是在并發(fā)的線程中。而,聲明并賦值就不是一個原子操作:int n=6;//這不是一個原子操作對于這個語句,至少有兩個操作:①聲明一個變量n②給n賦值為6——這樣就會有一個中間狀態(tài):變量n已經(jīng)被聲明了但是還沒有被賦值的狀態(tài)?!@樣,在多線程中,由于線程執(zhí)行順序的不確定性,如果兩個線程都使用m,就可能會導(dǎo)致不穩(wěn)定的結(jié)果出現(xiàn)。
指令重排
簡單來說,就是計算機為了提高執(zhí)行效率,會做的一些優(yōu)化,在不影響最終結(jié)果的情況下,可能會對一些語句的執(zhí)行順序進行調(diào)整。比如,這一段代碼:
int a ; // 語句1
a = 8 ; // 語句2
int b = 9 ; // 語句3
int c = a + b ; // 語句4
正常來說,對于順序結(jié)構(gòu),執(zhí)行的順序是自上到下,也即1234。但是,由于指令重排
的原因,因為不影響最終的結(jié)果,所以,實際執(zhí)行的順序可能會變成3124或者1324。
由于語句3和4沒有原子性的問題,語句3和語句4也可能會拆分成原子操作,再重排?!簿褪钦f,對于非原子性的操作,在不影響最終結(jié)果的情況下,其拆分成的原子操作可能會被重新排列執(zhí)行順序。
OK,了解了原子操作和指令重排的概念之后,我們再繼續(xù)看Version3代碼的問題。下面這段話直接從陳皓的文章(深入淺出單實例SINGLETON設(shè)計模式)中復(fù)制而來:主要在于singleton = new Singleton()這句,這并非是一個原子操作,事實上在 JVM 中這句話大概做了下面 3 件事情。
1. 給 singleton 分配內(nèi)存
2. 調(diào)用 Singleton 的構(gòu)造函數(shù)來初始化成員變量,形成實例
3. 將singleton對象指向分配的內(nèi)存空間(執(zhí)行完這步 singleton才是非 null了)但是在JVM的即時編譯器中存在指令重排序的優(yōu)化。
也就是說上面的第二步和第三步的順序是不能保證的,最終的執(zhí)行順序可能是 1-2-3 也可能是 1-3-2。如果是后者,則在 3 執(zhí)行完畢、2 未執(zhí)行之前,被線程二搶占了,這時 instance 已經(jīng)是非 null 了(但卻沒有初始化),所以線程二會直接返回 instance,然后使用,然后順理成章地報錯。
再稍微解釋一下,就是說,由于有一個『instance已經(jīng)不為null但是仍沒有完成初始化』的中間狀態(tài),而這個時候,如果有其他線程剛好運行到第一層if (instance ==null)這里,這里讀取到的instance已經(jīng)不為null了,所以就直接把這個中間狀態(tài)的instance拿去用了,就會產(chǎn)生問題。這里的關(guān)鍵在于——線程T1對instance的寫操作沒有完成,線程T2就執(zhí)行了讀操作。
對于代碼三出現(xiàn)的問題,解決方案為:給instance的聲明加上volatile關(guān)鍵字
代碼如下:
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
volatile關(guān)鍵字的一個作用是禁止指令重排,把instance聲明為volatile之后,對它的寫操作就會有一個內(nèi)存屏障(什么是內(nèi)存屏障?),這樣,在它的賦值完成之前,就不用會調(diào)用讀操作。
注意:volatile阻止的不singleton = new Singleton()這句話內(nèi)部[1-2-3]的指令重排,而是保證了在一個寫操作([1-2-3])完成之前,不會調(diào)用讀操作(if (instance == null))。
其它方法
靜態(tài)內(nèi)部類
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public >static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
這種寫法非常巧妙:對于內(nèi)部類SingletonHolder,它是一個餓漢式的單例實現(xiàn),在SingletonHolder初始化的時候會由ClassLoader來保證同步,使INSTANCE是一個真單例。
同時,由于SingletonHolder是一個內(nèi)部類,只在外部類的Singleton的getInstance()中被使用,所以它被加載的時機也就是在getInstance()方法第一次被調(diào)用的時候。
它利用了ClassLoader來保證了同步,同時又能讓開發(fā)者控制類加載的時機。從內(nèi)部看是一個餓漢式的單例,但是從外部看來,又的確是懶漢式的實現(xiàn)
枚舉
public enum SingleInstance {
INSTANCE;
public void fun1() {
// do something
}
}// 使用SingleInstance.INSTANCE.fun1();
是不是很簡單?而且因為自動序列化機制,保證了線程的絕對安全。三個詞概括該方式:簡單、高效、安全
這種寫法在功能上與共有域方法相近,但是它更簡潔,無償?shù)靥峁┝诵蛄谢瘷C制,絕對防止對此實例化,即使是在面對復(fù)雜的序列化或者反射攻擊的時候。雖然這中方法還沒有廣泛采用,但是單元素的枚舉類型已經(jīng)成為實現(xiàn)Singleton的最佳方法。