copyonwritearraylist适用于读多写少场景,1.其通过写时复制机制实现线程安全,读操作不加锁、性能高;2.写操作需加锁并复制整个数组,开销大;3.迭代器基于快照,不会抛出concurrentmodificationexception但可能读到过时数据;4.适合读远多于写、数据量小、可接受弱一致性的场景,不适用于频繁写或内存敏感环境;5.相比synchronizedlist,读并发更高,但写性能差,而concurrent集合在混合操作中更优。
CopyOnWriteArrayList是 Java 集合框架在处理并发场景下的一种策略性选择,它通过“写时复制”的机制,巧妙地解决了读操作的并发问题,特别适合那些读取操作远多于写入操作的列表型数据结构。
CopyOnWriteArrayList的核心思想,正如其名,在于“写时复制”。每当对列表进行修改操作(比如添加、删除或设置元素)时,它不会直接在原有的底层数组上进行修改,而是会先创建一个原数组的全新副本,然后在这个新副本上执行修改操作。一旦修改完成,列表内部的引用就会原子性地指向这个新创建的数组。
这样做的好处显而易见:所有的读操作都可以在不加锁的情况下进行,因为它们总是访问一个稳定不变的数组快照。这意味着读操作的并发性能极高,几乎没有竞争。然而,代价是写操作会相对昂贵,因为它涉及到整个数组的复制,对于大型列表来说,这会带来显著的内存和CPU开销。此外,写操作本身是需要加锁的,以确保在同一时刻只有一个线程进行数组复制和引用更新,从而保证数据的一致性。
CopyOnWriteArrayList的线程安全性,说到底,就是其内部实现如何巧妙地利用了不变性(immutability)和锁机制。当我们深入其源码,会发现它的写入操作,比如
add()或
set(),通常会由一个
ReentrantLock来保护。这个锁确保了在任何给定时间,只有一个线程能够执行修改操作,从而避免了多个写线程同时复制数组可能导致的混乱。
一旦获取到锁,修改的逻辑就开始了:它会获取当前的底层数组,创建一个新的、更大的(如果需要)数组副本,然后将旧数组的内容复制到新数组,并在新数组上执行添加或删除元素的操作。最后,一个关键步骤是,通过
setArray()方法,原子性地将内部指向数组的引用更新为这个新创建的数组。这个原子性更新至关重要,它保证了读线程在任何时候都能看到一个完整的、一致的数组版本,要么是旧的,要么是新的,绝不会是处于中间状态的“半成品”。
这种设计模式带来的一个独特副作用是,当你在迭代一个
CopyOnWriteArrayList时,即使有其他线程同时修改了列表,你的迭代器也不会抛出
ConcurrentModificationException。这是因为迭代器持有的,实际上是它被创建那一刻列表的一个快照。它遍历的始终是那个旧的、不变的数组,所以它对后续的修改是“无感”的。这与
ArrayList在迭代时被修改会迅速报错的行为截然不同,理解这一点对于避免潜在的逻辑错误至关重要。在我看来,这种“快照”特性既是它的强大之处,也可能是初学者容易忽视的陷阱。
选择
CopyOnWriteArrayList,绝不是一个拍脑袋的决定,它有非常明确的适用边界。
适用场景:
CopyOnWriteArrayList的高性能读特性会让你受益匪浅。想象一下一个事件监听器列表,注册(写)的频率远低于事件触发(读)的频率,它就非常合适。
潜在陷阱:
Collections.synchronizedList。
ConcurrentModificationException是一个特性,但如果开发者期望迭代器能反映实时变化,那么这种“快照”行为就成了问题。你可能遍历完了一个旧版本,而新版本的数据已经生效了。
CopyOnWriteArrayList保证了列表结构的线程安全,元素内部状态的改变仍然需要额外的同步措施。它只保证了对列表本身的结构性修改是线程安全的,不保证元素内容的线程安全。
Java 集合框架提供了多种并发工具,
CopyOnWriteArrayList只是其中之一。理解它们之间的差异,是做出正确选择的关键。
Collections.synchronizedList(new ArrayList<>())
:
这是最基础的同步列表,它通过在每个方法上加锁来实现线程安全。这意味着无论是读还是写,任何时候只有一个线程能访问列表。在并发度高的情况下,它的性能会非常糟糕,因为锁粒度太大,所有操作都会相互阻塞。它会抛出
ConcurrentModificationException。在我看来,除非是极其简单的、并发度极低的场景,或者你对性能要求不高,否则很少会优先选择它。
ConcurrentHashMap
/ ConcurrentLinkedQueue
/ ConcurrentSkipListSet
:
这些是Java并发包(
java.util.concurrent)中更高级的并发集合,它们通常采用更精细的锁机制(如分段锁、CAS操作)或无锁算法来达到更高的并发性能。
ConcurrentHashMap适用于并发的键值对存储,它通过分段锁或者JDK8之后的CAS+synchronized来提供极高的并发性能。
ConcurrentLinkedQueue是一个无界、线程安全的队列,基于链表实现,通过CAS操作实现无锁并发。
ConcurrentSkipListSet和
ConcurrentSkipListMap提供了并发的有序集合和映射,基于跳表实现,也通过CAS操作实现高度并发。 这些集合通常在混合读写或写操作频繁的场景下表现更优,因为它们避免了
CopyOnWriteArrayList那样的全数组复制开销。它们的迭代器也是“弱一致性”的,不会抛出
ConcurrentModificationException,但其内部实现与
CopyOnWriteArrayList
的“快照”机制不同。选择依据:
当你面临并发列表的选择时,问自己几个问题:
CopyOnWriteArrayList可能是个不错的选择。否则,考虑其他
Concurrent集合。
CopyOnWriteArrayList的内存和CPU开销会成为瓶颈。
CopyOnWriteArrayList的“弱一致性”可能不符合要求。如果允许看到稍微旧一点的数据,则可以接受。
ConcurrentHashMap、
ConcurrentLinkedQueue等会是更自然、更高效的选择。
总的来说,
CopyOnWriteArrayList是一个非常专业的工具,它在特定场景下能提供卓越的读性能,但并非万能。理解其内部机制和局限性,是正确利用它的前提。在多数通用并发场景下,
java.util.concurrent包中的其他集合往往能提供更好的综合性能和更灵活的并发控制。