一文搞懂Java的CopyOnWriteArrayList:从原理到实战,连面试官都直呼内行!
引言:当ArrayList有了“分身术”,多线程再也不怕翻车了!
在Java的并发编程江湖中,ArrayList因为“线程不安全”的标签被各路高手嫌弃,而Vector又因“全副武装的锁”导致性能拉胯。这时,一位名叫 CopyOnWriteArrayList(COW) 的侠客横空出世,凭借“写时复制”的独门绝技,在“读多写少”的江湖场景中一战封神!今天我们就来扒一扒这位侠客的底裤(划掉)……底层原理!
一、CopyOnWriteArrayList是什么?
1.1 一句话定义
COW是Java并发包(JUC)中的线程安全List,通过“写时复制”策略实现读写分离,读操作无锁,写操作加锁复制新数组,堪称“读多写少”场景的救星。
1.2 核心特点
- 读操作不锁:随便读,反正你读的是原数组的“快照”;
- 写操作复制:每次修改都像搬家一样,先复制整个房子(数组),在新家装修(修改),最后把门牌号(引用)换掉;
- 迭代器安全:遍历时用的是“历史快照”,不怕中途被修改。
二、用法与案例:黑名单系统的正确打开方式
2.1 基础操作
// 初始化
CopyOnWriteArrayList<String> blacklist = new CopyOnWriteArrayList<>();
// 添加元素(线程安全)
blacklist.add("hacker1");
// 读取元素(无锁,高性能)
System.out.println(blacklist.get(0)); // 输出:hacker1
// 遍历(不会抛ConcurrentModificationException)
Iterator<String> it = blacklist.iterator();
while (it.hasNext()) {
System.out.println(it.next());
// it.remove(); // 报错!迭代器不支持修改操作
}
2.2 实战案例:黑名单系统
假设你运营一个社交平台,每天有1万次用户搜索请求,但黑名单每晚更新一次。用COW实现:
// 初始化黑名单
CopyOnWriteArrayList<String> blacklist = loadBlacklistFromDB();
// 用户搜索时检查(高频读操作,无锁)
public boolean isBlocked(String keyword) {
return blacklist.contains(keyword);
}
// 定时更新黑名单(低频写操作)
public void updateBlacklist(List<String> newList) {
blacklist.clear();
blacklist.addAll(newList);
}
优势:白天用户疯狂搜索时,读操作丝滑无阻;夜间更新时,短暂的写锁对用户体验无感。
三、原理揭秘:写时复制的“影分身术”
3.1 核心机制
- 写操作流程(以
add为例):- 加锁:用
ReentrantLock保证同一时间只有一个线程能写; - 复制数组:把原数组完整拷贝到新数组(长度+1);
- 修改新数组:在新数组末尾添加元素;
- 替换引用:将
volatile修饰的数组指针指向新数组。
- 加锁:用
// add方法源码(简化版)
public boolean add(E e) {
lock.lock();
try {
Object[] newArray = Arrays.copyOf(oldArray, oldArray.length + 1);
newArray[newArray.length - 1] = e;
setArray(newArray); // volatile写保证可见性
} finally {
lock.unlock();
}
}
- 读操作:直接访问
volatile数组,无需锁,性能炸裂。
3.2 迭代器的秘密
迭代器内部保存的是创建时的数组快照,即使其他线程修改了数据,迭代器看到的依然是“历史版本”,因此不会抛ConcurrentModificationException。
四、对比:COW vs Vector vs SynchronizedList
| 特性 | CopyOnWriteArrayList | Vector | SynchronizedList |
|---|---|---|---|
| 锁粒度 | 写操作加锁,读无锁 | 全方法synchronized | 同步块(部分方法优化) |
| 并发性能 | 读性能极高,写性能差 | 读写均差 | 读性能一般,写性能差 |
| 内存占用 | 写操作内存翻倍 | 低 | 低 |
| 数据一致性 | 最终一致性 | 强一致性 | 强一致性 |
| 适用场景 | 读多写少(如黑名单、缓存) | 已过时,不推荐 | 写少且需强一致性 |
总结:COW是“读霸”,Vector是“古董”,SynchronizedList是“折中派”。
五、避坑指南:COW的“七寸”在哪里?
- 内存炸弹:频繁写操作会导致大量数组拷贝,引发GC风暴。切忌在数据量大的高频写场景使用。
- 数据延迟:读到的可能是旧数据,不适合实时交易系统。比如“秒杀库存”场景会翻车。
- 不支持排序:直接调用
Collections.sort()会抛异常!正确做法是先转成普通List排序,再替换。List<String> temp = new ArrayList<>(copyOnWriteList); Collections.sort(temp); copyOnWriteList.clear(); copyOnWriteList.addAll(temp);
六、最佳实践:什么时候该请COW出山?
-
✅ 适用场景:
- 读操作占比超过90%(如配置项、缓存);
- 数据量较小(避免复制大数组);
- 允许短暂数据不一致(最终一致性)。
-
❌ 不适用场景:
- 高频写操作(如股票实时报价);
- 大数据量(内存扛不住);
- 强一致性要求(如银行余额)。
七、面试考点:COW的“灵魂拷问”
7.1 高频问题
-
COW的读写流程是怎样的?
(答:写时复制+锁,读无锁,引用volatile数组) -
为什么迭代器遍历时不会抛ConcurrentModificationException?
(答:迭代器用的是创建时的数组快照) -
COW和读写锁的区别?
(答:COW读完全无锁,读写锁读仍需竞争锁)
7.2 陷阱题
- “COW的size()方法实时准确吗?”
(答:不!因为size()返回的是当前数组长度,可能已被其他线程修改)
八、总结:COW的“武功秘籍”
优点:读操作快到飞起,迭代器安全,代码简洁;
缺点:写操作慢如蜗牛,内存消耗大,数据延迟;
精髓:读多写少用COW,写多读少快绕道!
彩蛋:COW的哲学启示
人生何尝不是一场“写时复制”?—— 想要改变现状,就得先拷贝一份自我,在新副本上努力升级,最后让世界看到全新的你!(当然,锁还是要加的,毕竟同一时间只能专心做一件事……)