CopyOnWriteArrayList 是一个线程安全的 ArrayList 变体,它通过以下机制确保线程安全:

  1. 写时复制(Copy-on-Write)机制:当列表被修改时(例如添加、删除、设置元素值等操作),CopyOnWriteArrayList 首先会将当前底层数组复制一份,然后在这个副本上进行修改。完成修改后,它再将原来的数组引用指向新修改过的副本。这样,读操作总是在不变的数组版本上进行,从而避免了并发读写冲突。

  2. 内部锁定:所有的写入操作(增加、移除、更新等操作)都是通过一个内部的重入锁(ReentrantLock)来同步的,以此来保证同时只有一个线程可以对列表内容进行修改。
  3. 迭代器的弱一致性CopyOnWriteArrayList 的迭代器支持弱一致性,意味着迭代器遍历列表时基于数组的一个快照,因此即使后续有修改,也不会抛出ConcurrentModificationException异常。迭代器看到的内容是创建迭代器时列表的状态,并不反映之后的修改。

详细写入实现逻辑请参考如下源代码:

    public boolean add(E e) {        synchronized (lock) {            Object[] elements = getArray();            int len = elements.length;            Object[] newElements = Arrays.copyOf(elements, len + 1);            newElements[len] = e;            setArray(newElements);            return true;        }    }
从源码很容易看出CopyOnWriteArrayList 很不适合处理大量写操作的场景,主要原因包括:
  1. 高成本的写操作:每次修改都需要复制整个底层数组,如果数组大小很大或者写操作很频繁,这将导致高额的内存占用和CPU资源消耗,并且每次写操作都需要加锁。

  2. 内存占用:为了创建数组的副本,系统需要额外的内存空间;在某些情况下,这可能导致内存溢出错误(如OutOfMemoryError)。
  3. 垃圾回收压力:由于频繁复制数组会产生大量不再使用的数组对象,这增加了垃圾收集器的工作负担,可能导致更频繁的垃圾收集事件。
因为CopyOnWriteArrayList读取不需要加锁所以在高并发环境中它读取操作的性能远远超过修改操作所以它适用于“读多写少”的场景,例如:
  • 事件监听器列表:通常注册监听器的操作不如触发事件频繁。
  • 共享配置数据:配置数据在初始化时被加载,之后主要进行读取而很少修改。
  • 发布/订阅模式中的订阅者列表:当消息相对少量更新时,读取操作(分发消息给订阅者)将比更新订阅者列表更频繁。
总结选择适当的数据结构应基于具体的应用场景和性能需求。在需要频繁写操作的场合,可能需要使用其他并发集合,如 ConcurrentHashMap 或 ConcurrentLinkedQueue,这些数据结构设计有更好的并发修改性能。

本篇文章来源于微信公众号: 互联网面试小帮手



微信扫描下方的二维码阅读本文

此作者没有提供个人介绍
最后更新于 2024-05-24