CopyOnWriteArrayList与java内存模型

关于CopyOnWriteArrayList的介绍可以参考之前文章中的CopyOnWriteArrayListCopyOnWriteArraySet(其中分析了它的特点以及适合的使用场景,建议看一下)。本文原本是打算分析CopyOnWriteArrayList的源码的,结果看了一下其源码实现比较简单,只是有一个地方很难理解。本文就只探讨这个点。

相关代码如下:

 /** The array, accessed only via getArray/setArray. */
    private volatile transient Object[] array;


 public E set(int index, E element) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            E oldValue = get(elements, index);

            if (oldValue != element) {
                int len = elements.length;
                Object[] newElements = Arrays.copyOf(elements, len);
                newElements[index] = element;
                setArray(newElements);
            } else {
                // Not quite a no-op; ensures volatile write semantics
                setArray(elements);
            }
            return oldValue;
        } finally {
            lock.unlock();
        }
 }

我们知道,COWArrayList底层有一个不可变数组,任何对它的修改都会导致整个数组被复制,这正是上面if代码块中所做的工作,难理解的地方发生在else代码块中,这里oldValue == element,值没有发生变化,按理说不需要任何修改,那为什么这里调用setArray方法呢?注释中所说的保证volatile写语义(ensures volatile write semantics)又是什么意思呢?

首先来看一下volatile write semantics是什么意思?

在回答这个问题之前,我们先来看一下与之相关的一个概念happens-before,在之前的文章《深入理解Java内存模型(一)——基础》中对其做了介绍,我们摘取部分内容如下。

从JDK5开始,java使用新的JSR -133内存模型(本文除非特别说明,针对的都是JSR- 133内存模型)。JSR-133使用happens-before的概念来阐述操作之间的内存可见性。在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。
与程序员密切相关的happens-before规则如下:

  • 程序顺序规则:一个线程中的每个操作,happens- before 于该线程中的任意后续操作。
  • 监视器锁规则:对一个监视器锁的解锁,happens- before 于随后对这个监视器锁的加锁
  • volatile变量规则:对一个volatile域的,happens- before 于任意后续对这个volatile域的
  • 传递性:如果A happens- before B,且B happens- before C,那么A happens- before C。

关于对happen-before的理解,个人的经验是不要纠结于多线程,要从底层(CPU、内存)理解。

接下来我们就来看一下volatile的内存语义,在前面的文章《深入理解Java内存模型(四)——volatile》中详细介绍了volatile的内存语义,强烈建议读者再仔细读一下。这里我们摘抄部分内容如下:

volatile写-读的内存语义

volatile写的内存语义如下:

  • 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存。

以上面示例程序VolatileExample为例,假设线程A首先执行writer()方法,随后线程B执行reader()方法,初始时两个线程的本地内存中的flag和a都是初始状态。下图是线程A执行volatile写后,共享变量的状态示意图:

《CopyOnWriteArrayList与java内存模型》

如上图所示,线程A在写flag变量后,本地内存A中被线程A更新过的两个共享变量的值被刷新到主内存中。此时,本地内存A和主内存中的共享变量的值是一致的。

volatile读的内存语义如下:

  • 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

下面是线程B读同一个volatile变量后,共享变量的状态示意图:

《CopyOnWriteArrayList与java内存模型》

如上图所示,在读flag变量后,本地内存B已经被置为无效。此时,线程B必须从主内存中读取共享变量。线程B的读取操作将导致本地内存B与主内存中的共享变量的值也变成一致的了。

如果我们把volatile写和volatile读这两个步骤综合起来看的话,在读线程B读一个volatile变量后,写线程A在写这个volatile变量之前所有可见的共享变量的值都将立即变得对读线程B可见。

下面对volatile写和volatile读的内存语义做个总结:

  • 线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所在修改的)消息。
  • 线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息。
  • 线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。
  • 现在答案基本上已经出来了,setArray(elements)是对volatile成员array的写操作,假设现在有两个线程A,B,有一个共享变量int global,有一个CopyOnWriteArrayList实例cow。
  • 其中A线程中执行如下操作:
  • global = 1;    //a
  • cow.set(1,element);     //b
  • 在线程B中执行如下操作:
  • cow.get(1);       // c
  • int copy = global;    //d
  • 根据上面所介绍的volatile的内存语义,线程A中对global变量的修改对线程B是可见的。分析如下:
  • a happen-before b (程序顺序规则:一个线程中的每个操作,happens- before 于该线程中的任意后续操作。
  • c happen-before d(同上)
  • b happen-before c (volatile变量规则:对一个volatile域的,happens- before 于任意后续对这个volatile域的
  • a happen-before d (传递性:如果A happens- before B,且B happens- before C,那么A happens- before C。
  • 假如上面的set函数中没有setArray(elements)的话,线程A中对global的修改就可能对线程B不可见。
  • 就分析到这里。






    原文作者:java内存模型
    原文地址: https://blog.csdn.net/cumtwyc/article/details/52267414
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞