單例模式
單例物件(Singleton)是一種常用的設計模式。在Java應用中,單例物件能保證在一個JVM中,該物件只有一個例項存在。這樣的模式有幾個好處:
-
某些類建立比較頻繁,對於一些大型的物件,這是一筆很大的系統開銷。
-
省去了new操作符,降低了系統記憶體的使用頻率,減輕GC壓力。
-
有些類如交易所的核心交易引擎,控制著交易流程,如果該類可以建立多個的話,系統完全亂了。(比如一個軍隊出現了多個司令員同時指揮,肯定會亂成一團),所以只有使用單例模式,才能保證核心交易伺服器獨立控制整個流程。
單例模式可以分為懶漢式 和餓漢式 :
- 懶漢式單例模式:在類載入時不初始化。
- 餓漢式單例模式:在類載入時就完成了初始化,所以類載入比較慢,但獲取物件的速度快。
第一種(懶漢,執行緒不安全):
public class SingletonDemo1 { private static SingletonDemo1 instance; private SingletonDemo1(){} public static SingletonDemo1 getInstance(){ if (instance == null) { instance = new SingletonDemo1(); } return instance; } }
這種寫法lazy loading很明顯,但是致命的是在多執行緒不能正常工作。
第二種(懶漢,執行緒安全):
public class SingletonDemo2 { private static SingletonDemo2 instance; private SingletonDemo2(){} public static synchronized SingletonDemo2 getInstance(){ if (instance == null) { instance = new SingletonDemo2(); } return instance; } }
這種寫法在getInstance()方法中加入了synchronized鎖。能夠在多執行緒中很好的工作,而且看起來它也具備很好的lazy loading,但是效率很低(因為鎖),並且大多數情況下不需要同步。
第三種(餓漢):
public class SingletonDemo3 { private static SingletonDemo3 instance = new SingletonDemo3(); private SingletonDemo3(){} public static SingletonDemo3 getInstance(){ return instance; } }
這種方式基於classloder機制避免了多執行緒的同步問題,不過,instance在類裝載時就例項化,這時候初始化instance顯然沒有達到lazy loading的效果。
第四種(餓漢,變種):
public class SingletonDemo4 { private static SingletonDemo4 instance = null; static{ instance = new SingletonDemo4(); } private SingletonDemo4(){} public static SingletonDemo4 getInstance(){ return instance; } }
表面上看起來差別挺大,其實跟第三種方式差不多,都是在類初始化即例項化instance
第五種(靜態內部類):
public class SingletonDemo5 { private static class SingletonHolder{ private static final SingletonDemo5 instance = new SingletonDemo5(); } private SingletonDemo5(){} public static final SingletonDemo5 getInsatance(){ return SingletonHolder.instance; } }
這種方式同樣利用了classloder的機制來保證初始化instance時只有一個執行緒,它跟第三種和第四種方式不同的是(很細微的差別):第三種和第四種方式是隻要Singleton類被裝載了,那麼instance就會被例項化(沒有達到lazy loading效果),而這種方式是Singleton類被裝載了,instance不一定被初始化。因為SingletonHolder類沒有被主動使用,只有顯示通過呼叫getInstance方法時,才會顯示裝載SingletonHolder類,從而例項化instance。想象一下,如果例項化instance很消耗資源,我想讓他延遲載入,另外一方面,我不希望在Singleton類載入時就例項化,因為我不能確保Singleton類還可能在其他的地方被主動使用從而被載入,那麼這個時候例項化instance顯然是不合適的。這個時候,這種方式相比第三和第四種方法就顯得更合理。
如果類的載入機制還不是很瞭解的同學,可以看我的另一篇文章深入理解Java虛擬機器之類載入機制
第六種(列舉):
public enum SingletonDemo6 { instance; public void whateverMethod(){ } }
這種方式是Effective Java作者Josh Bloch 提倡的方式,它不僅能避免多執行緒同步問題,而且還能防止反序列化重新建立新的物件,可謂是很堅強的壁壘啊,不過,個人認為由於1.5中才加入enum特性,用這種方式寫不免讓人感覺生疏,在實際工作中,我也很少看見有人這麼寫過。
第七種(雙重校驗鎖):
public class SingletonDemo7 { private volatile static SingletonDemo7 singletonDemo7; private SingletonDemo7(){} public static SingletonDemo7 getSingletonDemo7(){ if (singletonDemo7 == null) { synchronized (SingletonDemo7.class) { if (singletonDemo7 == null) { singletonDemo7 = new SingletonDemo7(); } } } return singletonDemo7; } }
接下來我們詳細介紹一下為什麼要引入雙重鎖機制
-
情況1:不加鎖
執行緒不安全,很容易理解,這裡就不做贅述了 - 情況2:只加一個鎖,例如
public static Singleton getInstance(){ if (instance == null) { synchronized (Singleton.class) { instance = new Singleton(); } } return instance; }
當 instance 為 null 時,兩個執行緒可以併發地進入 if 語句內部。然後,一個執行緒進入 synchronized 塊來初始化 instance ,而另一個執行緒則被阻斷。當第一個執行緒退出 synchronized 塊時,等待著的執行緒進入並建立另一個 Singleton 物件。
注意:當第二個執行緒進入 synchronized 塊時,它並沒有檢查 instance 是否非 null,此時就建立兩個不同的Singleton物件
此時我們只需要對 instance 進行第二次檢查。這就是“雙重檢查鎖定”名稱的由來。
public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) {//1 if (instance == null)//2 instance = new Singleton();//3 } } return instance; }
雙重檢查鎖定背後的理論是:在 //2 處的第二次檢查使建立兩個不同的 Singleton 物件成為不可能。假設有下列事件序列:
- 執行緒 1 進入 getInstance() 方法。
- 由於 instance 為 null ,執行緒 1 在 //1 處進入 synchronized 塊。
- 執行緒 1 被執行緒 2 預佔。
- 執行緒 2 進入 getInstance() 方法。
- 由於 instance 仍舊為 null ,執行緒 2 試圖獲取 //1 處的鎖。然而,由於執行緒 1 持有該鎖,執行緒 2 在 //1 處阻塞。
- 執行緒 2 被執行緒 1 預佔。
- 執行緒 1 執行,由於在 //2 處例項仍舊為 null ,執行緒 1 還建立一個 Singleton 物件並將其引用賦值給 instance 。
- 執行緒 1 退出 synchronized 塊並從 getInstance() 方法返回例項。
- 執行緒 1 被執行緒 2 預佔。
- 執行緒 2 獲取 //1 處的鎖並檢查 instance 是否為 null 。
- 由於 instance 是非 null 的,並沒有建立第二個 Singleton 物件,由執行緒 1 建立的物件被返回。