目录
一、基本介绍
并发包中的并发List只有CopyOnWriteArrayList。
CopyOnWriteArrayList是一个线程安全的ArrayList,对其进行的修改操作都是在底层的一个复制的
数组(快照)上进行的,也就是使用了写时复制策略。
CopyOnWriteArraylist的类图结构如图:
在CopyOnWriteArrayList的类图中,每个CopyOnWriteArrayList对象里面有一个array数组对象用
来存放具体元素,ReentrantLock独占锁对象用来保证同时只有一个线程对array进行修改。
CopyOnWriteArrayList使用写时复制的策略来保证list的一致性,而获取 - 修改 - 写入三步操作并不
是原子性的,所以在增删改的过程中都使用了独占锁,来保证在某个时间只有一个线程能对list数
组进行修改。
另外CopyOnWriteArrayList提供了弱一致性的迭代器,从而保证在获取迭代器后,其他线程对list
的修改是不可见的,迭代器遍历的数组是一个快照。
二、基本原理
很多时候,我们的系统应对的都是读多写少的并发场景。
CopyOnWriteArrayList容器允许并发读,读操作是无锁的,性能较高。
至于写操作,比如向容器中添加一个元素,则首先将当前容器复制一份,然后在新副本上执行写操
作,结束之后再将原容器的引用指向新容器。
- 线程安全的,多线程环境下可以直接使用,无需加锁;
- 通过锁 + 数组拷贝 + volatile 关键字保证了线程安全;
- 每次数组操作,都会把数组拷贝一份出来,在新数组上进行操作,操作成功之后再赋值回去。
从整体架构上来说,CopyOnWriteArrayList 数据结构和 ArrayList 是一致的,底层是个数组,只不
过CopyOnWriteArrayList 在对数组进行操作的时候,基本会分四步走:
- 加锁;
- 从原数组中拷贝出新数组;
- 在新数组上进行操作,并把新数组赋值给数组容器;
- 解锁
除了加锁之外,CopyOnWriteArrayList 的底层数组还被 volatile 关键字修饰,意思是一旦数组被修
改,其它线程立马能够感知到,代码如下:
private transient volatile Object[] array;
整体上来说,CopyOnWriteArrayList 就是利用锁 + 数组拷贝 + volatile 关键字保证了 List 的线程安
全。
三、优缺点
1. 优点
读操作(不加锁)性能很高,因为无需任何同步措施,比较适用于读多写少的并发场景。Java的
list在遍历时,若中途有别的线程对list容器进行修改,则会抛ConcurrentModificationException异
常。而CopyOnWriteArrayList由于其"读写分离"的思想,遍历和修改操作分别作用在不同的list容
器,所以在使用迭代器进行遍历时候,也就不会抛出ConcurrentModificationException异常了。
2. 缺点
一是内存占用问题,毕竟每次执行写操作都要将原容器拷贝一份。数据量大时,对内存压力较大,
可能会引起频繁GC;
二是无法保证实时性,因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同
时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,
只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内
存)。
四、源码分析
添加
public boolean add(E e) {
//ReentrantLock加锁,保证线程安全
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();
}
}
添加的逻辑很简单,先将原容器copy一份,然后在新副本上执行写操作,之后再切换引用。
当然此过程是要加锁的。
删除
public E remove(int index) {
//加锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
E oldValue = get(elements, index);
int numMoved = len - index - 1;
if (numMoved == 0)
//如果要删除的是列表末端数据,拷贝前len-1个数据到新副本上,再切换引用
setArray(Arrays.copyOf(elements, len - 1));
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 {
//解锁
lock.unlock();
}
}
删除操作同理,将除要删除元素之外的其他元素拷贝到新副本中,然后切换引用,将原容器引用指
向新副本。同属写操作,需要加锁。
我们再来看看读操作,CopyOnWriteArrayList的读操作是不用加锁的,性能很高。
public E get(int index) {
return get(getArray(), index);
}
直接读取即可,无需加锁
private E get(Object[] a, int index) {
return (E) a[index];
}
弱一致性的迭代器
所谓弱一致性是指返回迭代器后,其他线程对list的增删改查对迭代器是不可见的
// 演示多线程下迭代器的弱一致性结果
public class copylist {
private static volatile CopyOnWriteArrayList<String> arrayList = new CopyOnWriteArrayList<>();
public static void main(String[] args) throws InterruptedException {
arrayList.add("hello");
arrayList.add("alibaba");
arrayList.add("welcome");
arrayList.add("to");
arrayList.add("hangzhou");
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
// 修改list中下标为1的元素为ali
arrayList.set(1, "ali");
// 删除元素
arrayList.remove(2);
arrayList.remove(3);
}
});
// 保证在修改线程启动前获取迭代器
Iterator<String> itr = arrayList.iterator();
// 启动线程
threadOne.start();
// 等待子线程执行完毕
threadOne.join();
while(itr.hasNext()) {
System.out.println(itr.next());
}
}
}
执行程序:
hello
alibaba
welcome
to
hangzhou
Process finished with exit code 0
执行程序:
hello
alibaba
welcome
to
hangzhou
Process finished with exit code 0
从输出结果我们知道,在子线程里面进行的操作一个都没有生效,这就是迭代器弱一致性的体现。
需要注意的是,获取迭代器的操作必须在子线程操作之前进行。
五、ArrayList转为线程安全的方法
List list = Collections.synchronizedList(new ArrayList());