「源码分析」CopyOnWriteArrayList 中的隐藏知识,你Get了吗?
欢迎点击 “未读代码” ,关注公众号,文章每周更新
杭州-阿里园区墙
前言
本觉 CopyOnWriteArrayList 过于简单,寻思看名字就能知道内部的实现逻辑,所以没有写这篇文章的想法,最近又仔细看了下 CopyOnWriteArrayList 的源码实现,大体逻辑没有意外,不过还是发现很多有意思的地方,固留此篇文章分享之。
看完这篇文章你会了解到:
CopyOnWriteArrayList 的实现原理,扩容机制。 CopyOnWriteArrayList 的读写分离,弱一致性。 CopyOnWriteArrayList 的性能如何。 CopyOnWriteArrayList 修改元素时,为什么相同值也要重新赋值(作者 Doug Lea 这么写都是有道理的)。 CopyOnWriteArrayList 在高版本 JDK 的实现有什么不同,为什么。
线程安全 List
在 Java 中,线程安全的 List 不止一个,除了今天的主角 CopyOnWriteArrayList 之外,还有 Vector 类和 SynchronizedList 类,它们都是线程安全的 List 集合。在介绍 CopyOnWriteArrayList 之前,先简单介绍下另外两个。
如果你尝试你查看它们的源码,你会发现有点不对头,并发集合不都是在 java.util.concurrent
包中嘛,为什么Vector 类和 SynchronizedList 类 这两个是在 java.util
包里呢?
确实是这样的,这两个线程安全的 List 和线程安全的 HashTable 是一样的,都是比较简单粗暴的实现方式,直接方法上增加 synchronized
关键字实现的,而且不管增删改查,统统加上,即使是 get 方法也不例外,没错,就是这么粗暴。
Vector 类的 get 方法:
// Vector 中的 get 操作添加了 synchronized
public synchronized E get(int index) {
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
return elementData(index);
}
SynchronizedList 类的 ge t 方法:
public E get(int index) {
synchronized (mutex) {return list.get(index);}
}
同学不妨思考一下,其实在 get 方法上添加同步机制也是有原因的,虽然降低了效率,但是可以让写入的数据立即可以被查询到,这也保证了数据的强一致性。另外上面关于 synchronized 简单粗暴的描述也是不够准确的,因为在高版本的 JDK 中,synchronized 已经可以根据运行时情况,自动调整锁的粒度,后面介绍 CopyOnWriteArrayList 时会再次讲到。
CopyOnWriteArrayList
在 JDK 并发包中,目前关于 List 的并发集合,只有 CopyOnWriteArrayList 一个。上面简单介绍了 Vector 和 SynchronizdList 的粗暴实现,既然还有 CopyOnWriteArrayList,那么它一定是和上面两种是有区别的,作为唯一的并发 List,它有什么不同呢?
在探究 CopyOnWriteArrayList 的实现之前,我们不妨先思考一下,如果是你,你会怎么来实现一个线程安全的 List。
并发读写时该怎么保证线程安全呢? 数据要保证强一致性吗?数据读写更新后是否立刻体现? 初始化和扩容时容量给多少呢? 遍历时要不要保证数据的一致性呢?需要引入 Fail-Fast 机制吗?
通过类名我们大致可以猜测到 CopyOnWriteArrayList 类的实现思路:Copy-On-Write, 也就是写时复制策略;末尾的 ArrayList 表示数据存放在一个数组里。在对元素进行增删改时,先把现有的数据数组拷贝一份,然后增删改都在这个拷贝数组上进行,操作完成后再把原有的数据数组替换成新数组。这样就完成了更新操作。
但是这种写入时复制的方式必定会有一个问题,因为每次更新都是用一个新数组替换掉老的数组,如果不巧在更新时有一个线程正在读取数据,那么读取到的就是老数组中的老数据。其实这也是读写分离的思想,放弃数据的强一致性来换取性能的提升。
分析源码 ( JDK8 )
上面已经说了,CopyOnWriteArrayList 的思想是写时复制,读写分离,它的内部维护着一个使用 volatile 修饰的数组,用来存放元素数据。
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
CopyOnWriteArrayList 类中方法很多,这里不会一一介绍,下面会分析其中的几个常用的方法,这几个方法理解后基本就可以掌握 CopyOnWriteArrayList 的实现原理。
构造函数
CopyOnWriteArrayList 的构造函数一共有三个,一个是无参构造,直接初始化数组长度为0;另外两个传入一个集合或者数组作为参数,然后会把集合或者数组中的元素直接提取出来赋值给 CopyOnWriteArrayList 内部维护的数组。
// 直接初始化一个长度为 0 的数组
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
// 传入一个集合,提取集合中的元素赋值到 CopyOnWriteArrayList 数组
public CopyOnWriteArrayList(Collection c) {
Object[] es;
if (c.getClass() == CopyOnWriteArrayList.class)
es = ((CopyOnWriteArrayList)c).getArray();
else {
es = c.toArray();
if (c.getClass() != java.util.ArrayList.class)
es = Arrays.copyOf(es, es.length, Object[].class);
}
setArray(es);
}
// 传入一个数组,数组元素提取后赋值到 CopyOnWriteArrayList 数组
public CopyOnWriteArrayList(E[] toCopyIn) {
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
构造函数是实例创建时调用的,没有线程安全问题,所以构造方法都是简单的赋值操作,没有特殊的逻辑处理。
新增元素
元素新增根据入参的不同有好几个,但是原理都是一样的,所以下面只贴出了 add(E e )
的实现方式,是通过一个 ReentrantLock 锁保证线程安全的。
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#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); // 拷贝一个数据数组,长度+1
newElements[len] = e; // 加入新元素
setArray(newElements); // 用新数组替换掉老数组
return true;
} finally {
lock.unlock();
}
}
具体步骤:
加锁,获取目前的数据数组开始操作(加锁保证了同一时刻只有一个线程进行增加/删除/修改操作)。 拷贝目前的数据数组,且长度增加一。 新数组中放入新的元素。 用新数组替换掉老的数组。 finally 释放锁。
由于每次 add 时容量只增加了1,所以每次增加时都要创建新的数组进行数据复制,操作完成后再替换掉老的数据,这必然会降低数据新增时候的性能。下面通过一个简单的例子测试 CopyOnWriteArrayList 、Vector、ArrayList 的新增和查询性能。
public static void main(String[] args) {
CopyOnWriteArrayList