java volatile關鍵字作用及使用場景

  • 2019 年 10 月 3 日
  • 筆記

 

1. volatile關鍵字的作用:保證了變數的可見性(visibility)。被volatile關鍵字修飾的變數,如果值發生了變更,其他執行緒立馬可見,避免出現臟讀的現象。如以下程式碼片段,isShutDown被置為true後,doWork方法仍有執行。如用volatile修飾isShutDown變數,可避免此問題。

 1 public class VolatileTest3 {   2     static class Work {   3         boolean isShutDown = false;   4   5         void shutdown() {   6             isShutDown = true;   7             System.out.println("shutdown!");   8         }   9  10         void doWork() {  11             while (!isShutDown) {  12                 System.out.println("doWork");  13             }  14         }  15     }  16  17     public static void main(String[] args) {  18         Work work = new Work();  19  20         new Thread(work::doWork).start();  21         new Thread(work::doWork).start();  22         new Thread(work::doWork).start();  23         new Thread(work::shutdown).start();  24         new Thread(work::doWork).start();  25         new Thread(work::doWork).start();  26         new Thread(work::doWork).start();  27     }  28 }

出現臟讀時,運行結果如下:

2. 為什麼會出現臟讀?

Java記憶體模型規定所有的變數都是存在主存當中,每個執行緒都有自己的工作記憶體。執行緒對變數的所有操作都必須在工作記憶體中進行,而不能直接對主存進行操作。並且每個執行緒不能訪問其他執行緒的工作記憶體。變數的值何時從執行緒的工作記憶體寫回主存,無法確定。

3. happens-before規則的理解與勘誤

在網上查volatile關鍵字相關資訊時,多篇部落格提到了happens-before原則,個人對此原則的理解是:當操作該volatile變數時,所有前序對該變數的操作都已完成(如不存在已變更,但未寫回主存的情況),所有後續對該變數的操作,都未開始。僅此而已。

這裡,我認為網上很常見的一個理論對此理解有誤,如下圖。此觀點認為,由於volatile變數flag的happens-before原則,所以A執行緒2處對其的寫操作一定先於B執行緒3處對其的讀操作。其實這種觀點是有邏輯缺陷的,如果存在一個C執行緒,先讀取flag的值,後寫入flag的值,那C執行緒的執行時機是什麼呢?如果還有其他D、E執行緒呢。。。對於這段程式碼的正確理解是,只要3處拿到的flag是true,那麼a的值一定是1,而不是0.因為volition修飾的變數,處理器不會對其進行重排序,所以1處對a的賦值,一定發生在2處對flag的賦值之前。如果flag不是volatile變數,那麼1處和2處程式碼的執行順序是無法保證的(處理器的指令重排序),雖然大部分情況1會先於2執行。happens-before原則約束的並不是多執行緒對同一變數的讀和寫操作之間的順序,而是保證讀操作時,前序所有對該變數的寫操作已生效(寫回主存)。

 

 

驗證如下:

 1 public class VolatileTest {   2     static class A {   3         int a = 0;   4         volatile boolean flag = false;   5   6         void writer() {   7             a = 1;                   //1   8             flag = true;               //2   9             System.out.println("write");  10         }  11  12         void reader() {  13             if (flag) {                //3  14                 int i =  a;           //4  15                 System.out.println("read true");  16                 System.out.println("i is :" + i);  17             } else {  18                 int i = a;  19                 System.out.println("read false");  20                 System.out.println("i is :" + i);  21             }  22         }  23  24     }  25  26     public static void main(String[] args) {  27         A aaa = new A();  28         new Thread(() -> aaa.reader()).start();  29         new Thread(() -> aaa.writer()).start();  30     }  31 }

運行結果如下,在寫操作執行之前,讀操作已完成

 

 4. volatile關鍵字使用場景

注意:volatile只能保證變數的可見性,不能保證對volatile變數操作的原子性,見如下程式碼:

 

 1 public class VolatileTest2 {   2     static class A {   3         volatile int a = 0;   4         void increase() {   5             a++;   6         }   7         int getA(){   8             return a;   9         }  10     }  11  12     public static void main(String[] args) {  13         A a = new A();  14  15         new Thread(() -> {  16             for (int i = 0;i < 1000;i++) {  17                 a.increase();  18             }  19             System.out.println(a.getA());  20         }).start();  21         new Thread(() -> {  22             for (int i = 0;i < 2000;i++) {  23                 a.increase();  24             }  25             System.out.println(a.getA());  26         }).start();  27         new Thread(() -> {  28             for (int i = 0;i < 3000;i++) {  29                 a.increase();  30             }  31             System.out.println(a.getA());  32         }).start();  33         new Thread(() -> {  34             for (int i = 0;i < 4000;i++) {  35                 a.increase();  36             }  37             System.out.println(a.getA());  38         }).start();  39         new Thread(() -> {  40             for (int i = 0;i < 5000;i++) {  41                 a.increase();  42             }  43             System.out.println(a.getA());  44         }).start();  45     }  46 }

 

運行結果如下,volatile無法保證a++操作的原子性。

volatile正確的使用方法可參考:https://blog.csdn.net/vking_wang/article/details/9982709