DCL單例模式中的缺陷及單例模式的其他實現

萌新J發表於2020-11-09

  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懶漢式更加簡單,同時也沒有加鎖解鎖操作,更加高效。

相關文章