volatile修飾數組


之前讀CHM的源碼(JDK8),其中有一段印象比較深,它內部有一個Node數組,volatile修飾, transient volatile Node<K,V>[] table; 。而Node對象本身,存儲數據的val變量,也是用volatile修飾的。這兩個一個是保證擴容時,變更table引用時的可見性,一個是保證value修改后的可見性。

1. 非volatile數組的可見性問題

  實驗一

 1 public class Test {
 2     static int[] a = new int[]{1};
 3 
 4     public static void main(String[] args) {
 5         new Thread(() -> {
 6             System.out.println("線程1開始休眠:" + LocalDateTime.now().toString());
 7             try {
 8                 Thread.sleep(1000);
 9             } catch (InterruptedException e) {
10                 e.printStackTrace();
11             }
12             System.out.println("線程1休眠結束:" + LocalDateTime.now().toString());
13             a[0] = 0;
14         }).start();
15 
16         while (a[0] != 0) {
17         }
18         System.out.println("主線程退出循環:" + LocalDateTime.now().toString());
19     }
20 }

  上述代碼測試時,主線程無法退出循環,這說明了主線程使用的一直是工作內存中的數組數據,沒有從主存刷新數據。

  多線程下,修改普通數組,是不可見的。

  實驗二

1 while (a[0] != 0) {
2     System.out.println("");
3 }
4 System.out.print("主線程退出循環:" + LocalDateTime.now().toString());

  修改實驗一的部分代碼,神奇的事情發生了 

  

  竟然可以了? System.out.println(""); 有這么大魔力嗎?我們看下方法實現

  

   看到synchronized有木有,synchronized保證了原子性、可見性和防止指令重排序。對於可見性,JMM規定,線程獲取Lock,需要將清空工作內存中共享變量的值,從主存中重新獲取。而釋放鎖前,需要將自身變量值同步回主存。請見:第十二章 Java內存模型與線程。實驗二改動的代碼部分,加入了獲取鎖,所以會不停刷新變量的值。並且,所有的 System.out.println 方法,鎖住的都是同一個鎖對象,即 public final static PrintStream out; 。提到這一點是,網上有些資料說,必須保證是同一個鎖的加鎖解鎖,才能保證可見性。

  那么我們再試一下,鎖住不同對象,還能正常刷新嗎?

1 while (a[0] != 0) {
2     synchronized ("") {
3     }
4 }

  將實驗二修改成如上代碼,再次實驗:

  

  可見並不需要是同一個鎖,只要獲取鎖就會去主存刷新緩存。  

 

  實驗三:  

 1 public class Test {
 2     static int[] a = new int[]{1};
 3     static volatile boolean b = false;
 4 
 5     public static void main(String[] args) {
 6         new Thread(() -> {
 7             System.out.println("線程1開始休眠:" + LocalDateTime.now().toString());
 8             try {
 9                 Thread.sleep(1000);
10             } catch (InterruptedException e) {
11                 e.printStackTrace();
12             }
13             System.out.println("線程1休眠結束:" + LocalDateTime.now().toString());
14             a[0] = 0;
15         }).start();
16 
17         while (a[0] != 0) {
18             b = false;
19         }
20         System.out.println("主線程退出循環:" + LocalDateTime.now().toString());
21     }
22 }

  參考資料1中提到,當線程讀取一個volatile修飾的變量時,會將這個線程中所有的變量都從主存中刷新一下。所以這里主線程訪問變量b時,也會同時刷新數組。

    

2. volatile數組的可見性問題

  實驗三

 1 public class Test {
 2     static volatile int[] a = new int[]{1};
 3 
 4     public static void main(String[] args) {
 5         new Thread(() -> {
 6             System.out.println("線程1開始休眠:" + LocalDateTime.now().toString());
 7             try {
 8                 Thread.sleep(1000);
 9             } catch (InterruptedException e) {
10                 e.printStackTrace();
11             }
12             System.out.println("線程1休眠結束:" + LocalDateTime.now().toString());
13             a[0] = 0;
14         }).start();
15 
16         while (a[0] != 0) {
17         }
18         System.out.println("主線程退出循環:" + LocalDateTime.now().toString());
19     }
20 }

  主線程正常退出,那么問題來了,volatile到底只保證引用的可見性,還是包含了引用指向對象的可見性?

  

3. volatile修飾數組的作用

  在網上查閱資料,說這里需要區分一下基礎類型數組和對象類型數組。上面的實驗都是基於整數數組,那我們繼續實驗一下對象數組

  實驗四

 1 public class Test {
 2     static volatile A[] a = new A[]{new A(1)};
 3 
 4     public static void main(String[] args) {
 5         new Thread(() -> {
 6             System.out.println("線程1開始休眠:" + LocalDateTime.now().toString());
 7             try {
 8                 Thread.sleep(1000);
 9             } catch (InterruptedException e) {
10                 e.printStackTrace();
11             }
12             System.out.println("線程1休眠結束:" + LocalDateTime.now().toString());
13             a[0] = new A(0);
14         }).start();
15 
16         while (a[0].val != 0) {
17         }
18         System.out.println("主線程退出循環:" + LocalDateTime.now().toString());
19     }
20 
21 
22     static class A {
23         public int val;
24 
25         A(int val) {
26             this.val = val;
27         }
28     }
29 }

  很遺憾,跟實驗三的結果是一樣的。

  那么為什么CHM需要再使用volatile保證Node對象value屬性的可見性呢?而網上說的volatile只能保證引用的可見性是否正確呢?

  JUC下的另一個並發工具類CopyOnWriteArrayList,這個也定義了一個對象數組 private transient volatile Object[] array; ,但是在訪問元素時,並沒有特殊的手段保證可見性,在設置元素時,先獲取鎖,將原數組拷貝一份,修改新數組后,修改array指向新數組。

4. 引申

  其實這個問題,是之前寫一個小功能遇到的,原問題是:線程1需要在線程2和線程3執行完成之后執行,實現方式有很多,比如柵欄、Jdk8的CompletableFuture、同步機制等,還想到一個數組形式,比如一個長度為2的數組,每個線程執行完畢之后,修改對應位置標志,這樣避免了同步的問題。我們拋開上面的問題不談,假設使用volatile修飾數組,實現這個功能,是否沒有其他問題呢?

  其實還有一個緩存行偽共享的問題。見參考資料2,其實就是說不同線程修改同一個緩存行的問題,每個線程讀取一個緩存行,修改之后,同步到主存,會導致其他線程中相同的緩存行失效,這將帶來性能上的問題。

5. 更新

  關於volatile的可見性,參考文獻3及文獻5中說明了,從JDK5開始,volatile保證可見性不僅局限於其修飾的變量,還包括了線程中使用的其他變量。具體是

  1. 讀取volatile變量時,在該變量之后的變量也將從主存中重新讀取(在volatile變量讀操作發生之后的變量,因為禁止了指令重排序,所以是可見的)

  2. 寫入volatile變量時,在該變量之前的變量產生的修改也將寫入到主存中(在volatile變量寫操作發生之前的變量,因為禁止了指令重排序,所以是可見的)

  volatile防止指令重排序(參考4):

  

  

  對實驗三和實驗四的結果做出解釋,即我們先讀取了volatile修飾的數組,這個操作將導致之后所有用到的值都會從主存中刷新一下,意味着數組內部的元素的值也被刷新了,如此我們才能訪問到最新的數據。

  

 

參考:

1. volatile修飾數組,那么數組元素可見嗎?

2. 偽共享(False Sharing)

3. volatile修飾List能否保證內部元素的可見性?

4. 【並發】volatile是否能保證數組中元素的可見性?

5. Java Volatile Keyword


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM