zoukankan      html  css  js  c++  java
  • 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

    人生就像蒲公英,看似自由,其实身不由己。
  • 相关阅读:
    python数据采集与多线程效率分析
    Memcache使用基础
    《大规模 web服务开发》笔记
    画了一张PHPCMSV9的运行流程思维导图
    MySQL的正则表达式
    linux patch 格式与说明(收录)
    Memcached笔记之分布式算法
    bzoj 2120 带修改莫队
    bzoj 2073 暴力
    bzoj 1814 Ural 1519 Formula 1 插头DP
  • 原文地址:https://www.cnblogs.com/walker993/p/14865224.html
Copyright © 2011-2022 走看看