Java內存可見性volatile

概述

JMM規範指出,每個線程都有本身的工做內存(working memory),當變量的值發生變化時,先更新本身的工做內存,而後再拷貝到主存(main memory),這樣其餘線程就能讀取到更新後的值了。
注意:工做內存和主存是JMM規範裏抽象的概念,在JVM的內存模型下,能夠將CPU緩存對應做線程工做內存,將JVM堆內存對應主存。
html

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

內存可見性帶來的問題

以下代碼片斷,寫線程每隔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;

再次執行程序,輸出結果以下:bash

12345

Process finished with exit code 0

綜上,volatile關鍵字使得各個線程對共享變量的操做變得一致。在非volatile字段上作更新操做時,沒法保證其修改後的值什麼時候從工做內存(CPU緩存)刷新到主存。對於非volatile字段的讀操做也是如此,沒法保證線程什麼時候從主存中讀取最新的值。多線程

volatile沒法保證線程安全性

以下代碼片斷,多個線程同時遞增一個計數器:oracle

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);
    }

輸入結果:ide

總和: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 Locks

相關文章
相關標籤/搜索