图解CopyOnWriteArrayList数据结构设计与应用案例

185 阅读5分钟

image.png

CopyOnWriteArrayList 是 Java 中的一个线程安全变体的 ArrayList,它用于读多写少的场景。在 CopyOnWriteArrayList 中,所有的修改操作(如添加、删除元素)都会在数组的副本上进行,修改完成后再将原数组引用指向新的副本。这种方法可以减少锁的使用,提高读操作的性能,因为在读取时不需要加锁。由于写操作需要复制数组,所以当列表元素较多或写操作频繁时,可能会影响性能。CopyOnWriteArrayList 适用于迭代操作远多于修改操作的场景,如并发缓存、实时查询结果的存储等。

1、CopyOnWriteArrayList

CopyOnWriteArrayList 在 Java 中是一个线程安全的变体数组列表,其特点是在修改(写操作)时通过复制整个底层数组来实现,以此保证读操作的线程安全和高性能。以下是 CopyOnWriteArrayList 的设计:

设计思考:

  1. 需求场景
    • 在多线程环境中,读操作远比写操作频繁,且对数据的实时性要求不是非常高的场景。例如,缓存系统、实时数据的订阅发布模型等。
  2. 现有技术局限性
    • 传统的线程安全实现,如 Vector 或通过 synchronized 同步代码块或方法,可能会因为写操作导致的线程阻塞,严重影响并发性能。
  3. 技术融合
    • CopyOnWriteArrayList 采用了写时复制(Copy-On-Write)的策略,当进行写操作(添加、删除等)时,先复制整个数组,然后在新数组上进行操作,而读操作则直接作用于原数组,从而提高了读操作的性能。
  4. 设计理念
    • 利用了读操作远多于写操作的特性,通过分离读和写操作,使得读操作无需加锁,从而提高了并发读的性能。
  5. 实现方式
    • 内部使用一个数组来存储元素,所有写操作都会创建一个新的数组,并将修改应用于新数组,然后原子性地将内部数组引用指向新数组。

2、 数据结构

image.png

图说明:
  • CopyOnWriteArrayList:
    • 表示 CopyOnWriteArrayList 的实例。
  • Object[] array:
    • CopyOnWriteArrayList 内部使用的一个数组 array 来存储元素。这是原始数组,所有读操作都访问这个数组。
  • Object[] newArray:
    • 写操作时创建的新数组。当写操作发生时,这个数组是原始数组的一个深拷贝。
  • 写操作:
    • 包括添加、删除或修改元素。写操作不是在原始数组上进行,而是在新数组上进行。
工作原理:
  1. 读操作:
    • 多个读线程可以同时访问和遍历 array,因为数组是不可变的。
  2. 写操作:
    • 当写操作发生时(如添加、删除或修改元素),写线程首先会创建原始 array 的一个副本 newArray
    • 写线程在 newArray 上进行添加、删除或修改操作。
    • 写操作完成后,写线程会原子性地将 CopyOnWriteArrayList 的内部数组引用指向 newArray
  3. 数据一致性:
    • 在写操作进行时,读线程仍然可以访问旧的内部数组 array,从而保证了数据的一致性。

3、 执行流程

image.png

图说明:
  • 初始化 CopyOnWriteArrayList:
    • 创建一个空的 CopyOnWriteArrayList 实例。
  • 内部数组 array:
    • CopyOnWriteArrayList 内部使用一个数组来存储元素。
  • 读操作:
    • 直接读取内部数组的元素,是线程安全的,因为内部数组不可变。
  • 写操作:
    • 包括添加、删除和修改元素,需要创建内部数组的一个新副本。
  • 复制数组:
    • 在执行写操作前,复制内部数组,以保证新元素的添加不会影响读操作。
  • 添加元素:
    • 向 CopyOnWriteArrayList 添加新元素。
  • 删除元素:
    • 从 CopyOnWriteArrayList 删除元素。
  • 修改元素:
    • 修改 CopyOnWriteArrayList 中的元素。
  • 数组拷贝:
    • 创建内部数组的一个新副本,并在新副本上执行写操作。

4、优点:

  1. 读操作性能高
    • 读操作不需要加锁,可以由多个线程并发进行。
  2. 线程安全
    • 写操作通过复制整个数组来保证线程安全。
  3. 写操作不阻塞读操作
    • 写操作不会影响读操作的执行,因为读操作作用于原数组。

5、缺点:

  1. 写操作性能开销
    • 写操作需要复制整个数组,对于大数据量的数组,性能开销较大。
  2. 内存消耗
    • 写操作时会创建新的数组,可能会导致内存消耗较高。
  3. 数据实时性
    • 读操作可能读取到的不是最新数据,因为写操作完成后,数据的变更才会被复制到新数组。

6、使用场景:

  • 适用于读多写少的场景,如配置信息的存储、统计信息的收集等。

7、类设计

image.png

8、应用案例

CopyOnWriteArrayList 通常用于实现线程安全的动态数组,特别是在读操作远多于写操作的情况下。这是一个简单的实时消息系统,用于管理在线用户列表:

import java.util.concurrent.CopyOnWriteArrayList;
import java.util.List;

// 用户类,用于表示系统中的用户
class User {
    private String username;
    private String status;

    public User(String username, String status) {
        this.username = username;
        this.status = status;
    }

    public String getUsername() {
        return username;
    }

    public String getStatus() {
        return status;
    }

    @Override
    public String toString() {
        return username + " (" + status + ")";
    }
}

// 消息系统类,用于管理在线用户列表
class MessagingSystem {
    private List<User> onlineUsers;

    public MessagingSystem() {
        onlineUsers = new CopyOnWriteArrayList<>();
    }

    // 添加用户到在线列表
    public void addUser(User user) {
        onlineUsers.add(user);
        System.out.println("User added: " + user);
    }

    // 移除用户从在线列表
    public void removeUser(User user) {
        onlineUsers.remove(user);
        System.out.println("User removed: " + user);
    }

    // 获取当前所有在线用户
    public List<User> getOnlineUsers() {
        return onlineUsers;
    }
}

public class Main {
    public static void main(String[] args) {
        MessagingSystem messagingSystem = new MessagingSystem();

        // 模拟添加用户到系统
        messagingSystem.addUser(new User("Alice", "online"));
        messagingSystem.addUser(new User("Bob", "online"));
        messagingSystem.addUser(new User("Charlie", "online"));

        // 获取并打印所有在线用户
        List<User> onlineUsers = messagingSystem.getOnlineUsers();
        for (User user : onlineUsers) {
            System.out.println("Online user: " + user);
        }

        // 模拟移除用户
        messagingSystem.removeUser(new User("Bob", "online"));
    }
}