之前读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());

  修改实验一的部分代码,神奇的事情发生了 

  volatile修饰数组_JDK

  竟然可以了?

  实验三:  

 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 }

  参考资料中提到,当线程读取一个volatile修饰的变量时,会将这个线程中所有的变量都从主存中刷新一下。所以这里主线程访问变量b时,也会同时刷新数组。

  volatile修饰数组_volatile_02  

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到底只保证引用的可见性,还是包含了引用指向对象的可见性?

  volatile修饰数组_volatile_03

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,其实就是说不同线程修改同一个缓存行的问题,每个线程读取一个缓存行,修改之后,同步到主存,会导致其他线程中相同的缓存行失效,这将带来性能上的问题。