集合 - CopyOnWriteArrayList

前言

ArrayList 是一个非线程安全集合,需要使用方自行处理线程安全问题,或者使用 Collections.synchronizedList 包装。从 JDK 1.5 开始 JUC 中提供了使用写时复制机制实现的并发容器 CopyOnWriteArrayList

概述

CopyOnWriteArrayList 遵从 CopyOnWrite,顾名思义就是写时复制。简单来说就是当我们操作容器时并不直接操作当前容器,而是先根据当前容器复制出一个新的容器,然后在这个新的容器上操作,完成操作后再将原容器引用指向新容器。

源码解析

CopyOnWriteArrayList 的类继承关系类图如下:

属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private static final long serialVersionUID = 8673264195747942595L;

/**
* 保护所有更改操作的锁
*/
final transient ReentrantLock lock = new ReentrantLock();

/**
* 内部数组。
* 所有的读操作都是基于当前 array 进行的;所有的写操作都会复制一个新的 array,然后在新复制的数组上执行操作。
*/
private transient volatile Object[] array;
}

CopyOnWriteArrayList 核心属性就是以上两个。lock 用于保护所有更改操作,控制并发写操作。array 作为内部数组用于保存数据,读操作都是操作当前 array,写操作都是根据当前 array 复制一个新的 array,然后在这个新的数组中进行操作。

新增

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
public boolean add(E e) {
final ReentrantLock lock = this.lock;
// 1 获得独占锁
lock.lock();
try {

// 2 获得 array 数组
Object[] elements = getArray();

// 3 获得 array 数组长度
int len = elements.length;

// 4 根据当前 array 复制一个新的数组,长度 +1
Object[] newElements = Arrays.copyOf(elements, len + 1);

// 5 将元素加入到新数组
newElements[len] = e;

// 6 替换旧数组
setArray(newElements);
return true;
} finally {
// 7 释放独占锁
lock.unlock();
}
}

下面对上述代码流程进行说明:

  1. 使用 ReentrantLock 独占锁保证同时只有一个线程对集合进行写操作。
  2. 数据存储在 CopyOnWriteArrayList 的内部 array 数组中。
  3. 元素并不是直接存储到 array 数组中,而是复制出一个新的数组,并且复制出的数组的长度是旧数组长度+1,然后将元素加入到新数组中,最后再把旧的数组替换成新的数组。

查询

1
2
3
4
5
6
7
8
9
10
11
12
13
 /**
* {@inheritDoc}
*
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E get(int index) {
return get(getArray(), index);
}

@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
return (E) a[index];
}

由于整个 get 方法是没有加锁的,而 CopyOnWriteArrayList 的写操作是通过复制出新的数组来完成操作的,这可能会导致读写的短暂不一致。

修改

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
public E set(int index, E element) {
final ReentrantLock lock = this.lock;
// 1 获得独占锁
lock.lock();
try {

// 2 获得当前 数组 array
Object[] elements = getArray();

// 3 根据下标,获得旧的元素
E oldValue = get(elements, index);

// 4 如果旧的元素不等于新的元素,等同于 add 方法,不过这里没有增加数组容量大小
if (oldValue != element) {
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len);
newElements[index] = element;
setArray(newElements);

// 5 为了保证 volatile 语义,即使元素没有改变也要替换成新的数组
} else {
// Not quite a no-op; ensures volatile write semantics
setArray(elements);
}
return oldValue;
} finally {

// 6 释放独占锁
lock.unlock();
}
}

和 add 方法类似,不同的是对要修改的元素进行判断。

删除

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
public E remove(int index) {
final ReentrantLock lock = this.lock;
// 1 获取独占锁
lock.lock();
try {

// 2 获取对应下标元素
Object[] elements = getArray();
int len = elements.length;
E oldValue = get(elements, index);

// 3 确定调整位置,并根据该位置移动元素
int numMoved = len - index - 1;

// 3.1 要删除的元素在末尾
if (numMoved == 0)
setArray(Arrays.copyOf(elements, len - 1));

// 3.2 要删除的元素在数组中间
else {
Object[] newElements = new Object[len - 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
setArray(newElements);
}
return oldValue;
} finally {
// 4 释放锁
lock.unlock();
}
}

删除方法和其它修改方法类似,先是获取独占锁来保证线程安全,接着判断要删除的元素是否是最后一个,如果是最后一个则复制出一个长度-1的新数组然后替换掉旧数组;如果要删除的元素不是最后一个,则分两次复制,随后替换旧数组。

迭代器

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
/**
* 迭代器
*
* @param <E>
*/
static final class COWIterator<E> implements ListIterator<E> {
/**
* 数据快照
*/
private final Object[] snapshot;

/**
* 遍历数组元素的游标
*/
private int cursor;

private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}

/**
* 判断是否还有下一个元素
*
* @return
*/
public boolean hasNext() {
return cursor < snapshot.length;
}

/**
* 判断是否有上一个元素
*
* @return
*/
public boolean hasPrevious() {
return cursor > 0;
}

/**
* 获取下个元素
*
* @return
*/
@SuppressWarnings("unchecked")
public E next() {
if (!hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}

@SuppressWarnings("unchecked")
public E previous() {
if (!hasPrevious())
throw new NoSuchElementException();
return (E) snapshot[--cursor];
}

// 不支持写操作
public void remove() {
throw new UnsupportedOperationException();
}
public void set(E e) {
throw new UnsupportedOperationException();
}
public void add(E e) {
throw new UnsupportedOperationException();
}
}

当获取迭代器时,内部会调用 COWIterator 的构造方法,该构造方法有两个参数,一个是 array 数组,另一个是下标 0 。构造方法中会把 array 数组赋值给 snapshot 变量,当其他线程进行了增删改的操作,旧的数组会被新的数组给替换掉,但是 snapshot 还是原来旧的数组的引用。当我们使用迭代器遍历 CopyOnWriteArrayList 的时候,不能保证拿到的数据是最新的,这是弱一致性问题。

设计特点

CopyOnWriteArrayList 仅适用于写操作非常少的场景,而且能够容忍读写的短暂不一致。虽然采用了读写分离思想,但写操作时内存里会同时存在两个对象的内存,若这些对象占用内存较大,可能会带来系列问题,如造成频繁 GC。

小结

CopyOnWriteArrayList 是 Java 并发包中相对比较简单的一个实现,它的核心思想是写时复制机制,支持并发读写,但带来的后果是内存 double 和数据一致性问题。