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

問題

代碼清單1java

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能夠解決這個問題,可你知道爲何能解決嗎?spa

volatile的語義是能保持有序性可見性,可是不能保證原子性線程

可見性

什麼是可見性?code

對象

count = 0;
couont++;

爲例這個行代碼的執行過程以下:排序

  1. 將 count 的值從內存加載到本身的線程棧中
  2. 在本身的線程棧中對count進行加一操做
  3. 把修改後的值放回到主內存中。

在多線程的狀況下內存

  • 線程1執行了第一個操做
  • 以後線程2也執行了第一操做
  • 線程1執行了後面兩個操做,此時主存中的count值變成了1;
  • 線程2繼續執行第二個操做,它用的是本身棧中的副本其值爲0進行加1,最後執行第三個操做把1寫回到主存中。

看到問題了吧,加了兩遍仍是1,出事了啊兄弟!get

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了。

相關文章
相關標籤/搜索