關於java volatile關鍵字,以後別再面試中說不清楚了

Drummor發表於2018-03-25

問題

程式碼清單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++;

複製程式碼

為例這個行程式碼的執行過程如下:

  1. 將 count 的值從記憶體載入到自己的執行緒棧中
  2. 在自己的執行緒棧中對count進行加一操作
  3. 把修改後的值放回到主記憶體中。

在多執行緒的情況下

  • 執行緒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了。

相關文章