CopyOnWriteArrayList

opyOnWriteArrayList 是一种线程安全的 List 实现,通过“写时复制”来保证线程安全性。它在读取时提供的是一致性的快照数据,而不是实时更新的数据。在写入时则会创建一个新的数组,因此,读取操作不会看到正在写入或扩容中的中间状态。

1. CopyOnWriteArrayList 的核心数据结构

  • 底层数据结构CopyOnWriteArrayList 是基于动态数组的实现,其底层维护了一个 Object[] 数组。
  • 写时复制:在写操作(如 add()set()remove() 等)时,会创建一个新数组,在新数组上进行操作,然后将新数组赋值给底层的数组引用。

2. add() 方法的实现逻辑

2.1 添加元素的步骤

  • 步骤 1:复制当前数组
    • 在添加元素时,CopyOnWriteArrayList 会首先复制当前底层数组到一个新数组中。
  • 步骤 2:在新数组上添加元素
    • 在新数组的最后一个位置添加新元素。
  • 步骤 3:将新数组替换旧数组
    • 使用原子性操作,将新的数组替换旧的底层数组,使其指向新的内存空间。

代码示例

javaCopy codepublic boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock(); // 1. 获取锁
    try {
        Object[] elements = getArray(); // 2. 获取当前数组
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1); // 3. 复制并扩容
        newElements[len] = e; // 4. 添加新元素
        setArray(newElements); // 5. 替换为新数组
        return true;
    } finally {
        lock.unlock(); // 6. 释放锁
    }
}

原理说明

  • 获取锁:在执行写操作时,会通过 ReentrantLock 加锁,确保在添加新元素时不会有其他线程进行写入。
  • 复制并扩容CopyOnWriteArrayList 的写操作会复制整个数组,因此每次写操作的开销是线性 O(n) 的。
  • 最终一致性:由于写时创建了一个新数组,因此在写操作完成前,其他线程的读取操作不会受到影响。

3. get() 方法的实现逻辑

3.1 读取元素的步骤

  • 步骤 1:读取数组引用
    • get() 操作中,CopyOnWriteArrayList 会直接读取底层数组引用,获取当前快照数据。
  • 步骤 2:返回元素
    • 根据索引直接从数组中返回元素。

代码示例

javaCopy codepublic E get(int index) {
    Object[] elements = getArray(); // 1. 获取当前数组
    return (E) elements[index]; // 2. 返回指定索引的元素
}

原理说明

  • 无锁读取get() 方法是无锁的,直接读取底层数组,因此性能非常高。
  • 读取快照:由于读取的数组是写时复制产生的快照,即使写操作正在进行,读取操作也不会被阻塞,且能获取到一致的旧数据。

4. 扩容的实现逻辑

4.1 扩容时的处理

  • 扩容在写操作时发生,例如在 add()set()remove() 操作中。CopyOnWriteArrayList 不会像 ArrayList 那样通过自动扩容实现容量增长,而是通过每次复制整个数组来扩容。

扩容和写入的详细步骤

  1. 锁定操作:写入和扩容需要获取锁,通过 ReentrantLock 实现独占锁定。
  2. 复制和扩容:写入或扩容操作时,会创建一个比当前数组更大的新数组,将现有元素复制到新数组中。
  3. 插入新元素:在新数组中执行插入或更新操作。
  4. 替换旧数组:将新数组设置为底层数组。

5. 在读取时的影响

读取操作是否会受到扩容影响?

  • 不会受到扩容影响:由于 CopyOnWriteArrayList 在写入时总是复制整个数组,且复制完成后才会将新数组替换为当前数组,读取操作始终访问的是旧数组快照数据,因此不会读取到扩容过程中的中间状态。
  • 可能读到旧数据:在写操作进行时,读取操作仍然会获取到写操作前的旧数据,写操作完成后,新的读取操作才能看到最新数据。

6. 多线程下的写时复制机制

  • CopyOnWriteArrayList 通过“写时复制”来确保线程安全。当多个线程同时进行写操作时,由于写操作需要获取锁,因此写入是互斥的。
  • 读取操作是无锁的,读取线程只会读取到旧的快照数据,这种设计确保了读取的高效性和一致性。

7. 性能分析和适用场景

性能分析

  • 读操作效率高get() 方法是无锁的,直接访问底层数组,因此读操作效率非常高。
  • 写操作性能较低:由于每次写操作都需要复制整个数组,因此写操作的时间复杂度是 O(n),在写操作频繁时性能较低。
  • 内存消耗高:由于每次写操作都会创建一个新数组,因此在写操作频繁时,会消耗较多的内存。

适用场景

  • 适用于读多写少的场景,如事件监听器列表、配置快照、缓存快照等。
  • 不适用于写操作频繁的场景,因为大量的写操作会导致性能瓶颈和内存消耗。

8. 总结

  • CopyOnWriteArrayList 在扩容或写入时,通过写时复制机制保证了线程安全,但读取操作只能获取到旧的快照数据。
  • 读取操作不会被扩容和写入操作阻塞,但无法读取到最新的数据,直到写操作完成。
  • 适合读多写少的场景,在写操作频繁的情况下可能引发性能瓶颈。
0 0 投票数
Article Rating
订阅评论
提醒
guest
0 评论
最旧
最新 最多投票
内联反馈
查看所有评论
0
希望看到您的想法,请您发表评论x