單例模式都不陌生,有餓漢式單例、懶漢式單例等。
餓漢式單例:
public class Singleton { PRivate static Singleton instance = new Singleton(); public static Singleton getInstance(){ return instance; }}在類第一次加載的時候,單例就完成了初始化。下面來驗證餓漢式單例的線程安全性:
public class MyThread extends Thread{ public void run() { System.out.println(Singleton.getInstance().hashCode()); }}public class Test { public static void main(String[] args) throws Exception { Thread t1 = new MyThread(); Thread t2 = new MyThread(); Thread t3 = new MyThread(); t1.start(); t2.start(); t3.start(); }}運行得到:
763347431 763347431 763347431
三次得到的 hashcode() 返回值都一樣。
結論:餓漢式單例在類第一次加載的時候完成初始化,是線程安全的。
懶漢式單例:
public class Singleton { private static Singleton instance = null; public static Singleton getInstance(){ if(instance == null){ // 1 instance = new Singleton(); // 2 } // 3 return instance; }}運用了延遲加載,在需要的時候進行初始化。 然而 1、2、3 整體不具有原子性,所以懶漢式單例應該不是線程安全的。
下面證明懶漢式單例是非線程安全的:
public class MyThread extends Thread{ public void run() { System.out.println(Singleton.getInstance().hashCode()); }}public class Test { public static void main(String[] args) throws Exception { Thread t1 = new MyThread(); Thread t2 = new MyThread(); Thread t3 = new MyThread(); t1.start(); t2.start(); t3.start(); }}運行得到:
2146509683 810456228 1996534722
結論:懶漢式單例運用延遲加載在需要時候進行初始化,保證了特定情況下其性能要優于餓漢式單例。然而它卻是非線程安全的。
我們嘗試修改代碼,目的是把懶漢式單例修改成線程安全的。
第一次嘗試 為 getInstance() 加鎖:
public class Singleton { private static Singleton instance = null; synchronized public static Singleton getInstance(){ if(instance == null){ instance = new Singleton(); } return instance; }}運行得到:
2000544445 2000544445 2000544445
這樣修改可以保證線程安全性;但由于鎖的獨占性,多個線程頻繁調用 getIntance() 很可能會阻塞,效率低下。
第二次嘗試 繼續縮小同步塊的范圍:
public class Singleton { private static Singleton instance = null; public static Singleton getInstance() { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } return instance; }}運行得到:
略
hashcode() 返回值都一樣。
這種方法基本等同于第一次嘗試。
第三次嘗試 繼續縮小同步塊的范圍:
public class Singleton { private static Singleton instance = null; public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { instance = new Singleton(); } } return instance; }}運行得到:
1175759956 2000544445 2146509683
相比前兩次嘗試,第三次嘗試執行效率會有明顯提升;但是破壞了原子性,不是線程安全的,得到的可能不是單例。
第四次嘗試 雙重檢查鎖定機制(double check lock)(DCL):
public class Singleton { private static Singleton instance = null; public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; }}運行得到:
略
hashcode() 返回值都一樣。
此方案綜合了前面幾次嘗試的優點:
鎖同步保證了原子性,保證線程安全性;第一次空檢測對提升性能起到了很大的作用。但是真的實現了線程安全嗎?并沒有。
在 DCL 基礎上,為變量 instance 加上 voltile 關鍵字,才算是真正的實現了線程安全:
public class Singleton { private volatile static Singleton instance = null; public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; }}運行得到:
略
hashcode()返回值都一樣。
Q:回頭來看,為什么必須加 volatile 關鍵字呢? A:重排序
結論:懶漢式單例,經過基于 volatile 的 DCL 進行改造,能夠具有線程安全性。
volatile + synchronized 實現了線程安全性。DCL 的第一個空檢測很大程度上優化了性能增加一個 final 域同樣能解決問題:
public class Singleton { private static Singleton instance = null; private final int para; public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } private Singleton(){ para = 1; }}寫內存語義:在構造函數內對一個 final 域的寫入,與隨后將對象引用賦值給引用變量,這兩個操作不能重排序; 實現原理:在 final 域的寫之后,構造函數 return 之前,插入一個 StoreStore 屏障; 寫內存語義可以確保在對象的引用為任意線程可見之前,final 域已經被初始化過了。
讀內存語義:初次讀一個包含 final 域的對象的引用,與隨后初次讀這個 final 域,這兩個操作不能重排序; 實現原理:在讀 final 域之前插入一個 LoadLoad 屏障。 讀內存語義可以確保如果對象的引用不為 null,則說明 final 域已經被初始化過了。
總之,final 域的內存語義提供了初始化安全保證:只要 this 引用沒有在構造函數中“逸出”,不需要同步就可以保證任意線程看到的都是初始化后的值。 注意:此時 Singleton 并非不可變,引入 final 域的目的是能夠安全地初始化。
調用 getInstance() 導致 Holder 類被裝載,Holder 對應的的 Class 類型的對象自動創建,JVM 會獲得這個 Class 對象初始化鎖,這個鎖可以同步多個線程對同一個類的初始化。因此指令重排還是可能發生的,但是并不影響獲得初始化鎖的下一個線程,因為下一個線程進來的時候,上個線程已經完成了類的初始化。
看圖更形象一些:

可能得到的執行時序:

結論:靜態內部類實現的單例模式能夠保證線程安全,同時具有延遲加載特性。而且代碼夠簡潔哦。
新聞熱點
疑難解答