[面试题] CopyOnWriteArrayList 的底层原理是什么?为什么它适合读多写少?
📖 版块 A:知识讲解版 (温故知新)
-
定义:
CopyOnWriteArrayList是 JUC 提供的线程安全 List,实现思想是 写时复制。 -
核心原理:
- 读操作直接读取内部数组,不加锁。
- 写操作时先加锁。
- 复制出一份新数组,在新数组上完成修改。
- 最后把内部引用指向新数组。
-
优点:
- 读操作几乎无锁,性能很好。
- 读线程不会被写线程阻塞。
- 迭代时基于快照,不会抛
ConcurrentModificationException。
-
缺点:
- 写操作开销大,需要复制整个数组。
- 占用额外内存。
- 读到的是快照,可能不是最新实时数据。
-
适用场景:
- 配置项列表。
- 监听器列表。
- 黑白名单缓存。
- 读远远多于写的共享集合。
🎤 版块 B:面试实战版 (高手逐字稿)
Q:CopyOnWriteArrayList 的底层原理是什么?为什么它适合读多写少?
💬 高手逐字稿:
CopyOnWriteArrayList 的核心思想其实都写在名字里了,就是“写的时候复制一份”。它并没有像普通线程安全容器那样,把所有读写操作都围在一把大锁里,而是做了一个很激进的取舍:读尽量不加锁,写的时候我来承担全部代价。
具体做法是这样的。内部真实数据是一个数组。读操作直接读取当前数组引用,不需要加锁;而写操作,比如 add、set、remove,会先拿锁,然后复制出一份新数组,在新数组上完成修改,最后再把内部数组引用一次性切换过去。这样读线程始终读的是某个稳定数组,不会读到一半结构变化。
它为什么适合读多写少?因为它把成本几乎都压到了写操作上。一次写可能要复制整份数组,元素越多,成本越高;但读操作非常轻量,多个线程并发读时基本没有锁竞争。所以如果你的场景是“偶尔更新,频繁读取”,比如监听器列表、配置白名单、事件订阅者集合,这种模型就很合适。
它还有一个很典型的特征,就是快照语义。迭代器拿到的是创建那一刻的数组快照,后续即使有线程在修改集合,这个迭代器也不会抛 ConcurrentModificationException。但反过来说,它读到的也可能不是最新数据,所以它并不适合那种对实时一致性要求很高的业务列表。
所以我的理解是:CopyOnWriteArrayList 不是“通用线程安全 List”,而是一个非常典型的“用空间和写成本换读性能”的特化容器。场景对了很好用,场景不对就会很浪费。
最后更新于: