原文地址java
在介紹單例模式以前,咱們先了解一下,什麼是設計模式? 設計模式(Design Pattern): 是一套被反覆使用,多數人知曉的,通過分類編目的,代碼設計經驗的總結。 目的: 使用設計模式是爲了可重用性代碼,讓代碼更容易被他人理解,保證代碼可靠性。設計模式
本文將會用到的關鍵詞:安全
單例模式:多線程
單例,顧名思義就是隻能有一個、不能再出現第二個。就如同地球上沒有兩片如出一轍的樹葉同樣。併發
在這裏就是說:一個類只能有一個實例,而且整個項目系統都能訪問該實例。函數
單例模式共分爲兩大類:優化
單例模式UML圖atom
按照定義咱們能夠寫出一個基本代碼:spa
public class Singleton {
// 使用private將構造方法私有化,以防外界經過該構造方法建立多個實例
private Singleton() {
}
// 因爲不能使用構造方法建立實例,因此須要在類的內部建立該類的惟一實例
// 使用static修飾singleton 在外界能夠經過類名調用該實例 類名.成員名
static Singleton singleton = new Singleton(); // 1
// 若是使用private封裝該實例,則須要添加get方法實現對外界的開放
private static Singleton instance = new Singleton(); // 2
// 添加static,將該方法變成類全部 經過類名訪問
public static Singleton getInstance(){
return instance;
}
//1和2選一種便可,推薦2
}
複製代碼
對於餓漢模式來講,這種寫法已經很‘perfect’了,惟一的缺點就是,因爲instance的初始化是在類加載時進行的,類加載是由ClassLoader來實現的,若是初始化太早,就會形成資源浪費。 固然,若是所需的單例佔用的資源不多,而且也不依賴於其餘數據,那麼這種實現方式也是很好的。線程
懶漢模式的代碼以下
// 代碼一
public class Singleton {
private static Singleton instance = null;
private Singleton(){
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
複製代碼
每次獲取instance以前先進行判斷,若是instance爲空就new一個出來,不然就直接返回已存在的instance。
這種寫法在單線程的時候是沒問題的。可是,當有多個線程一塊兒工做的時候,若是有兩個線程同時運行到 if (instance == null),都判斷爲null(第一個線程判斷爲空以後,並無繼續向下執行,當第二個線程判斷的時候instance依然爲空),最終兩個線程就各自會建立一個實例出來。這樣就破環了單例模式 實例的惟一性
要想保證明例的惟一性就須要使用 synchronized ,加上一個同步鎖
// 代碼二
public class Singleton {
private static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
synchronized(Singleton.class){
if (instance == null)
instance = new Singleton();
}
return instance;
}
}
複製代碼
加上synchronized關鍵字以後,getInstance方法就會鎖上了。若是有兩個線程(T一、T2)同時執行到這個方法時,會有其中一個線程T1得到同步鎖,得以繼續執行,而另外一個線程T2則須要等待,當第T1執行完畢getInstance以後(完成了null判斷、對象建立、得到返回值以後),T2線程纔會執行執行。
因此這段代碼也就避免了 代碼一 中,可能出現由於多線程致使多個實例的狀況。可是,這種寫法也有一個問題:給getInstance方法加鎖,雖然避免了可能會出現的多個實例問題,可是會強制除T1以外的全部線程等待,實際上會對程序的執行效率形成負面影響。
代碼二 相對於代碼一 的效率問題,實際上是爲了解決1%概率的問題,而使用了一個100%出現的防禦盾。那有一個優化的思路,就是把100%出現的防禦盾,也改成1%的概率出現,使之只出如今可能會致使多個實例出現的地方。 代碼以下:
// 代碼三
public class Singleton {
private static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null){
synchronized(Singleton.class){
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
複製代碼
這段代碼看起來有點複雜,注意其中有兩次if(instance==null)的判斷,這個叫作『雙重檢查 Double-Check』。
這段代碼看起來已經完美無瑕了。固然,只是『看起來』,仍是有小几率出現問題的。想要充分理解須要先弄清楚如下幾個概念:原子操做、指令重排。
原子操做
簡單來講,原子操做(atomic)就是不可分割的操做,在計算機中,就是指不會由於線程調度被打斷的操做。好比,簡單的賦值是一個原子操做:
m = 6; // 這是個原子操做
複製代碼
假如m原先的值爲0,那麼對於這個操做,要麼執行成功m變成了6,要麼是沒執行 m仍是0,而不會出現諸如m=3這種中間態——即便是在併發的線程中。
可是,聲明並賦值就不是一個原子操做:
int n=6;//這不是一個原子操做
複製代碼
對於這個語句,至少有兩個操做:①聲明一個變量n ②給n賦值爲6——這樣就會有一箇中間狀態:變量n已經被聲明瞭可是尚未被賦值的狀態。這樣,在多線程中,因爲線程執行順序的不肯定性,若是兩個線程都使用m,就可能會致使不穩定的結果出現。
指令重排
簡單來講,就是計算機爲了提升執行效率,會作的一些優化,在不影響最終結果的狀況下,可能會對一些語句的執行順序進行調整。好比,這一段代碼:
int a ; // 語句1
a = 8 ; // 語句2
int b = 9 ; // 語句3
int c = a + b ; // 語句4
複製代碼
正常來講,對於順序結構,執行的順序是自上到下,也即1234。可是,因爲指令重排 的緣由,由於不影響最終的結果,因此,實際執行的順序可能會變成3124或者1324。
因爲語句3和4沒有原子性的問題,語句3和語句4也可能會拆分紅原子操做,再重排。——也就是說,對於非原子性的操做,在不影響最終結果的狀況下,其拆分紅的原子操做可能會被從新排列執行順序。
OK,瞭解了原子操做和指令重排的概念以後,咱們再繼續看代碼三的問題。
主要在於singleton = new Singleton()這句,這並不是是一個原子操做,事實上在 JVM 中這句話大概作了下面 3 件事情。
在JVM的即時編譯器中存在指令重排序的優化。 也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序多是 1-2-3 也多是 1-3-2。若是是後者,則在 3 執行完畢、2 未執行以前,被線程二搶佔了,這時 instance 已是非 null 了(但卻沒有初始化),因此線程二會直接返回 instance,而後使用,而後瓜熟蒂落地報錯。 再稍微解釋一下,就是說,因爲有一個『instance已經不爲null可是仍沒有完成初始化』的中間狀態,而這個時候,若是有其餘線程恰好運行到第一層if (instance ==null)這裏,這裏讀取到的instance已經不爲null了,因此就直接把這個中間狀態的instance拿去用了,就會產生問題。這裏的關鍵在於線程T1對instance的寫操做沒有完成,線程T2就執行了讀操做。
對於代碼三出現的問題,解決方案爲:給instance的聲明加上volatile關鍵字
代碼以下:
public class Singleton {
private static volatile Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null){
synchronized(Singleton.class){
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
複製代碼
volatile 關鍵字的一個做用是禁止指令重排,把instance聲明爲volatile以後,對它的寫操做就會有一個內存屏障,這樣,在它的賦值完成以前,就不用會調用讀操做。
注意:volatile阻止的不是singleton = new Singleton()這句話內部[1-2-3]的指令重排,而是保證了在一個寫操做([1-2-3])完成以前,不會調用讀操做(if (instance == null))。
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
複製代碼
這種寫法的巧妙之處在於:對於內部類SingletonHolder,它是一個餓漢式的單例實現,在SingletonHolder初始化的時候會由ClassLoader來保證同步,使INSTANCE是一個真單例。
同時,因爲SingletonHolder是一個內部類,只在外部類的Singleton的getInstance()中被使用,因此它被加載的時機也就是在getInstance()方法第一次被調用的時候。 它利用了ClassLoader來保證了同步,同時又能讓開發者控制類加載的時機。從內部看是一個餓漢式的單例,可是從外部看來,又的確是懶漢式的實現
public enum SingleInstance {
INSTANCE;
public void fun1() {
// do something
}
}// 使用SingleInstance.INSTANCE.fun1();
複製代碼
是否是很簡單?並且由於自動序列化機制,保證了線程的絕對安全。三個詞歸納該方式:簡單、高效、安全
這種寫法在功能上與共有域方法相近,可是它更簡潔,無償地提供了序列化機制,絕對防止對此實例化,即便是在面對複雜的序列化或者反射攻擊的時候。雖然這中方法尚未普遍採用,可是單元素的枚舉類型已經成爲實現Singleton的最佳方法。