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