CopyOnWriteArrayList的简单使用

CopyOnWriteArrayList的介绍

CopyOnWriteArrayList是ArrayList的一个线程安全的变体,其中所有可变操作(add、set等等)都是通过对底层数组进行一次新的复制来实现的, 一般在多线程操作时,一个线程对list进行修改。一个线程对list进行foreach时会出现, java.util.ConcurrentModificationException错误。

下面来看一个列子:两个线程,一个线程foreach,一个线程修改list的值。

读线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 读线程
*/
private static class ReadTask implements Runnable {
List<String> list;
public ReadTask(List<String> list) {
this.list = list;
}
@Override
public void run() {
for (String str : list) {
log.info("ReadTask>>>>>" + str);
}
}
}

写线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 写线程
*/
private static class WriteTask implements Runnable {
private int index;
private List<String> list;
public WriteTask(int index, List<String> list) {
this.index = index;
this.list = list;
}
@Override
public void run() {
list.remove(index);
list.add(index, "write>>" + index);
}
}

运行代码

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
private void run() {
final int NUM = 10;
List<String> list = new ArrayList<String>();
// List<String> list = new CopyOnWriteArrayList<String>();
for (int i = 0; i < NUM; i++) {
list.add("main_" + i);
}
// 创建指定线程数量的线程池
ExecutorService executorService = Executors.newFixedThreadPool(NUM);
for (int i = 0; i < NUM; i++) {
executorService.execute(new ReadTask(list));
executorService.execute(new WriteTask(i, list));
}
// 等待线程任务执行结束之后,关闭线程
// executorService.shutdown();
// 立即关闭所有线程(即便是正在执行线程任务的线程,但结果不一定能够成功关闭)
executorService.shutdownNow();
}
public static void main(String[] args) {
// java.util.ConcurrentModificationException,修改并发异常
new CopyOnWriteArrayListDemo().run();
}

运行结果

从结果中可以看出来。在多线程情况下报错。其原因就是多线程操作结果:那这个种方案不行我们就换个方案。用jdk自带的类CopyOnWriteArrayList来做容器。

换了种方案看代码 :

1
2
3
final int NUM = 10;
// List<String> list = new ArrayList<String>();
List<String> list = new CopyOnWriteArrayList<String>();

运行上面的代码,没有报出,java.util.ConcurrentModificationException异常,说明了CopyOnWriteArrayList并发多线程的环境下,仍然能很好的工作

CopyOnWriteArrayList源码分析

CopyOnWriteArrayList使用了一种叫写时复制的方法,当有新元素添加到CopyOnWriteArrayList时,先从原有的数组中拷贝一份出来,然后在新的数组做写操作,写完之后,再将原来的数组引用指向到新数组。

当有新元素加入的时候,如下图,创建新数组,并往新数组中加入一个新元素,这个时候,array这个引用仍然是指向原数组的。

当元素在新数组添加成功后,将array这个引用指向新数组。

CopyOnWriteArrayList的整个add操作都是在锁的保护下进行的,这样做是为了避免在多线程并发add的时候,复制出多个副本出来,把数据搞乱了,导致最终的数组数据不是我们期望的。

CopyOnWriteArrayList的add()方法操作的源代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public boolean add(E e) {
//1、先加锁
final ReentrantLock lock = this.lock;
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);
return true;
} finally {
//5、解锁
lock.unlock();
}
}

由于所有的写操作都是在新数组进行的,这个时候如果有线程并发的写,则通过锁来控制,如果有线程并发的读,则分几种情况:

(1) 如果写操作未完成,那么直接读取原数组的数据;
(2) 如果写操作完成,但是引用还未指向新数组,那么也是读取原数组数据;
(3) 如果写操作完成,并且引用已经指向了新的数组,那么直接从新数组中读取数据。

可见,CopyOnWriteArrayList的读操作是可以不用加锁的。

CopyOnWriteArrayList的使用场景

通过上面的分析,CopyOnWriteArrayList 有几个缺点:

(1) 由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致young gc或者full gc
(2) 不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个set操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求;

CopyOnWriteArrayList 合适读多写少的场景,不过这类慎用,因为谁也没法保证CopyOnWriteArrayList 到底要放置多少数据,万一数据稍微有点多,每次add/set都要重新复制数组,这个代价实在太高昂了。在高性能的互联网应用中,这种操作分分钟引起故障。

CopyOnWriteArrayList总结

如上面的分析CopyOnWriteArrayList表达的一些思想:

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

分享

Powered by Hexo and Hexo-theme-hiker

Copyright © 2018 - 2019 ZhouXu'Blog All Rights Reserved.

UV : | PV :