volatile修饰数组
Posted walker993
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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());
修改实验一的部分代码,神奇的事情发生了
竟然可以了?
实验三:
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时,也会同时刷新数组。
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,其实就是说不同线程修改同一个缓存行的问题,每个线程读取一个缓存行,修改之后,同步到主存,会导致其他线程中相同的缓存行失效,这将带来性能上的问题。
参考:
以上是关于volatile修饰数组的主要内容,如果未能解决你的问题,请参考以下文章
Java并发编程:volatile变量修饰符-意料之外的问题(含代码)