copyonwritearraylist的核心原理是写时复制。当进行写操作时,它会复制原数组并修改副本,再用原子操作替换原引用,保证读写不冲突。读操作直接访问当前数组且无锁,性能高。其步骤为:1.获取reentrantlock锁;2.复制内部数组;3.在新数组上执行修改;4.替换引用;5.释放锁。该设计适合读多写少场景,但写操作存在内存和性能瓶颈,如频繁gc、o(n)时间复杂度及弱一致性问题。
Java并发容器CopyOnWriteArrayList的核心原理,简单来说,就是“写时复制”。当对列表进行修改操作(如添加、删除、修改元素)时,它不会直接在原数组上进行,而是先将当前数组复制一份,然后在新复制的数组上进行修改,最后再将这个新数组替换掉旧数组。读操作则直接在当前数组上进行,无需加锁,因此读操作的性能非常高。
解决方案
CopyOnWriteArrayList的精髓在于其独特的并发控制策略。它并没有采用传统意义上的锁机制来保护读写操作的互斥,而是利用了jvm的内存模型和对象的不可变性思想。当一个线程需要修改列表内容时,它会创建一个内部数组的副本。所有的修改操作都在这个副本上进行,修改完成后,通过一个原子操作(通常是volatile关键字或者CAS操作)将内部引用指向这个新的数组。
这意味着在写操作进行时,旧的数组仍然存在,并且可以被正在进行的读操作访问,这样就避免了读写之间的锁竞争。只有当新的数组替换旧数组后,后续的读操作才会看到最新的数据。这种设计哲学在读多写少的并发场景下表现出色,因为它几乎消除了读操作的同步开销。
立即学习“Java免费学习笔记(深入)”;
CopyOnWriteArrayList的写操作是如何进行的?
理解CopyOnWriteArrayList的写操作,是掌握其原理的关键。每当调用add(), set(), remove()等方法时,事情并不是我们想象的那样直接在原地修改。
拿add()方法举例,当你尝试向列表中添加一个新元素时:
- 获取锁: 尽管读操作是无锁的,但写操作依然需要一个内部的ReentrantLock来保证同一时间只有一个线程能进行写操作。这是为了避免多个写操作同时复制数组,导致数据不一致或者内存浪费。
- 复制数组: 获得锁后,它会获取当前列表的内部数组,并创建一个这个数组的全新副本。这个副本的长度通常会比原数组大1(对于添加操作)。
- 在新数组上修改: 新元素被添加到这个新创建的数组副本中。
- 替换引用: 使用一个原子操作(如Unsafe.compareAndSwapObject或简单的volatile变量赋值,取决于具体实现和JVM优化)将CopyOnWriteArrayList内部指向数组的引用更新为这个新数组。
- 释放锁: 写操作完成后,释放锁。
这个过程确保了在写操作进行时,任何读操作看到的都是旧的、完整的数组,不会出现部分修改导致的数据混乱。一旦新数组的引用被替换,后续的读操作自然就会看到最新的数据。这种“先复制,再修改,后替换”的模式,有效地将并发写操作串行化,同时保证了读操作的并行性。
CopyOnWriteArrayList的读写性能如何?
CopyOnWriteArrayList的性能特性,可以说是一把双刃剑,它的设计目标决定了其在特定场景下的优异表现,但也带来了明显的局限性。
读操作性能: 读操作是CopyOnWriteArrayList的强项。由于读操作直接访问内部数组,并且在读的过程中不会有任何锁的竞争或同步开销,它的性能非常高,几乎可以达到与普通ArrayList在单线程环境下的读性能相当。这使得它在“读多写少”的场景中表现卓越,比如配置信息的列表、事件监听器列表等,这些数据通常被频繁读取但很少修改。
写操作性能: 写操作是CopyOnWriteArrayList的弱点。每次修改操作(无论是添加、删除还是设置)都需要复制整个内部数组。这意味着:
- 内存开销: 每次写操作都会创建一个新的数组副本,如果列表非常大,这会带来显著的内存消耗,尤其是在短时间内有大量写操作发生时,可能会导致频繁的GC。
- CPU开销: 数组复制本身就是一个O(N)的操作,N是列表的当前大小。对于大型列表,复制操作会消耗大量的CPU时间,从而降低写操作的吞吐量。
- 锁竞争: 尽管读操作是无锁的,但写操作之间仍然需要获取内部锁来保证修改的原子性。这意味着多个写线程仍然会互相阻塞,虽然这种阻塞是为了保证数据一致性,但确实限制了写操作的并发度。
因此,如果你的应用场景是写操作非常频繁,或者列表中的元素数量巨大,那么CopyOnWriteArrayList可能不是一个好的选择,它的性能瓶颈会很快显现出来。
使用CopyOnWriteArrayList有哪些潜在问题或陷阱?
尽管CopyOnWriteArrayList在某些特定场景下非常有用,但它并非银弹,在使用时需要注意一些潜在的问题和陷阱:
-
内存消耗: 这是最直接的问题。每次写操作都会复制整个底层数组。如果列表包含大量元素,并且写操作频繁,那么内存消耗将非常显著。旧的数组在不再被引用后会被垃圾回收,但在此之前,可能会有多个版本的数组同时存在于内存中,这可能导致内存占用飙升,甚至引发频繁的Full GC,影响系统性能。
-
数据一致性(弱一致性): CopyOnWriteArrayList提供的是“最终一致性”或“弱一致性”。这意味着当你遍历CopyOnWriteArrayList时,你看到的是创建迭代器那一刻的数组快照。如果在迭代过程中有其他线程修改了列表,你当前的迭代器是看不到这些修改的。它会继续遍历旧的数据。这在某些场景下可能不是问题,但在需要强实时性或精确一致性的场景下,就可能导致逻辑错误。例如,你可能遍历到一个已经被删除的元素,或者错过一个新添加的元素。
-
写操作性能瓶颈: 如前所述,写操作的O(N)时间复杂度使得它在大数据量或高并发写入场景下性能很差。如果你需要一个写操作也很快的并发列表,那么CopyOnWriteArrayList显然不是最佳选择。
-
不适用于所有集合操作: CopyOnWriteArrayList主要针对List接口的实现。对于Set或map的需求,你需要考虑ConcurrentHashSet(通常基于ConcurrentHashMap实现)或ConcurrentHashMap。
总而言之,CopyOnWriteArrayList是一个针对特定并发模式(读多写少)的优化方案。在使用前,务必深入分析你的应用场景,权衡其带来的便利性与潜在的内存和性能开销。如果写操作的频率相对较高,或者你需要更强的数据一致性保证,那么像Collections.synchronizedList(虽然性能一般)或者自己基于ReentrantReadWriteLock实现一个并发列表,可能会是更合适的选择。