1. 程式人生 > >Java記憶體可見性volatile

Java記憶體可見性volatile

概述

JMM規範指出,每一個執行緒都有自己的工作記憶體(working memory),當變數的值發生變化時,先更新自己的工作記憶體,然後再拷貝到主存(main memory),這樣其他執行緒就能讀取到更新後的值了。
注意:工作記憶體和主存是JMM規範裡抽象的概念,在JVM的記憶體模型下,可以將CPU快取對應作執行緒工作記憶體,將JVM堆記憶體對應主存。

寫執行緒更新後的值何時拷貝到主存?讀執行緒何時從主存中獲取變數的最新值?hotspotJVM中引入volatile關鍵字來解決這些問題,當某個變數被volatile關鍵字修飾後,多執行緒對該變數的操作都將直接在主存中進行。在CPU時鐘順序上,某個寫操作執行完成後,後續的讀操作一定讀取的都是最新的值。

記憶體可見性帶來的問題

如下程式碼片段,寫執行緒每隔1秒遞增共享變數counter,讀執行緒是個死迴圈,如果讀執行緒始終能讀取到counter的最新值,那麼最終的輸出應該是 12345。

public class App {
    // 共享變數
    static int counter = 0;

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            int temp = 0;
            while (true) {
                if (temp != counter) {
                    temp = counter;
                    // 列印counter的值,期望列印 12345
                    System.out.print(counter);
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                counter++;
                // 等待1秒,給讀執行緒足夠的時間讀取變數counter的最新值
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            // 退出程式
            System.exit(0);
        });

        thread1.start();
        thread2.start();
    }
}

在沒有volatile的情況下,實際的輸出結構如下:

1

Process finished with exit code 0

通過volatile解決問題

將共享變數用volatile關鍵字修飾即可,如下:

// 共享變數
static volatile int counter = 0;

再次執行程式,輸出結果如下:

12345

Process finished with exit code 0

綜上,volatile關鍵字使得各個執行緒對共享變數的操作變得一致。在非volatile欄位上做更新操作時,無法保證其修改後的值何時從工作記憶體(CPU快取)重新整理到主存。對於非volatile欄位的讀操作也是如此,無法保證執行緒何時從主存中讀取最新的值。

volatile無法保證執行緒安全性

如下程式碼片段,多個執行緒同時遞增一個計數器:

public class App {
    // 共享變數
    static volatile int counter = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                counter++;
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                counter++;
            }
        });

        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();

        System.out.println("總和:" + counter);
    }

輸入結果:

總和:12374

如果volatile能保證執行緒安全,那麼輸出結果應該是20000,但上面的程式碼輸出12374,所以說,volatile不能解決執行緒安全(thread)的問題。
所以,還是要通過其他手段來解決多執行緒安全的問題,比如synchronized。

volatile和synchronized的區別

在上述的程式碼示例中,我們並沒有涉及到多執行緒競態(race condition)的問題,核心點是“多執行緒情況下,對共享變數的寫入如何被其他執行緒及時讀取到”。
synchronized關鍵字是Java中最常用的鎖機制,保證臨界區(critical section)中的程式碼在同一個時間只能有一個執行緒執行,臨界區中使用的變數都將直接從主存中讀取,對變數的更新也會直接重新整理到主存中。所以利用synchronized也能解決記憶體可見性問題。
程式碼如下:

public class App {
    // 共享變數
    static int counter = 0;

    public static void main(String[] args) {
        // 讀取變數的執行緒
        Thread readThread = new Thread(() -> {
            int temp = 0;
            while (true) {
                synchronized (App.class) {
                    if (temp != counter) {
                        temp = counter;
                        // 列印counter的值,期望列印 12345
                        System.out.print(counter);
                    }
                }
            }
        });

        // 修改變數的執行緒
        Thread writeThread = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                synchronized (App.class) {
                    counter++;
                }

                // 等待1秒,給讀執行緒足夠的時間讀取變數counter的最新值
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            System.exit(0);
        });

        readThread.start();
        writeThread.start();
    }
}

執行,輸入結果:

12345

Process finished with exit code 0

雖然通過synchronized也能解決記憶體可見性的問題,但是這個解決方案也帶來了其他問題,比如效能會比較差。

總結

多執行緒可以提升程式的執行速度,充分利用多核CPU的算力,但多執行緒也是“惡魔”,會給程式設計師帶來很多問題,比如本文中的記憶體可見性問題。volatile可以使變數的更新及時重新整理到主存,變數的讀取也是直接從主存中獲取,保證了資料的記憶體一致性。但是volatile不是用來解決執行緒安全問題的,無法替代鎖機制。

參考:
[1] Java Memory Model - Visibility problem, fixing with volatile variable
[2] Guide to the Volatile Keyword in Java
[3] Managing volatility
[4] Java Volatile Keyword
[5] Thread and Lo