原文: Gerrard_Fenghtml
二:2-懶漢模式(Lazy)
思想:相比於餓漢模式,懶漢模式實際中的應用更多,由於在系統中,「被用到時再初始化」是更佳的解決方案。java
設計思想與餓漢模式相似,一樣是持有一個自身的引用,只是將 new 的動做延遲到 getinstance() 方法中執行。多線程
public final class LazySingleton { private static LazySingleton instance; private LazySingleton() { if (instance != null) { throw new IllegalStateException(); } } public static synchronized LazySingleton getInstance() { if (instance == null) { instance = new LazySingleton(); } return instance; } }
反射可否打破單例?
對於 LazySingleton,這是個頗有趣的問題,雖然咱們在私有構造器中增長了 instance==null 的判斷,可是因爲延遲加載的緣由,使得它沒法完美地規避反射的入侵。性能
這涉及到了反射入侵和 getInstance() 方法調用順序的問題。若是在調用 getInstance() 方法以前進行反射入侵,那麼就會打破單例,反之,能夠保證單例。優化
public class LazySingletonTest { [@Test](https://my.oschina.net/azibug) public void testReflectSuccess() throws Exception { Constructor<?> constructor = LazySingleton1.class.getDeclaredConstructor(); constructor.setAccessible(true); LazySingleton1 singleton1 = (LazySingleton1) constructor.newInstance(); LazySingleton1 singleton2 = LazySingleton1.getInstance(); Assert.assertNotSame(singleton1, singleton2); } [@Test](https://my.oschina.net/azibug) public void testReflectFailure() throws Exception { LazySingleton1 singleton1 = LazySingleton1.getInstance(); Constructor<?> constructor = LazySingleton1.class.getDeclaredConstructor(); constructor.setAccessible(true); try { LazySingleton1 singleton2 = (LazySingleton1) constructor.newInstance(); Assert.fail(); } catch (Exception e) { // Do nothing, test pass } } }
爲何是 synchronized 方法?
由於是延遲加載,考慮到多線程狀況,須要對方法同步。.net
同步方法帶來的性能問題?
可使用 synchronized 代碼塊 + Double-check Locking + volatile 關鍵字,對 LazySingleton 進行深一步優化:線程
Step1:基礎的懶漢模式翻譯
public class LazySingleton { private static LazySingleton instance = null; private LazySingleton() { } public static LazySingleton getInstance() { if (instance == null) { instance = new LazySingleton(); } return instance; } }
基礎的懶漢模式保證了在調用 getInstance() 方法的時候才第一次初始化單例對象。設計
可是這麼作沒法保證在多線程環境下只建立一個對象。code
顯然,假設有多個線程同時調用 getInstance() 方法,在第一個線程執行完畢以前,會有多個 LazyInstance 對象被建立。
Step2:爲 getInstance() 方法加上同步鎖
public class LazySingleton { private static LazySingleton instance = null; private LazySingleton() { } public synchronized static LazySingleton getInstance() { if (instance == null) { instance = new LazySingleton(); } return instance; } }
經過簡單地在方法上加上同步鎖,能夠保證同時只有一個線程調用這個靜態方法,從而保證在多線程環境下的單例。
然而這麼作有明顯的 性能 隱患。
假設有多個線程想要獲取 instance,不管此時對象是否已經被建立,都要頻繁地獲取鎖,釋放鎖。這種作法很影響效率。
Step3:在 getInstance() 方法內部增長同步代碼塊
public class LazySingleton { private static LazySingleton instance = null; private LazySingleton() { } public static LazySingleton getInstance() { if (instance == null) { synchronized (LazySingleton.class) { instance = new LazySingleton(); } } return instance; } }
既然在方法上加同步鎖不合適,那麼就在方法內部增長同步代碼塊。
在判斷 instance == null 以後,增長的同步代碼塊就不會產生 performance 問題,由於以後的訪問會直接 return,不會進入同步代碼塊。
可是這麼作,不能完整地保證單例。
參照 Step1,假設有多線程調用,且都經過了 instance == null 的判斷,那麼同樣會有多個 LazySingleton 對象被建立。
Step4:使用 Double-Checked Locking
public class LazySingleton { private static LazySingleton instance = null; private LazySingleton() { } public static LazySingleton getInstance() { if (instance == null) { synchronized (LazySingleton.class) { if (instance == null) { instance = new LazySingleton(); } } } return instance; } }
經過增長雙重判斷( Double-Checked Locking),以及同步代碼塊,就能夠避免 Step3 中可能出現的隱患。
可是 Double-Checked Locking 雖然可以保證單例的建立,可是在多線程的狀況下可能出現某個線程使用建立不徹底的對象的狀況。
Step5:使用 volatile 關鍵字修飾字段 instance
public class LazySingleton { private static volatile LazySingleton instance = null; private LazySingleton() { } public static LazySingleton getInstance() { if (instance == null) { synchronized (LazySingleton.class) { if (instance == null) { instance = new LazySingleton(); } } } return instance; } }
參考文檔:The "Double-Checked Locking is Broken" Declaration
若是不適應英文描述,ImportNew 對這篇文檔進行了翻譯:能夠不要再使用Double-Checked Locking了
這裏面講述了 Double-Checked Locking 在懶漢模式下可能出現的問題。
主要問題在於 Java 指令重排。
當 Java 代碼被編譯器翻譯成字節碼被存儲在 JVM 時,爲了提升性能,編譯器會對這些操做指令進行指令重排。
也就是說,代碼在計算機上執行的順序,會被打亂。
返回到本例的問題,懶漢模式最關鍵的2個操做:
1.在 heap 中建立一個 LazyInstance 對象。 2.爲字段 instance 賦值。 假設操做1在操做2以前被執行,那麼代碼就沒有問題。
反之若操做2在操做1以前被執行,若是不能保證建立 LazyInstance 對象的過程是原子的,那麼代碼仍是會出現問題,由於 instance 指向了一個沒有被建立徹底的對象。
事實上,引用類型和64位類型(long 和 double)都不能被原子地讀寫。
解決方案是經過 volatile 關鍵字來禁止指令重排(這是 volatile 的兩個做用之一,另外一個做用是保證共享變量的可見性,這裏不深刻展開)
優點?劣勢?
優點:延遲加載。
劣勢:不能徹底屏蔽反射入侵,並且代碼較爲繁瑣。