JDK并发工具类源码学习系列——CopyOnWriteArrayList

CopyOnWriteArrayList是ArrayList的一个线程安全的变体,其中所有可变操作(add、set 等等)都是通过对底层数组进行一次新的复制来实现的。 这一般需要很大的开销,但是当遍历操作的数量大大超过可变操作的数量时,这种方法可能比其他替代方法更 有效。在不能或不想进行同步遍历,但又需要从并发线程中排除冲突时,它也很有用。“快照”风格的迭代器方法在创建迭代器时使用了对数组状态的引用。此数组在迭代器的生存期内不会更改,因此不可能发生冲突,并且迭代器保证不会抛出 ConcurrentModificationException。创建迭代器以后,迭代器就不会反映列表的添加、移除或者更改。在迭代器上进行的元素更改操作(remove、set 和 add)不受支持。这些方法将抛出 UnsupportedOperationException。 允许使用所有元素,包括 null。

内存一致性效果:当存在其他并发 collection 时,将对象放入 CopyOnWriteArrayList 之前的线程中的操作 happen-before 随后通过另一线程从 CopyOnWriteArrayList 中访问或移除该元素的操作。


以上介绍摘自API文档。

根据API对CopyOnWriteArrayList的介绍,其原理以及使用场景已经比较清晰了,下面我们通过源码来分析下。

实现原理

API已经说的比较清楚了,由于数组的特殊结构,所以如果想要对数据进行结构性修改,如增加一个元素,删除一个元素,都是很麻烦的,所以无法将对一个数组的结构性修改缩小到一个原子指令范围,不像链表可以通过CAS修改next指针来修改链表。所以CopyOnWriteArrayList通过将任何对底层数组进行结构性修改的操作变成针对一个新的副本的修改,然后用修改后的副本来替换原来的数组,来实现遍历与修改分离,以保证数组高效的访问效率。

常用方法解读

CopyOnWriteArrayList的重要的几个方法:add(int, E)/add(E)/set(int, E)/remove(int)/iterator(),其中前四个是对CopyOnWriteArrayList的结构进行修改,最后一个是对CopyOnWriteArrayList进行遍历。下面针对源码逐一进行分析。

add(int, E)和add(E)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/**
* Inserts the specified element at the specified position in this list.
* Shifts the element currently at that position (if any) and any subsequent
* elements to the right (adds one to their indices).
*
* @throws IndexOutOfBoundsException
* {@inheritDoc}
*/

public void add(int index, E element) {
// 加锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 读取底层数组对象
Object[] elements = getArray();
int len = elements.length;
if (index > len || index < 0)
throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + len);
Object[] newElements;
// 由于数组的长度不可变,所以插入一个元素需要新建一个新的数组以容纳新插入的元素
// 所以需要将原数组复制到新的数组
int numMoved = len - index;// 需要移动的元素的开始位置(要插入位置的下一个位置)
if (numMoved == 0) // ==0表示插入的位置是数组的最后一个位置,所以该位置前面的元素原样不动复制到新的数组即可
// 这里通过复制elements数组生成一个新的数组,注意这里新的数组长度是原数组+1,所以新数组的最后一个元素是NULL
newElements = Arrays.copyOf(elements, len + 1);
else {
// 将原数组的0~index-1原样复制到新的数组中,
// 而index之后的元素对应复制到新数组的index+1之后,即中间空出一个位置用于放置带插入元素
newElements = new Object[len + 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index, newElements, index + 1, numMoved);
}
// 将element插入到新的数组
newElements[index] = element;
// 将更新底层数组的引用,由于array是volatile的,所以对其的修改能够立即被后续线程可见
setArray(newElements);
} finally {
// 释放锁
lock.unlock();
}
}

/**
* Appends the specified element to the end of this list.
*
* @param e
* element to be appended to this list
* @return <tt>true</tt> (as specified by {@link Collection#add})
*/

// @By Vicky:该方法相当于调用add(array.length, e)
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}

add()方法的实现很简单,通过加锁保证线程安全,通过Arrays.copyOf根据原数组复制一个新的数组,将要插入的元素插入到新的数组的对应位置,然后将新的数组赋值给array,通过volatile保证内存可见。

set(int, E)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* Replaces the element at the specified position in this list with the
* specified element.
*
* @throws IndexOutOfBoundsException
* {@inheritDoc}
*/

// @By Vicky:更新指定位置元素
public E set(int index, E element) {
// 加锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
// 获取需更新的元素
Object oldValue = elements[index];
// //
// 需更新的值不等于原值(注意此处的不等是==,不是equals(),即oldValue和element必须是引用同一个对象才可)
if (oldValue != element) {
int len = elements.length;
// 复制一个新的数组,并将index更新成新的值,更新引用
Object[] newElements = Arrays.copyOf(elements, len);
newElements[index] = element;
setArray(newElements);
} else {
// Not quite a no-op; ensures volatile write semantics
// 此处由于更新的值与原值是同一个对象,所以其实可不更新引用
// 从注释可以看出更新的目的是出于写volatile变量
setArray(elements);
}
return (E) oldValue;
} finally {
// 释放锁
lock.unlock();
}
}

set()比add()更新简单,只需要复制一个新的数组,然后更新新的数组的指定位置的元素,然后更新引用即可。

remove(int)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
/**
* Removes the element at the specified position in this list.
* Shifts any subsequent elements to the left (subtracts one from their
* indices). Returns the element that was removed from the list.
*
* @throws IndexOutOfBoundsException {@inheritDoc}
*/

// @By Vicky:删除指定位置的元素
public E remove(int index) {
// 加锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object oldValue = elements[index];
int numMoved = len - index - 1;// 需要移动的元素的个数
if (numMoved == 0) // ==0表示删除的位置是数组的最后一个元素,只需要简单的复制原数组的len-1个元素到新数组即可
setArray(Arrays.copyOf(elements, len - 1));
else {
// 将原数组的0-index-1复制到新数组的对应位置
// 将原数组的index+1之后的元素复制到新数组,丢弃原数组的index位置的元素
Object[] newElements = new Object[len - 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index + 1, newElements, index, numMoved);
setArray(newElements);
}
return (E) oldValue;
} finally {
lock.unlock();
}
}

// @By Vicky:删除指定元素,而非指定位置的元素
public boolean remove(Object o) {
// 加锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
if (len != 0) {
// Copy while searching for element to remove
// This wins in the normal case of element being present
int newlen = len - 1;// 删除之后数组的长度
Object[] newElements = new Object[newlen];// 创建新的数组

for (int i = 0; i < newlen; ++i) {// 从0-len-1遍历原数组
if (eq(o, elements[i])) {// 如果是待删除元素,则将该元素之后的元素复制到新数组中
// found one; copy remaining and exit
for (int k = i + 1; k < len; ++k)
newElements[k - 1] = elements[k];
// 设置新数组
setArray(newElements);
return true;
} else
// 将该元素插入到新数组
newElements[i] = elements[i];
}

// 确认最后原数组一个元素是否与待删除元素相等,是的话直接将修改引用即可,因为前面已经为新数组赋完值了
// special handling for last cell
if (eq(o, elements[newlen])) {
setArray(newElements);
return true;
}
}
// 到这里说明数组中没有与待删除元素相等的元素,所以直接返回false,
// 但是这里并没有写volatile变量,看来set那里也只是写着好玩
return false;
} finally {
lock.unlock();
}
}

remove()有两种方式,根据指定位置删除以及指定元素删除两种方式。

iterator()

这里的iterator()只是很简单的迭代器,内部将remove/set/add三个修改操作进行了限制,因为这里的迭代器不能修改集合,代码就不细看了。注意到iterator并没有加锁,因为iterator所访问的数组是不会变的,就算有其他线程对集合进行修改。

使用场景

CopyOnWriteArrayList适合读多写少的场景。通过空间换时间的方式来提高读的效率并保证写的安全性。