你是否瞭解單例模式在多線程下的DCL同步處理機制

多線程下的單例模式DCL安全機制

寫在前面:歡迎來到「發奮的小張」的博客。我是小張,一名普通的在校大學生。在學習之餘,用博客來記錄我學習過程中的點點滴滴,也希望我的博客能夠更給同樣熱愛學習熱愛技術的你們帶來收穫!希望大家多多關照,我們一起成長一起進步。也希望大家多多支持我鴨,喜歡我就給我一個關注吧!

最近在看陽哥的面試題,在多線程那裏深有感觸!

相信大家在學習單例模式的時候都寫過下面這樣的單例模式:

public class SingletonDemo {

    private volatile static SingletonDemo instance=null;

    private SingletonDemo(){
        System.out.println(Thread.currentThread().getName()+"\t 我是構造方法SingletonDemo()");
    }

    /**
     * @return 單例
     */
    public  static SingletonDemo getInstance(){//此處的方法可能同一時間被多個線程訪問,因此會產生多個對象
        if (instance==null){//線程1執行到此處。。。
            instance = new SingletonDemo();//線程2執行到此處。。。
        }
        return instance;//線程3執行到此處。。。
    }

    public static void main(String[] args) {
         for (int i = 0; i < 10; i++) {
            new Thread(()->{
                SingletonDemo.getInstance();
            },String.valueOf(i)).start();
        }
    }
}

此時運行會發現,單例模式出現了多個對象!

這個單例模式在單線程模式下沒有任何問題,但是在多線程環境中就會出現多個instance對象

在Java多線程中,我們有時候需要採用延遲初始化來降低初始化類和創建對象的開銷DCL(雙端檢鎖機制)是常見的延遲初始化技術。

此處可能會有人會說,給方法加上鎖就可以解決了,但是加鎖會導致性能大幅降低,因此是不划算的!

因此,我們可以把上面的單例模式改造成如下形式:

public class SingletonDemo {

    private static SingletonDemo instance=null;

    private SingletonDemo(){
        System.out.println(Thread.currentThread().getName()+"\t 我是構造方法SingletonDemo()");
    }

    /**
     * DCL雙端檢鎖機制
     * @return 單例
     */
    public  static SingletonDemo getInstance(){
        if (instance==null){//第一次檢索,無法防止多線程
           synchronized (SingletonDemo.class){//加鎖,保證安全性
               if (instance==null){//第二次檢索
                   instance = new SingletonDemo();//此處有一個雷
               }
           }
        }
        return instance;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                SingletonDemo.getInstance();
            },String.valueOf(i)).start();
        }
    }
}

DCL 優勢:
上面的代碼的好處在這裏,如果第一次檢查 instance 不爲 null ,那麼就不需要執行下面的加鎖和初始化操作。因此,可以大幅降低synchronized 帶來的性能開銷。
保證安全性:多個線程試圖在同一時間創建對象時,會通過加鎖來保證有一個線程創建對象。
保證性能:在對象創建好之後,執行 getInstance() 方法不需要獲取鎖,直接返回已創建好的對象。

此時測試不會出現多個對象的情況!但是,我們也僅僅是測試了一部分數據,如果用海量的數據測試,會不會出現對個對象呢?

此處的雷就在這個 instance = new SingletonDemo() 這裏

相信接觸過volatile的小夥伴都知道,它有三大作用:

  1. 可見性
  2. 不保證原子性
  3. 禁止指令重排

這裏,問題的關鍵就在於指令重排序的問題!
我們都知道,java虛擬機爲了提高性能,在底層有一個指令重排序的過程!
因此,這裏的DCL並不完美,仍然不能保證多線程下的安全性!
在代碼進行第一次檢索時,代碼讀取到 instance 不爲 null 時,instance 引用的對象有可能還沒有完成初始化。因此還是有可能產生安全問題!

解決DCL的指令重排序的安全問題

我們要解決這個問題,需要對instance對象加一個volatile來禁止它指令重排序!

代碼優化如下:

public class SingletonDemo {

    private volatile static SingletonDemo instance=null;

    private SingletonDemo(){
        System.out.println(Thread.currentThread().getName()+"\t 我是構造方法SingletonDemo()");
    }

    /**
     * DCL雙端檢鎖機制
     * @return 單例
     */
    public  static SingletonDemo getInstance(){
        if (instance==null){
           synchronized (SingletonDemo.class){
               if (instance==null){
                   instance = new SingletonDemo();
               }
           }
        }
        return instance;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                SingletonDemo.getInstance();
            },String.valueOf(i)).start();
        }
    }
}

此時的單例模式在多線程下的DCL雙重檢測機制纔算是線程安全的!

博主後記:

此處的指令重排序博主在這裏不詳細介紹了,因爲涉及到jvm的底層知識點,以及彙編語言和操作系統的知識,博主心知肚明道不清/(ㄒoㄒ)/~~!感興趣的小夥伴可以自行上網查詢相關資料!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章