CopyOnWriteArrayList
CopyOnWriteArrayList(写时复制数组)之所以是线程安全的,核心原因就是:
读操作不加锁,写操作加锁并复制底层数组。
一、先看它的底层结构
CopyOnWriteArrayList 内部维护了一个 volatile 修饰的数组:
private transient volatile Object[] array;
例如:
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("A");
list.add("B");
内部实际上是:
array = ["A", "B"];
二、为什么读是线程安全的?
读取时:
list.get(index);
list.contains(obj);
for(String s : list)
源码:
public E get(int index) {
return get(getArray(), index);
}
final Object[] getArray() {
return array;
}
只是获取:
volatile Object[] array
并不会修改数组。
volatile 保证什么?
假设:
线程A:
array = ["A", "B", "C"];
线程B:
Object[] arr = array;
由于 array 是 volatile:
- 保证可见性;
- 保证读取到的是最新数组引用;
- 保证引用替换具有内存语义。
因此:
多个线程同时读:
get()
contains()
size()
iterator()
不会有问题。
三、为什么写是线程安全的?
例如:
list.add("D");
源码(简化):
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);
} finally {
lock.unlock();
}
流程:
假设原数组:
["A","B","C"]
线程A执行:
add("D")
过程:
第一步:加锁
lock.lock();
保证同时只有一个写线程。
第二步:复制旧数组
newElements =
Arrays.copyOf(oldArray,len+1);
得到:
["A","B","C",null]
第三步:修改新数组
["A","B","C","D"]
第四步:替换引用
array = newElements;
变成:
oldArray ----------> ["A","B","C"]
newArray ----------> ["A","B","C","D"]
array -----------> newArray
注意:
整个过程中:
旧数组从来没有被修改。
因此:
正在读旧数组的线程完全不受影响。
四、为什么 iterator 不会 ConcurrentModificationException?
普通 ArrayList:
for(String s : list){
list.add("A");
}
会抛:
ConcurrentModificationException
因为:
modCount
发生变化。
CopyOnWriteArrayList:
Iterator<E> iterator() {
return new COWIterator(getArray(), 0);
}
迭代器拿到的是:
快照(snapshot)
例如:
创建迭代器时:
["A","B","C"]
随后:
list.add("D");
内部变成:
["A","B","C","D"]
但迭代器仍然遍历:
["A","B","C"]
因此:
不会抛 ConcurrentModificationException
但:
不会看到最新数据。
这是典型的:
弱一致性(Weak Consistency)
五、为什么叫 Copy-On-Write?
因为:
不是直接修改原数组:
原数组:
[A,B,C]
而是:
复制:
[A,B,C]
↓
[A,B,C,null]
↓
[A,B,C,D]
最后:
array = 新数组
即:
只有写的时候才复制。
六、它适用于什么场景?
适合:
读远远大于写
例如:
读:99999次
写:1次
配置中心:
List<Listener>
监听器列表:
List<EventHandler>
黑名单缓存:
List<String>
Spring 中也大量使用类似思想。
不适合:
写很多
add()
remove()
set()
因为每次写:
O(n)复制数组
例如:
100万个元素:
add()
需要:
复制100万个元素
代价非常高。
七、与 Vector 的区别
| Vector | CopyOnWriteArrayList | |
|---|---|---|
| 读操作 | 加锁 | 不加锁 |
| 写操作 | 加锁 | 加锁 |
| 读写是否互斥 | 是 | 否 |
| 迭代器 | fail-fast | snapshot |
| 读性能 | 较低 | 很高 |
| 写性能 | 一般 | 较差 |
| 适用场景 | 读写均衡 | 读多写少 |
八、总结
CopyOnWriteArrayList 的线程安全来自于两个机制:
- 写操作使用
ReentrantLock保证互斥。
lock.lock();
- 写时复制,不修改原数组,而是创建新数组。
newArray = Arrays.copyOf(oldArray,...)
array = newArray
- 底层数组引用使用
volatile修饰,保证可见性。
private transient volatile Object[] array;
因此可以概括为:
CopyOnWriteArrayList = volatile + ReentrantLock + 写时复制(snapshot)。
这也是 Java 并发包中一个非常经典的“以空间换时间、以写性能换读性能”的设计。
浙公网安备 33010602011771号