傳統單例模式雙重檢查鎖存在的問題
阿新 • • 發佈:2018-11-30
單例模式1.0:
public class Singleton { private static Singleton sInstance; public static Singleton getInstance() { if (sInstance == null) { // 1 sInstance = new Singleton(); } return sInstance; } private Singleton() { } }
這種方式很辣雞,因為多執行緒環境下不能保證單例。
單例模式2.0:
public class Singleton { private static volatile Singleton sInstance; public static synchronized Singleton getInstance() { if (sInstance == null) { sInstance = new Singleton(); } return sInstance; } private Singleton() { } }
這種方式也很辣雞,因為多執行緒環境下每個執行緒執行getInstance()都要阻塞,效率很低。
單例模式3.0:
public class Singleton { private static Singleton sInstance; public static Singleton getInstance() { if (sInstance == null) { // 位置1 synchronized (Singleton.class) { if (sInstance == null) { sInstance = new Singleton(); // 位置2 } } } return sInstance; } private Singleton() {} }
這種方式使用雙重檢查鎖,多執行緒環境下執行getInstance()時先判斷單例物件是否已經初始化,如果已經初始化,就直接返回單例物件,如果未初始化,就在同步程式碼塊中先進行初始化,然後返回,效率很高。
但是這種方式是一個錯誤的優化,問題的根源出在位置2
sInstance =new Singleton();這句話建立了一個物件,他可以分解成為如下3行程式碼:
memory = allocate(); // 1.分配物件的記憶體空間 ctorInstance(memory); // 2.初始化物件 sInstance = memory; // 3.設定sInstance指向剛分配的記憶體地址
上述虛擬碼中的2和3之間可能會發生重排序,重排序後的執行順序如下
memory = allocate(); // 1.分配物件的記憶體空間 sInstance = memory; // 2.設定sInstance指向剛分配的記憶體地址,此時物件還沒有被初始化 ctorInstance(memory); // 3.初始化物件
因為這種重排序並不影響Java規範中的規範:intra-thread sematics允許那些在單執行緒內不會改變單執行緒程式執行結果的重排序。
但是多執行緒併發時可能會出現以下情況
執行緒B訪問到的是一個還未初始化的物件。
解決方案1:
public class Singleton { private static volatile Singleton sInstance; public static Singleton getInstance() { if (sInstance == null) { synchronized (Singleton.class) { if (sInstance == null) { sInstance = new Singleton(); } } } return sInstance; } private Singleton() {} }
將物件宣告為volatitle後,前面的重排序在多執行緒環境中將會被禁止
解決方案2:
public class Singleton { private Singleton(){}; private static class Inner{ private static Singleton SINGLETION=new Singleton(); } public static Singleton getInstance(){ return Inner.SINGLETION; } }
靜態內部類不會隨著外部類的初始化而初始化,他是要單獨去載入和初始化的,當第一次執行getInstance方法時,Inner類會被初始化。
靜態物件SINGLETION的初始化在Inner類初始化階段進行,類初始化階段即虛擬機器執行類構造器<clinit>()方法的過程。
虛擬機器會保證一個類的<clinit>()方法在多執行緒環境下被正確的加鎖和同步,如果多個執行緒同時初始化一個類,只會有一個執行緒執行這個類的<clinit>()方法,其它執行緒都會阻塞等待。