問題
程式碼清單1
class SingleInstance{
private static single = null;
private SingeInstance(){}
public SingleInstance getSingleInstance(){
if(single == null)//0
synchroznied(SingleInstance.class){
if(single == null){
single = new SingleInstance();//1
}
}
return single;
}
}
複製程式碼
如上程式碼的問題是在1處不能保證有序性,即這句程式碼其實分為兩個大的步驟
- 初始化SingleInstance
- 把這個物件賦值給single這個變數
這個步驟前後是不確定的。當執行緒一執行到1處的時候可能會先物件賦值給single了但是此時的single還沒有初始化完成。執行緒2執行的0處的時候會發現這個條件是不符合的於是就返回了single。這時候的single雖然是一個非空的引用,但卻不是一個正確的物件。 這個就是雙重校驗可能出現的問題。
volatile
可能你聽說過JDK1.4以後用volatile修飾變數single可以解決這個問題,可你知道為什麼能解決嗎?
volatile的語義是能保持有序性和可見性,但是不能保證原子性
可見性
什麼是可見性?
以
count = 0;
couont++;
複製程式碼
為例這個行程式碼的執行過程如下:
- 將 count 的值從記憶體載入到自己的執行緒棧中
- 在自己的執行緒棧中對count進行加一操作
- 把修改後的值放回到主記憶體中。
在多執行緒的情況下
- 執行緒1執行了第一個操作
- 之後執行緒2也執行了第一操作
- 執行緒1執行了後面兩個操作,此時主存中的count值變成了1;
- 執行緒2繼續執行第二個操作,它用的是自己棧中的副本其值為0進行加1,最後執行第三個操作把1寫回到主存中。
看到問題了吧,加了兩遍還是1,出事了啊兄弟!
volatile內在其中起什麼作用呢?
當用volatile修飾count後這樣執行緒執行操作的時候也就是上述的****2步驟他不會在副本中取值,而是去主存中取值。
即便是這樣也不能解決計數問題,為什麼呢?
- 執行緒1從記憶體中取值進行加1操作,執行緒副本count值變成了1。
- 然後執行緒2從主存中取值,這時候取到的值是0,進行加1操作,寫會到主存,主存中count變成了1。
- 執行緒1執行步驟3把自己副本中值為1的count寫回到主存,主存還是1。
小結
volatile的可見性語義是保證執行緒進行操作也就是上述的步驟2是從主存中取最新的值而不是在自己副本中取值。
有序性
在Java記憶體模型中,允許編譯器和處理器對指令進行重排序,但是重排序過程不會影響到單執行緒程式的執行,卻會影響到多執行緒併發執行的正確性。
例子: 程式碼清單2
執行緒A中
context = initContext();//1
flag = true;//2
執行緒B中
while(!flag){
sleep(100);
}
dosomething(context);
複製程式碼
在單執行緒中程式碼是沒有問題的,但是如程式碼清單二中,執行緒A的程式碼可能會發生重排序也就是執行程式碼2再執行程式碼1這就有問題了。
如果用volatile修飾就會禁止他進行重排序。
原子性
原子性簡單來說就是不可分割,如果是原子操作,那它必定是要麼被執行完畢,要麼完全沒執行兩種情況之一,不可能出現執行了一部分這種情況。
總結
volatile欄位能保證可見性、禁止重排序,但並不能提供原子性。原因在於在多執行緒的條件下,不能保證執行順序,中間會有執行緒切換的情況出現。
回到程式碼清單1還記得當初的問題嗎?程式碼清單1中這個單例有什麼問題我們已經說過了。怎麼解決呢? 其實有了volatile這個關鍵字就好解決了,在single這個變數上新增volatile就可以完美解決了。原因是volatile具有禁止重排序的功能。所以會先進行初始化物件再賦值給變數,0處檢測到的single不為空的時候就能正確返回single而不再是一個不完整的single了。