單例類在Java開發者中很是經常使用,可是它給初級開發者們形成了不少挑戰。他們所面對的其中一個關鍵挑戰是,怎樣確保單例類的行爲是單例?也就是說,不管任何緣由,如何防止單例類有多個實例。在整個應用生命週期中,要保證只有一個單例類的實例被建立,雙重檢查鎖(Double checked locking of Singleton)是一種實現方法。顧名思義,在雙重檢查鎖中,代碼會檢查兩次單例類是否有已存在的實例,一次加鎖一次不加鎖,一次確保不會有多個實例被建立。順便提一下,在JDK1.5中,Java修復了其內存模型的問題。在JDK1.5以前,這種方法會有問題。本文中,咱們將會看到怎樣用Java實現雙重檢查鎖的單例類,爲何Java 5以前的版本雙重檢查鎖會有問題,以及怎麼解決這個問題。順便說一下,這也是重要的面試要點,我曾經在金融業和服務業的公司面試被要求手寫雙重檢查鎖實現單例模式、相信我,這很棘手,除非你清楚理解了你在作什麼。你也能夠閱讀個人完整列表「單例模式設計問題」來更好的準備面試。html
一個常見情景,單例類在多線程環境中違反契約。若是你要一個新手寫出單例模式,可能會獲得下面的代碼:java
1 private static Singleton _instance; 2 3 public static Singleton getInstance() { 4 if (_instance == null) { 5 _instance = new Singleton(); 6 } 7 return _instance; 8 }
而後,當你指出這段代碼在超過一個線程並行被調用的時候會建立多個實例的問題時,他極可能會把整個getInstance()
方法設爲同步(synchronized),就像咱們展現的第二段示例代碼getInstanceTS()
方法同樣。儘管這樣作到了線程安全,而且解決了多實例問題,但並不高效。在任何調用這個方法的時候,你都須要承受同步帶來的性能開銷,然而同步只在第一次調用的時候才被須要,也就是單例類實例建立的時候。這將促使咱們使用雙重檢查鎖模式(double checked locking pattern),一種只在臨界區代碼加鎖的方法。程序員稱其爲雙重檢查鎖,由於會有兩次檢查 _instance == null
,一次不加鎖,另外一次在同步塊上加鎖。這就是使用Java雙重檢查鎖的示例:程序員
1 public static Singleton getInstanceDC() { 2 if (_instance == null) { // Single Checked 3 synchronized (Singleton.class) { 4 if (_instance == null) { // Double checked 5 _instance = new Singleton(); 6 } 7 } 8 } 9 return _instance; 10 }
這個方法表面上看起來很完美,你只須要付出一次同步塊的開銷,但它依然有問題。除非你聲明_instance
變量時使用了volatile關鍵字。沒有volatile
修飾符,可能出現Java中的另外一個線程看到個初始化了一半的_instance
的狀況,但使用了volatile
變量後,就能保證先行發生關係(happens-before relationship)。對於volatile
變量_instance
,全部的寫(write)都將先行發生於讀(read),在Java 5以前不是這樣,因此在這以前使用雙重檢查鎖有問題。如今,有了先行發生的保障(happens-before guarantee),你能夠安全地假設其會工做良好。另外,這不是建立線程安全的單例模式的最好方法,你能夠使用枚舉實現單例模式,這種方法在實例建立時提供了內置的線程安全。另外一種方法是使用靜態持有者模式(static holder pattern)。面試
1 /* 2 * A journey to write double checked locking of Singleton class in Java. 3 */ 4 5 class Singleton { 6 7 private volatile static Singleton _instance; 8 9 private Singleton() { 10 // preventing Singleton object instantiation from outside 11 } 12 13 /* 14 * 1st version: creates multiple instance if two thread access 15 * this method simultaneously 16 */ 17 18 public static Singleton getInstance() { 19 if (_instance == null) { 20 _instance = new Singleton(); 21 } 22 return _instance; 23 } 24 25 /* 26 * 2nd version : this definitely thread-safe and only 27 * creates one instance of Singleton on concurrent environment 28 * but unnecessarily expensive due to cost of synchronization 29 * at every call. 30 */ 31 32 public static synchronized Singleton getInstanceTS() { 33 if (_instance == null) { 34 _instance = new Singleton(); 35 } 36 return _instance; 37 } 38 39 /* 40 * 3rd version : An implementation of double checked locking of Singleton. 41 * Intention is to minimize cost of synchronization and improve performance, 42 * by only locking critical section of code, the code which creates instance of Singleton class. 43 * By the way this is still broken, if we don't make _instance volatile, as another thread can 44 * see a half initialized instance of Singleton. 45 */ 46 47 public static Singleton getInstanceDC() { 48 if (_instance == null) { 49 synchronized (Singleton.class) { 50 if (_instance == null) { 51 _instance = new Singleton(); 52 } 53 } 54 } 55 return _instance; 56 } 57 }
這就是本文的全部內容了。這是個用Java建立線程安全單例模式的有爭議的方法,使用枚舉實現單例類更簡單有效。我並不建議你像這樣實現單例模式,由於用Java有許多更好的方式。可是,這個問題有歷史意義,也教授了併發是如何引入一些微妙錯誤的。正如以前所說,這是面試中很是重要的一點。在去參加任何Java面試以前,要練習手寫雙重檢查鎖實現單例類。這將加強你發現Java程序員們所犯編碼錯誤的洞察力。另外,在如今的測試驅動開發中,單例模式因爲難以被模擬其行爲而被視爲反模式(anti pattern),因此若是你是測試驅動開發的開發者,最好避免使用單例模式。安全