CopyOnWriteArrayList是Java并发中提供的一个并发容器,它是个线程安全且读操作无锁的ArrayList。
写操作则通过创建底层数组的新副本来实现,是一种读写分离的并发策略,我们也可以称这种容器为”写时复制器”。
COW写时复制
CopyOnWrite简称COW
,当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。
这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。
所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
CopyOnWriteArrayList简介
CopyOnWriteArrayList是ArrayList的线程安全版本,从它的名字可以推测,CopyOnWriteArrayList是在有写操作的时候会copy一份数据,然后写完再设置成新的数据。
JDK 中提供了 CopyOnWriteArrayList 类,为了将读取的性能发挥到极致,CopyOnWriteArrayList 读取是完全不用加锁的,并且更厉害的是写入也不会阻塞读取操作。
只有写入和写入之间需要进行同步等待,这样一来,读操作的性能就会大幅度提升。
CopyOnWriteArrayList使用场景
CopyOnWriteArrayList适合使用在数据读多写少的情况下,如果数据对实时性要求比较高的业务场景则不适合使用CopyOnWriteArrayList。
在很多应用场景中,读操作可能会远远多于写操作。比如,有些系统级别的信息,往往只需要加载或者修改很少的次数,但是会被系统内所有模块频繁的访问。对于这种场景,我们最希望看到的就是读操作可以尽可能的快,而写即使慢一些也没关系。
读多写少
黑名单是最典型的场景,假如我们有一个搜索网站,用户在这个网站的搜索框中,输入关键字搜索内容,但是某些关键字不允许被搜索。这些不能被搜索的关键字会被放在一个黑名单中,黑名单并不需要实时更新,可能每天晚上更新一次就可以了。当用户搜索时,会检查当前关键字在不在黑名单中,如果在,则提示不能搜索。这种读多写少的场景也很适合使用 CopyOnWrite 集合。
CopyOnWriteArrayList优缺点
CopyOnWriteArrayList的优点
CopyOnWriteArrayList
的优点主要有两个:
- 线程安全
- 大大的提高了“读”操作的并发度(相比于
Vector
)
CopyOnWriteArrayList的缺点
1. 内存占用太大,缺点很明显的就是在wirte操作的时候,我们发现它都是使用Arrays.copyOf复制到一个新数组来完成的,如果有频繁的wirte操作就比较的占用内存,可能引起频繁的JVMYong GC和Full GC操作,write操作效率比较低;
2. 无法保证数据的实时一致性,因为其读和写在不同的array中操作,所以如果多个线程在操作,例如线程A、B,如果想要保证线程A写入线程B马上能读到就不行,但是不适合对实时性要求比较高的场景。
CopyOnWriteArrayList实现原理
我们都知道,集合框架中的ArrayList是非线程安全的,Vector虽是线程安全的,但由于简单粗暴的锁同步机制,性能较差,而CopyOnWriteArrayList则提供了另一种不同的并发处理策略(当然是针对特定的并发场景)。
很多时候,我们的系统应对的都是读多写少的并发场景,CopyOnWriteArrayList容器允许并发读,读操作是无锁的,性能较高。
至于写操作,比如向容器中添加一个元素,则首先将当前容器复制一份,然后在新副本上执行写操作,结束之后再将原容器的引用指向新容器。
下面我们就来一起看看CopyOnWriteArrayList
是如何实现线程安全的。
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private static final long serialVersionUID = 8673264195747942595L;
/** The lock protecting all mutators */
final transient ReentrantLock lock = new ReentrantLock();
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
从源码中我们可以知道CopyOnWriteArrayList
这和ArrayList
底层实现都是通过一个Object
的数组来实现的,只不过 CopyOnWriteArrayList
的数组是通过volatile
来修饰的,还有新增了ReentrantLock
。
add方法
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();
}
}
上述源码我们可以发现比较简单,有几个点需要稍微注意下
- 增加数据的时候是通过
ReentrantLock
加锁操作来(在jdk11
的时候采用了synchronized
来替换ReentrantLock
)保证多线程写的时候只有一个线程进行数组的复制,否则的话内存中会有多份被复制的数据,导致数据错乱。 - 数组是通过
volatile
修饰的,根据volatile
的happens-before
规则,写线程对数组引用的修改是可以立即对读线程是可见的。 - 通过写时复制来保证读写在两个不同的数据容器中进行操作。
get方法
get(int index)
方法用于获取指定位置的元素,源码如下:
/** * {@inheritDoc} * * @throws IndexOutOfBoundsException {@inheritDoc} */ public E get(int index) { // 调用内部get方法 return get(getArray(), index); } @SuppressWarnings("unchecked") private E get(Object[] a, int index) { return (E) a[index]; }
可以看到get(int index)
不需要加锁,因为CopyOnWriteArrayList
在add/remove
操作时,不会修改原数组,所以读操作不会存在线程安全问题。
这其实就是读写分离的思想,只有写入的时候才加锁,复制副本来进行修改。
CopyOnWriteArrayList总结
CopyOnWriteArrayList
利用ReentrantLock + volatile + 数组拷贝实现了线程安全的ArrayList,CopyOnWriteArrayList适合读远大于写的场景。
陈睿mikechen
10年+大厂架构经验,资深技术专家,就职于阿里巴巴、淘宝、百度等一线互联网大厂。
关注「mikechen」公众号,获取更多技术干货!
后台回复【面试】即可获取《史上最全阿里Java面试题总结》,后台回复【架构】,即可获取《阿里架构师进阶专题全部合集》