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關鍵字修飾便可,以下:安全
// 共享變量 static volatile int counter = 0;
再次執行程序,輸出結果以下:bash
12345 Process finished with exit code 0
綜上,volatile關鍵字使得各個線程對共享變量的操做變得一致。在非volatile字段上作更新操做時,沒法保證其修改後的值什麼時候從工做內存(CPU緩存)刷新到主存。對於非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。性能
在上述的代碼示例中,咱們並無涉及到多線程競態(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