天天看點

java volatile關鍵字總結

之前就看過很多關于volatile的資料,本文是作者對volatile關鍵字的一些總結,在這裡先感謝《java記憶體模型》的作者程曉明。

目錄

      • 目錄
      • java關鍵字volatile總結
        • 線程的可見性
        • 指令重排序

java關鍵字volatile總結

關于volatile修飾的變量,虛拟機做出如下保證:

  • 線程的可見性
  • 禁止指令的重排序

線程的可見性

java記憶體模型(簡稱JMM)規定了所有的變量都存儲在主存中,每個線程都有自己的工作記憶體,工作記憶體中儲存了主存中對應變量的拷貝,對變量的修改是在工作記憶體中完成,然後同步至主存中。JMM模型如圖:

java volatile關鍵字總結

由上述可以得出,多個線程對主存中同一普通變量的修改,是存在”可見性”問題的,也就是指在一個線程中對變量修改後,其他線程不一定及時知道。而虛拟機會保證對于volatile的變量,修改是對其他線程立即可見的。那麼虛拟機是如何做到這一點的呢?

在JMM中定義了八種操作來實作工作記憶體與主存的互動,這些操作都是原子操作,期間不會發生其他的線程切換:

  • Lock:将主存中的變量标記為一條線程獨占狀态;
  • Unlock:将鎖定的變量釋放;
  • Read:将主存中的變量傳輸到工作記憶體中;
  • Load:把read操作接收到的變量值放入工作記憶體的變量副本中;
  • Use:把工作記憶體中的值傳遞給執行引擎;
  • Assign:把從執行引擎中接收到的值指派給工作記憶體中的變量;
  • Store:把工作記憶體中的變量傳遞至主存;
  • Write:将store接收到的變量的值指派給主存中的變量;

在虛拟機中,對于volatile有如下規則,假設T表示一個線程,P和Q表示兩個volatile變量,在進行上面描述的操作時:

  • 隻有當T對P執行的前一個動作是load時,T才能對P執行use動作,并且隻有T對P執行的後一個動作是use時,T才能對P進行load操作;這樣就保證執行引擎每次在使用變量之前,都會從主存中讀取最新的值。
  • 隻有當T對P執行的前一個動作是assign時,T才能對P進行store操作,并且隻有T對P執行的後一個動作是store時,T才能對P執行assign;這樣就保證每次工作記憶體中的值修改後,會馬上寫入主存中。
  • 保證volatile的重排序規則(下文會有說明)

既然虛拟機對volatile變量做了這麼多規定,這樣可以保證volatile修飾的變量就是線程安全的嗎?看例子:

package test;

import java.util.concurrent.CountDownLatch;

public class Test {

    public static volatile int num = ;

    private static CountDownLatch end = new CountDownLatch();

    public static void addNum() {
        num++;
    }

    public static void main(String[] args) {
        for(int i = ; i < ; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        for(int i = ; i < ; i++) {
                            addNum();
                        }
                    } finally {
                        end.countDown();
                    }
                }
            }).start();
        }

        try {
            end.await();
            System.out.println(num);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
           

說明:20個線程,每個線程對num進行10000次自增操作,如果volatile是線程安全的,那執行完所有線程後應輸出200000,但結果每次輸出都不同,但都小于200000.

但是虛拟機不是規定對volatile變量的操作會對其他線程立即可見嗎?怎麼還會輸出錯誤的結果呢?原因是:對num的操作 num++其實是一個複合操作而不是原子操作,也就是說,在執行num++時,會出現”可見性”問題。為了便于了解,可以參照synchronized關鍵字:

public class SynaTest {

    private volatile int num;//volatile變量

    public int getNum() {
        return num;
    }

    public void setNum(int num) {
        this.num = num;
    }

    public void add() {
        num++;
    }
}
           

等價于

public class SynaTest {

    private int num; //普通變量

    public synchronized int getNum() {
        return num;
    }

    public synchronized void setNum(int num) {
        this.num = num;
    }

    public void add() {
        int tmp = getNum();
        tmp = tmp+;
        setNum(tmp);
    }
}
           

至此,關于第一點”對其他線程的可見”說完。

指令重排序

處理器和編譯器為提高效率,可能會對程式進行指令重排序,但我們不會意識到這種操作,因為重排序不會影響程式的輸出結果,當然,這裡不影響輸出結果隻是在單線程中。那麼JMM是如何是volatile修飾的變量不會發生指令重排序呢?

先來說說記憶體屏障,在JMM中,記憶體屏障可以分為:

屏障類型 指令示例 說明
LoadLoad Load1;LoadLoad;Load2 確定Load1資料的裝載,之前于Load2及所有後續裝載指令的裝載
StoreStore Store1;StoreStore;Store2 確定Store1資料對其他處理器可見(重新整理到記憶體),之前于Store2及後續存儲指令的存儲
LoadStore Load1;LoadStore;Store2 確定Load1資料裝載,之前于Store2及後續的存儲指令
StoreLoad Store1;StoreLoad;Load2 確定Store1資料對其他處理器變得可見(重新整理到記憶體),之前于Load2及後續裝載指令的裝載。StoreLoad會使屏障之前的所有記憶體指令(存儲和裝載)完成之後,才執行該屏障之後的記憶體通路指令

在JMM中,關于volatile的重排序規則定義如下:

  • 當第二個操作是volatile寫時,不論前一個操作是什麼,都不能進行重排序。
  • 當第一個操作是volatile讀時,不論後一個操作是什麼,都不能進行重排序。
  • 第一個操作是volatile寫,後一個操作是volatile讀時,不能進行重排序

為了實作上述三點,JMM采用插入記憶體屏障:

  • 在每個volatile寫操作的前面插入一個StoreStore屏障
  • 在每個volatile寫操作的後面插入一個StoreLoad屏障
  • 在每個volatile讀操作的後面插入一個LoadLoad屏障
  • 在每個volatile讀操作的後面插入一個LoadStore屏障

通過這幾個記憶體屏障,JMM就可以保證volatile語義:當寫一個volatile變量時,JMM會把該線程對應的工作記憶體中的值重新整理到主存中;檔讀一個volatile變量時,JMM會把工作記憶體中對應的變量值設為無效,從主存中擷取變量值。

通過上述的描述,可以看出其實volatile并不是” 線程安全”的,如果要保證同步,還需要額外的同步手段,比如通過synchronized關鍵字或者java.util.concurrent工具,但是volatile在某些情況下是非常适用的,比如隻有單一線程對volatile變量進行寫操作:

public class VolaTest {

    volatile boolean stop = false;

    public void shutdown() {//調用該方法後,可以使所有線程的doWork立即停下來
        stop = true;
    }

    public void doWork() {
        while(!stop) {
            //...
        }
    }
}
           

參考:http://ifeve.com/java-memory-model-0/

如果有不對的地方,歡迎大家指正。