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的作用了。