CopyOnWrite*并发集合的优缺点是什么?适用于哪些场景?
参考回答
CopyOnWrite
是 Java 提供的一种线程安全的并发集合机制,主要包括 CopyOnWriteArrayList
和 CopyOnWriteArraySet
。其核心原理是在执行写操作时,复制底层数组,对新数组进行修改,然后将引用指向新的数组。因此,它非常适合读操作远多于写操作的场景。
一、CopyOnWrite
并发集合的优缺点
优点:
- 线程安全:
- 读操作不需要加锁,因为
CopyOnWrite
采用的是写时复制的策略。读操作直接访问底层数组,天然是线程安全的,避免了锁竞争。
- 读操作不需要加锁,因为
- 读写分离:
- 读操作和写操作使用不同的数组,因此读操作不会被写操作阻塞,大大提高了读操作的性能。
- 无锁的读操作:
- 由于读操作直接基于快照数组,因此不需要任何同步操作,非常适合读多写少的场景。
- 线程间一致性:
- 读操作始终访问的是修改前的快照,写操作完成后其他线程才会看到新的数据。这样可以保证线程之间的数据一致性。
缺点:
- 写操作性能较低:
- 每次写操作都会复制整个底层数组,这会带来较大的时间和空间开销。因此,写操作非常昂贵,不适合写操作频繁的场景。
- 内存占用高:
- 写操作需要复制整个数组,意味着在写操作期间,内存中会同时存在两个数组。如果数组较大,内存开销会非常明显。
- 无法实时看到最新数据:
- 由于读操作访问的是修改前的快照,某些情况下可能导致读操作无法实时看到写操作的最新结果。
- 不适合实时性要求高的场景:
- 因为写操作是基于复制后的新数组,读操作只能看到旧数据,因此在需要实时性强的场景中表现不佳。
二、CopyOnWrite
并发集合的适用场景
适用场景:
- 读多写少的场景:
- 例如,配置数据加载、黑名单/白名单校验、缓存场景等,读操作频繁,写操作相对较少。
- 读操作占主导、写操作不频繁:
- 比如:系统中有大量的读请求,而写请求相对较少,且写操作的延迟对系统性能影响不大。
- 数据较小的场景:
CopyOnWrite
的写操作会复制整个数组,因此适合数据规模较小的场景。如果数据量较大,复制操作的开销会非常高。
- 多线程环境中需要避免读写冲突:
- 比如:用户访问菜单配置,系统需要动态修改菜单权限列表时,可以使用
CopyOnWriteArrayList
。
- 比如:用户访问菜单配置,系统需要动态修改菜单权限列表时,可以使用
不适用场景:
- 写操作频繁的场景:
- 如果写操作频繁(例如:高并发写入日志、实时更新系统状态等),
CopyOnWrite
的写操作开销太大,性能会急剧下降。
- 如果写操作频繁(例如:高并发写入日志、实时更新系统状态等),
- 对实时性要求高的场景:
- 例如:股票交易系统、实时消息处理系统等需要最新数据的场景。
- 大数据量存储的场景:
- 当底层数组的数据量较大时,复制数组的内存和时间开销会非常高。
三、示例代码
示例 1:读多写少场景
假设一个系统需要动态更新黑名单,但大部分情况下是查询黑名单。
import java.util.concurrent.CopyOnWriteArrayList;
public class CopyOnWriteExample {
public static void main(String[] args) {
CopyOnWriteArrayList<String> blackList = new CopyOnWriteArrayList<>();
// 初始化黑名单
blackList.add("192.168.0.1");
blackList.add("192.168.0.2");
// 多线程读取黑名单
Runnable readTask = () -> {
for (String ip : blackList) {
System.out.println(Thread.currentThread().getName() + " - Read: " + ip);
}
};
// 动态更新黑名单
Runnable writeTask = () -> {
blackList.add("192.168.0.3");
System.out.println(Thread.currentThread().getName() + " - Added new IP");
};
// 启动线程
Thread thread1 = new Thread(readTask);
Thread thread2 = new Thread(writeTask);
thread1.start();
thread2.start();
}
}
输出(示例):
Thread-0 - Read: 192.168.0.1
Thread-0 - Read: 192.168.0.2
Thread-1 - Added new IP
Thread-0 - Read: 192.168.0.3
示例 2:并发下的读写分离
import java.util.concurrent.CopyOnWriteArrayList;
public class CopyOnWriteListExample {
public static void main(String[] args) {
CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
list.add(1);
list.add(2);
list.add(3);
// 启动一个线程读取数据
new Thread(() -> {
for (Integer value : list) {
System.out.println("Read: " + value);
try {
Thread.sleep(100); // 模拟读取延迟
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
// 启动另一个线程写入数据
new Thread(() -> {
list.add(4);
System.out.println("Added 4");
}).start();
}
}
输出:
Read: 1
Read: 2
Read: 3
Added 4
说明:
- 写线程在写入数据时,读线程仍然可以读取旧的快照数据,不会被阻塞。
四、总结对比
优点 | 缺点 |
---|---|
线程安全,读操作不需要加锁 | 写操作性能较低(需要复制数组) |
读写分离,读操作不会被写操作阻塞 | 内存占用较高,尤其是写操作时的内存消耗 |
数据一致性高,读操作始终访问快照 | 对实时性要求高的场景不适用 |
适合读多写少的场景 | 不适合写操作频繁或大数据量存储的场景 |
五、CopyOnWrite
适合的场景
- 缓存数据:如缓存系统中少量配置数据的动态更新。
- 黑名单/白名单:如防火墙中的 IP 黑名单或敏感词过滤列表。
- 读多写少:如应用中菜单、按钮配置动态更新的场景。