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;
 

由于 arrayvolatile

  • 保证可见性;
  • 保证读取到的是最新数组引用;
  • 保证引用替换具有内存语义。

因此:

多个线程同时读:

 
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 的区别

 VectorCopyOnWriteArrayList
读操作 加锁 不加锁
写操作 加锁 加锁
读写是否互斥
迭代器 fail-fast snapshot
读性能 较低 很高
写性能 一般 较差
适用场景 读写均衡 读多写少

八、总结

CopyOnWriteArrayList 的线程安全来自于两个机制:

  1. 写操作使用 ReentrantLock 保证互斥。
 
lock.lock();
 

  1. 写时复制,不修改原数组,而是创建新数组。
 
newArray = Arrays.copyOf(oldArray,...)

array = newArray
 

  1. 底层数组引用使用 volatile 修饰,保证可见性。
 
private transient volatile Object[] array;
 

因此可以概括为:

CopyOnWriteArrayList = volatile + ReentrantLock + 写时复制(snapshot)。

这也是 Java 并发包中一个非常经典的“以空间换时间、以写性能换读性能”的设计。

 
 
 
 
 
 
 

posted on 2026-07-05 15:31  日思日睿  阅读(20)  评论(0)    收藏  举报