Java进阶—CopyOnWriteArrayList

并发编程—ArrayList线程安全问题一文中提到ArrayList是线程不安全的,可以使用CopyOnWriteArrayList来保证线程安全。本文主要介绍一下CopyOnWriteArrayList这个类。

源码分析

首先我们可以看下CopyOnWriteArrayList的数据结构,通过源码可以看到CopyOnWriteArrayList类中维护一个array对象数组用于存储集合的每个元素,并且array数组只能通过getArray和setArray方法来访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/** The lock protecting all mutators */
final transient ReentrantLock lock = new ReentrantLock();

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

/**
* Gets the array. Non-private so as to also be accessible
* from CopyOnWriteArraySet class.
*/
final Object[] getArray() {
return array;
}

/**
* Sets the array.
*/
final void setArray(Object[] a) {
array = a;
}

为什么和ArrayList相比,CopyOnWriteArrayList是线程安全的呢?就是因为CopyOnWriteArrayList的写操作都加了锁。看一下add(E e)方法的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public boolean add(E e) {
final ReentrantLock lock = this.lock;
//1、先加锁
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
//2、拷贝数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
//3、将元素加入到新数组中
newElements[len] = e;
//4、将array引用指向到新数组
setArray(newElements);
//5、解锁
return true;
} finally {
lock.unlock();
}
}

可以看到调用add方法的时候,也是通过getArray()获取到对象数组,再直接新生成一个数组,最后把旧的数组的值复制到新的数组中,然后直接使用新的数组覆盖实例变量array。这也是它名字的由来:写时复制

同样的在删除CopyOnWriteArrayList里的元素时,也同样会有一个这样复制的过程。

下图演示了两个线程并发读写CopyOnWriteArrayList的情况,其中线程1需要通过iterator()方法读取数据,线程2往集合中添加一个元素:元素3,线程2的操作是直接基于集合当前的数据进行复制一份到新的一个数组,最后将array变量指向新的一个数组。

iterator()方法本质上就是遍历数组array:

1
2
3
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}

注意思考这样的一个场景:假设线程1在遍历元素的时候,拿到的是复制之前的数组,里面包含了元素1和元素2,于此同时线程2复制出一个新数组并添加了元素3,这个时候线程1是无法读取到元素3的。这也是CopyOnWriteArrayList的一个特点:弱一致性。意思就是说线程1看到的是某一时刻的一份“快照数据”,无法保证能读取到最新的数据。

总结

CopyOnWriteArrayList通过加锁的方式解决了ArrayList并发的线程安全问题,通过弱一致性提升读请求并发,适合用在数据读多写少的场景,比如数据库配置这种,基本已读取为主。但它也存在一些缺点:

  1. 有了锁的额外开销。
  2. 每次写操作都要进行拷贝,比较消耗性能和内存。
  3. 无法保证数据额实时一致性。

CopyOnWriteArrayList透露的思想

分析CopyOnWriteArrayList表达的一些思想:

  1. 读写分离,读和写分开
  2. 最终一致性
  3. 使用另外开辟空间的思路,来解决并发冲突
------ 本文完 ------