天天看點

Java關鍵字volatile是如何保證記憶體可見性以及防止指令重排序的?

本文首發位址 https://www.dgjava.com/archives/javavolatile01

volatile在Java并發程式設計中常用于保持記憶體可見性和防止指令重排序

這句話很簡單,看起來很好了解,但似乎又不好了解,是以我們幹脆通過代碼來說明。

/**
 * <p>深入了解volatile關鍵字<p/>
 *
 * @Author: 地瓜(yangxw)
 * @Date: 2020/4/8 12:55 AM
 */
public class VolatileTest {
    final static int MXA = 5;
    static int init_value = 0;

    public static void main(String[] args) {
        new Thread(() -> {
            int local_value = init_value;
            while (local_value < MXA) {
                if (init_value != local_value) {
                    System.out.printf("init_value 更新為[%d]\n", init_value);
                    local_value = init_value;
                }
            }
        }, "reader").start();

        new Thread(() -> {
            int local_value = init_value;
            while (local_value < MXA) {
                ++local_value;
                System.out.printf("init_value 變為[%d]\n", local_value);
                init_value = local_value;
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "update").start();
    }
}
           

這是一段很簡單的代碼,main方法中啟動兩個線程。第一個線程比較 init_value 和 local_value 的值,如果不相等,列印最新的 init_value 的值,并更新。第二個線程則對 local_value 做自加操作,并指派給 init_value 。理想狀态下兩個線程同時運作,應該輸出類似 :

  • init_value 更新為1
  • init_value 變為1

如此交替遞增。然而實際效果如下圖:

Java關鍵字volatile是如何保證記憶體可見性以及防止指令重排序的?

可以看到,第一個線程的輸入代碼沒有如預期的那樣實作,那就意味着在第一個線程的while循環中,local_value 首次指派後,init_value 的值一直沒變。實際上真的沒有變嗎?基于以上代碼,我将第一個線程做一個改造,在循環中休眠200ms,并輸出最新的local_value和init_value,代碼如下:

new Thread(() -> {
            int local_value = init_value;
            while (local_value < MXA) {
                try {
                    TimeUnit.MILLISECONDS.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.printf("local_value=[%d],init_value=[%d]\n", local_value, init_value);
                if (init_value != local_value) {
                    System.out.printf("init_value 更新為[%d]\n", init_value);
                    local_value = init_value;
                }
            }
        }, "reader").start();

           

實際輸出結果為:

Java關鍵字volatile是如何保證記憶體可見性以及防止指令重排序的?

看到這個結果,似乎有些明了。在第一個線程中,并不是init_value沒有發生變化,隻是線程執行太快,沒能及時讀取到init_value的最新值。

在一開始說到過,volatile在Java并發程式設計中常用于保持記憶體可見性和防止指令重排序。那我們不妨在init_value前加上volatile關鍵字,看看是否能在第一個線程中實時讀取到init_value在記憶體中的最新值,我們把休眠的代碼去掉,隻保正常留輸出。代碼如下:

public class VolatileTest {
    final static int MXA = 5;
    volatile static int init_value = 0;

    public static void main(String[] args) {
        new Thread(() -> {
            int local_value = init_value;
            while (local_value < MXA) {
                if (init_value != local_value) {
                    System.out.printf("init_value 更新為[%d]\n", init_value);
                    local_value = init_value;
                }
            }
        }, "reader").start();

        new Thread(() -> {
            int local_value = init_value;
            while (local_value < MXA) {
                ++local_value;
                System.out.printf("init_value 變為[%d]\n", local_value);
                init_value = local_value;
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "update").start();
    }
}
           

再來看看運作結果

Java關鍵字volatile是如何保證記憶體可見性以及防止指令重排序的?

通過這個圖,那麼volatile在Java并發程式設計中常用于保持記憶體可見性就很好了解了。同時也可以結果,為什麼volatile關鍵字在高并發場景下,為何那麼好用了。

源碼位址