DCL:Double Check Lock ,意為雙重檢查鎖。在單例模式中懶漢式中可以使用DCL來保證程式執行的效率。
1 public class SingletonDemo { 2 private static SingletonDemo singletonDemo = null; 3 private SingletonDemo(){ 4 } 5 6 public SingletonDemo getSingletonDemo(){ 7 if(singletonDemo == null){ 8 synchronized (SingletonDemo.class){ 9 if(singletonDemo == null){ 10 singletonDemo = new SingletonDemo(); 11 } 12 } 13 } 14 return singletonDemo; 15 } 16 }
上面是傳統的DCL單例模式一種實現,第一個非空判斷是為了避免例項屬性已經例項化賦值後,後面的執行緒依然進入 synchronized 修飾的程式碼塊,進行加鎖、解鎖,造成效率低下;第二個非空判斷是為了避免例項屬性已經賦值後,等待佇列中的執行緒重複執行物件建立與賦值。而DCL可以保證多執行緒下只會進行一次物件初始化。但是這樣的程式碼還是會有缺陷。
缺陷
指令集
讓我們單獨寫一個物件建立方法,看一下這個操作對應的指令集是什麼
public void aa(){ singletonDemo = new SingletonDemo(); }
先對這個類進行編譯,然後使用idea 的 jclasslib外掛對這個類進行檢視,指令集如下:
指令分別是 new 、dup、invokespecial、putstatic,最後return返回,這裡dup是複製操作,也就是對棧頂的元素複製一份,這裡可以忽略不看。所以關鍵的指令是三個,1、new(例項化,對物件進行宣告,分配地址),2、invokespecial(初始化,呼叫構造方法,進行屬性賦值),3、putstatic(將這個物件賦值給屬性singletonDemo)。
指令重排
單執行緒下存在著指令重排,什麼是指令重排,比如說程式碼 x=1; y=1; x++; y++; y=x+y; JVM在載入時可能先載入到 y,那麼它不會再去等待x載入,直接去執行 y++ ,這樣就提高了運算效率,這種程式碼衝排序就是指令重排。但同時指令重排也不是隨意的重排,它會遵守資料依賴性,比如雖然先載入了y,執行了 y++ ,也載入了 x,但是並不會接著去執行 y=x+y;因為右邊操作的y 在前面的 y++修改了值,所以產生了對y++資料依賴,JVM並不會允許這樣的指令重排(其實這個例子裡的x++,y++指令會劃分為三步,這裡只需要知道表達的意思就可以了)。但是在多執行緒下資料依賴性就不能保證執行緒安全問題了。
回到前面的物件建立的指令集,2和3因為不存在資料依賴所以可能發生指令重排,所以在多執行緒下,可能執行緒1在執行new指令後直接執行指令3,執行緒2就執行到第7行第一個非空判斷了,此時因為物件地址分配了,所以判斷是非空,直接return,但是此時還沒有執行初始化指令,所以該物件只是分配了空間還沒有建立完物件,導致這個方法還是返回了一個null值(這個null值表示的是該位置的物件實際是獲取不到的但是判斷是非空的)。
解決
volatile 關鍵字可以禁止指令重排和保證可見性,但是由於不能保證原子性,所以在這裡還是需要配合 synchronized 來使用。關於volatile在多執行緒基礎裡面說到了,所以這裡最終的程式碼是:
1 public class SingletonDemo { 2 private volatile static SingletonDemo singletonDemo = null; 3 private SingletonDemo(){ 4 } 5 6 public SingletonDemo getSingletonDemo(){ 7 if(singletonDemo == null){ 8 synchronized (SingletonDemo.class){ 9 if(singletonDemo == null){ 10 singletonDemo = new SingletonDemo(); 11 } 12 } 13 } 14 return singletonDemo; 15 } 16 }
補充
單例模式的其他實現:
餓漢式
由於餓漢式是類載入時就會將物件例項建立賦值完成,所以在多執行緒下也是安全的,所以它的優點是不存線上程安全問題,缺點是沒有延遲載入的優勢,比如這個單例模式物件是一開始就載入好的,但是整個程式執行過程中過了很久才用上,那麼從類被載入時就建立在堆中,一直到被用上,在堆中是一直佔用空間的,如果存在多個餓漢式的單例類,就無形提高了GC發生的次數。降低程式的效能。
1、直接例項化餓漢式
public class Singleton1 { private static final Singleton1 INSTANCE=new Singleton1(); private Singleton1() { } public static Singleton1 getSingleton() { return INSTANCE; } }
特點:簡單直接
2、列舉式餓漢式
public enum Singleton12 { INSTANCE; public void aa() { //要呼叫的方法 } }
特點:最簡潔
3、靜態程式碼塊餓漢式
public class Singleton13 { private static final Singleton13 INSTANCE; static { INSTANCE=new Singleton13(); } public static Singleton13 getSingleton() { return INSTANCE; } }
特點:可以在類初始化時增加其他操作
懶漢式
懶漢式單例模式就是直到呼叫方法去獲取物件時才會建立物件,會有執行緒安全問題,所以更為複雜,但是因為是延遲載入,所以會有延遲載入的優勢。
1、DCL懶漢式
程式碼如上。
2、靜態內部類懶漢式
public class Singleton22 { private Singleton22() { } private static class Inner{ private static final Singleton22 INSTANCE=new Singleton22(); } public static Singleton22 getInstance() { return Inner.INSTANCE; } }
相比於DCL懶漢式更加簡單,同時也沒有加鎖解鎖操作,更加高效。