关于CopyOnWriteArrayList的介绍可以参考之前文章中的CopyOnWriteArrayList、CopyOnWriteArraySet(其中分析了它的特点以及适合的使用场景,建议看一下)。本文原本是打算分析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写后,共享变量的状态示意图:
如上图所示,线程A在写flag变量后,本地内存A中被线程A更新过的两个共享变量的值被刷新到主内存中。此时,本地内存A和主内存中的共享变量的值是一致的。
volatile读的内存语义如下:
- 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
下面是线程B读同一个volatile变量后,共享变量的状态示意图:
如上图所示,在读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不可见。
- 就分析到这里。