volatile的語義與實踐

Pirate@f發表於2020-10-20

volatile是JVM虛擬機器提供的最輕量級的同步機制,如果能恰當的使用volatile的話,它比synchronized的執行成本更低,因為它不會引起上下文的切換和排程。

    Java語言規範第三版中對volatile的定義如下:Java語言允許執行緒訪問共享變數,為了確保共享變數能被準確和一致的更新,執行緒應該確保通過排它鎖單獨獲取這個變數。Java語言提供了volatile,在某些情況下比鎖要更加方便。如果一個欄位被宣告為volatile,Java執行緒記憶體模型確保所有執行緒看到這個變數的值是一致的。

    

    上面兩段話說明了Java中的volatile是什麼以及為什麼會出現volatile,實際上,volatile在JVM虛擬機器執行時具有兩個語義:

    1. 可見性。

    2. 相對有序性。

 

以下內容參考“Java併發程式設計的藝術”

    以X86處理器下通過工具獲取到的JIT編譯期生成的彙編指令來檢視對volatile變數進行寫操作,會有如下彙編程式碼:    

0x01a3de1d: movb $0×0,0×1104800(%esi);0x01a3de24: lock addl $0×0,(%esp);

        有volatile變數修飾的共享變數進行寫操作的時候會多出第二行lock彙編程式碼,通過查IA-32架構軟體開發者手冊可知,Lock字首的指令在多核處理器下會引發兩件事情:

        a. 將當前處理器快取行的資料寫回的到系統記憶體。

       b. 這個寫回記憶體的操作會使在其他CPU裡快取了該地址的資料失效。(這點通過快取一致性協議實現,具體做法:每個處理器通過嗅探在匯流排上傳播的資料來檢查自己快取的值是不是過期了,當處理器發現自己快取行對應的記憶體地址被修改,就會將當前處理器的快取行設定成無效狀態,當處理器對這個資料進行修改操作的時候,會重新從記憶體把資料讀取到處理器快取中,不直接從記憶體中修改,是為了提高處理速度,記憶體的頻率遠小於CPU的頻率,根據木桶效應,所以增加了L1,L2快取,以提高運算效能)。

 

    可見性釋義:

        volatile修飾的變數對所有的執行緒是立即可見的,對volatile的所有寫操作都能立即反映到其他執行緒中。換句話說,volatile變數在各個執行緒中是一致的, 但這並不能推出基於volatile變數的運算在併發下是安全的。可以推出基於volatile變數的原子操作在併發下是執行緒安全的。實際上也是如此,引用(周志明老師的“深入理解Java虛擬機器-JVM高階特性與最佳實踐”)中的一段程式碼:

    ​​​​

/** * {@link volatile} 測試 * <p> * 驗證volatile是不是執行緒安全的?參考周志明老師的深入理解JVM高階特性 */public class VolatileTest01 {    public static volatile int race = 0;    public static final int THREADS_COUNT = 20;    public static void increase() {        /**         * 產生執行緒不安全的根本原因:race++不是原子操作,反編譯後的位元組碼如下         *         *  public static void increase();         *     descriptor: ()V         *     flags: ACC_PUBLIC, ACC_STATIC         *     Code:         *       stack=2, locals=0, args_size=0         *          0: getstatic     #2                  // Field race:I         *          3: iconst_1         *          4: iadd         *          5: putstatic     #2                  // Field race:I         *          8: return         *       LineNumberTable:         *         line 15: 0         *         line 16: 8         */        race++;    }    public static void main(String[] args) {        Thread[] threads = new Thread[THREADS_COUNT];        for (int i = 0; i < THREADS_COUNT; i++) {            threads[i] = new Thread(() -> {                for (int j = 0; j < 10000; j++) {                    increase();                }            });            threads[i].start();        }        while (Thread.activeCount() > 1){            Thread.yield();        }        System.out.println(race);    }}

        這段程式碼執行後,結果顯然不是200000。原因是:雖然volatile保證了race線上程之間的可見性(讀取race變數時確實是最新的race值),可是執行緒讀取race之後,再對race進行運算,最後對race進行賦值,整個操作是非原子性的,這之間的時間,足以讓別的執行緒修改了剛才此執行緒讀到的最新的race值,因此才產生了這個bug。

        如果將race的讀取和運算整體改為原子操作,每次執行結果均為200000,程式碼如下:    

public class VolatileTest03 {    private static volatile AtomicInteger race = new AtomicInteger(0);    public static final int THREADS_COUNT = 20;    public static void increase(){        race.incrementAndGet();    }    public static void main(String[] args) {        Thread[] threads = new Thread[THREADS_COUNT];        for (int i = 0; i < THREADS_COUNT; i++) {            threads[i] = new Thread(() -> {                for (int j = 0; j < 10000; j++) {                    increase();                }            });            threads[i].start();        }        while (Thread.activeCount() > 1){            Thread.yield();        }        System.out.println(race);    }}

        

    相對有序性釋義:

          就是因為(a.將當前處理器快取行的資料寫回的到系統記憶體),將結果寫回記憶體時,意味著這條指令之前的指令已經得出結果,這就形成了類似於“記憶體屏障”的功能,導致volatile前後的程式碼無法指令重排序。為什麼說相對有序呢??volatile之前和之後的指令還是可以各自發生重排序的,只是重排序無法穿透volatile而已。

 

如果你覺得這篇內容對你還蠻有幫助,我想邀請你幫我三個小忙:

  1. 點贊,轉發,有你們的 『點贊和評論』,才是我創造的動力。

  2. 關注公眾號 『逆行的碎石機』,不定期分享原創知識。

  3. 同時可以期待後續文章ing?

 

相關文章