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

  • 2020 年 11 月 9 日
  • 筆記

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