1.前言
因為今天在想到這個問題的時候腦子不是很清楚,就想查一下網上的資料,結果發現一個個寫的囫圇吞棗。后來突然想起來了,於是打算記錄下來。
注意此種方法只針對JDK1.5及以上,之前好像是volatile的關鍵字設計有問題?
2.雙層檢查實現單例模式的由來
最開始只有一層檢查,
public
Resource getResource() {
if
(resource ==
null
) {
resource =
new
Resource();
}
return
resource;
}
public synchronized
Resource getResource() {
if
(resource ==
null
) {
resource =
new
Resource();
}
return
resource;
}
public
Resource getResource() {
if
(resource ==
null
) {
synchronized
(
this
){
if
(resource==
null
) {
resource =
new
Resource();
}
}
}
return
resource;
}
如果加了volatile關鍵字,這個方法是有效的。如果沒有,這個方法是有一定多線程風險的。
現在進入本文的重點,此處為什么不加volatile有風險。
首先聲明:
此處利用了volatile的一個關鍵字特性:防止局部指令重排序
簡單的概括就是,
volatile禁止指令重排序的一些規則:2
1.當第二個操作是voaltile寫時,無論第一個操作是什么,都不能進行重排序
2.當第一個操作是volatile讀時,不管第二個操作是什么,都不能進行重排序
3.當第一個操作是volatile寫時,第二個操作是volatile讀時,不能進行重排序
下面來看new Resource()這個操作,
它可以分解成三個操作,
【偽代碼】3
1.memory = allocate()
2.createInstance(memory)
3.resource = memory
其中1,2指令有數據依賴,所以不會被重排序。而2,3指令沒有數據依賴,如果沒有volatile關鍵字,可能會被重排序。
那么假如2,3執行順序進行了調換。
那么就有可能發生,假設A,B線程都即將執行getResource操作,目前在A線程,
首先A線程第一次判斷resource是否為null,結果為null,那么加鎖進入執行創建對象的這三步。
假設執行了上述的1,3后,發生了調度,B開始執行。
此時它面臨的狀態是,判斷resource是否為null,結果不為null.因為剛才A執行了3已經給resource賦值了。
那么B會認為resource已經初始化完成,它可能要對這個對象進行一些操作,但是事實上A還沒有執行2操作,resource對象還沒有初始化完成。
這樣運行下去可能會產生異常,風險由此產生。
不過加了volatile后,在執行對resource寫之前,volatile關鍵字保證了1,2,3操作不會重排序(需要考證),這樣就保證了resource要么為null,要么就是一個完整的resource對象。
順便補充一下,加了volatile之后,即使在執行1,2,3的時候發生了調度,因為鎖在A線程手里,所以B線程沒有辦法拿到鎖進行初始化。所以即使調度時候resource為null,也不會發生多次初始化的情形。
如果需要對volatile有更多的了解,
可以訪問官方Java Language Specification
關於volatile: http://docs.oracle.com/javase/specs/jls/se8/html/jls-8.html#d5e12277
關於Java內存模型: http://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.4
參考文檔:
1.http://www.jb51.net/article/80201.htm
2.https://blog.csdn.net/hqq2023623/article/details/51013468
3.http://blog.csdn.net/xiakepan/article/details/52444565
