DCL單例模式為什么要兩次判空


public class Test

{

    private volatile static Test instance;

    private Test() { } public static Test getInstance()

    {

       if (instance == null)

      {  

        synchronized (Test.class)

          {  if (instance == null)

              { instance = new Test(); } } }

        return instance; }

}

 

 

然后來分析getInstance()每一步的作用

第一個if語句,用來確認調用getInstance()時instance是否為空,如果不為空即已經創建,則直接返回,如果為空,那么就需要創建實例,於是進入synchronized同步塊。

synchronized加類鎖,確保同時只有一個線程能進入,進入以后進行第二次判斷,是因為,

對於首個拿鎖者,它的時段instance肯定為null,那么進入new Singleton()對象創建,

而在首個拿鎖者的創建對象期間,可能有其他線程同步調用getInstance(),那么它們也會通過if進入到同步塊試圖拿鎖然后阻塞。

這樣的話,當首個拿鎖者完成了對象創建,之后的線程都不會通過第一個if了,而這期間阻塞的線程開始喚醒,它們則需要靠第二個if語句來避免再次創建對象。

以上就是雙檢索的實現思路,synchronized與第二個if即是用來保證線程安全與不產生第二個實例,也是Double_Checked_Lock由來。

那么volatile的作用體現在哪呢?

一開始我認為是volatile的同步性,因為要在首個拿鎖者創建對象以后,立即保證instance的可見性,以讓被喚醒的阻塞線程能夠在第二個if語句的時候得到instance非空的結果。

但這個可見性其實是用synchronized來保障的,並不需要volatile來多此一舉了。

后來才知道應該是避免指令重排序,說明如下

這里的指令重排序主要體現在instance = new Singleton()這條語句上了。

這條語句顯然是個復合操作,可以簡單分下,(已完成類加載 ,假設在堆上分配內存)

1.在堆中分配對象內存

2.填充對象必要信息+具體數據初始化+末位填充

3.將引用指向這個對象的堆內地址

那么,在完成1后,對象的大小和地址已經確定,因此,2和3其實存在指令重排序的可能。

並且可以看到,3的操作明顯比2要少,那么如果讓2與3一起執行,並且反應到具體的順序上變成了1-3-2.

先完成3,引用變量instance先指向了在堆中給對象分配的空間,然后2仍在慢慢吞吞繼續。

這時候,被synchronized擋在外面的阻塞線程其實是不會有什么影響的,因為一定會等到對象創建完,首個拿鎖者才會釋放鎖。

那么關鍵是在,此刻如果在3完成而2未完成這個臨界點,有一個新線程調用getInstance(),那么第一個if,會怎么樣?

答案是因為第一個if沒在同步塊里,而此時instance已經非空,指向具體內存地址了,所以直接返回此時未完成初始化的instance實例

那么如果在Singleton里有個變量int number ,有個方法int getNumber()返回number,這時候調用

Singleton.getInstance().getNumber();

會怎樣?

不知道,可能會報錯,或者會得到錯誤結果,但這就是我能想到的volatile避免的情況了。

volatile修飾變量避免指令重排序,保證1-2-3按順序來,這樣即使在首個拿鎖者未釋放鎖前,有線程切入,當它在第一個if處得到instance非空時,此時instance的初始化也一定已經完成。

因此這就是volatile在DCL的作用了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM