读者说:我们100个人同时读,互不干扰!😎
写者说:我一个人写,复制整个数组!😅
适用场景:读多写少,否则内存爆炸!💥
一、开场:并发List的痛点😫
问题1:ArrayList线程不安全
java
List<String> list = new ArrayList<>();
// 多线程并发add
for (int i = 0; i < 100; i++) {
new Thread(() -> {
list.add("item"); // ❌ 可能抛出ConcurrentModificationException
}).start();
}
问题:
- 数组扩容时并发修改
- size计数不准确
- 数据丢失或覆盖
问题2:Collections.synchronizedList性能差
java
List<String> list = Collections.synchronizedList(new ArrayList<>());
// 读操作也要加锁!
list.get(0); // synchronized,慢!
问题:
- 所有操作都加锁
- 读写互斥
- 迭代需要手动加锁
java
synchronized (list) {
for (String item : list) {
// 必须在synchronized块内迭代
}
}
二、CopyOnWriteArrayList:读写分离的智慧💡
核心思想
写时复制(Copy-On-Write):
css
读操作:直接读原数组,无锁!
写操作:复制整个数组,修改副本,替换引用
[原数组] → 读线程1、2、3直接读
↓
写线程
↓
[新数组] ← 复制、修改、替换
生活类比:
传统List像共享笔记本📔:
- 100个人同时看一本笔记
- 有人要写,所有人都得停下来等
CopyOnWrite像复印笔记📄:
- 100个人看原版笔记
- 要修改?复印一份,改完了换新版
- 看旧版的人不受影响
三、源码剖析:CopyOnWriteArrayList如何工作🔍
核心数据结构
java
public class CopyOnWriteArrayList<E> implements List<E> {
/** 锁:保护写操作 */
final transient ReentrantLock lock = new ReentrantLock();
/** 数组:volatile保证可见性 */
private transient volatile Object[] array;
// 构造函数
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
final Object[] getArray() {
return array;
}
final void setArray(Object[] a) {
array = a;
}
}
关键点:
- volatile数组:保证引用的可见性
- ReentrantLock:保护写操作(读不加锁)
- Object[]:实际存储数据
读操作:无锁
java
public E get(int index) {
return get(getArray(), index); // 直接读,不加锁!
}
private E get(Object[] a, int index) {
return (E) a[index];
}
public int size() {
return getArray().length; // 无锁
}
为什么读不需要锁?
- 读的是不可变的数组引用
- 写操作会替换整个数组,不会修改原数组
- volatile保证引用的可见性
写操作:加锁+复制
java
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock(); // 加锁
try {
Object[] elements = getArray();
int len = elements.length;
// 1. 复制数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 2. 修改副本
newElements[len] = e;
// 3. 替换引用(volatile写)
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
步骤详解:
ini
时刻1: 旧数组 [A, B, C]
↓
读线程正在读
时刻2: 写线程加锁
↓
复制数组 [A, B, C] → [A, B, C, D]
时刻3: 替换引用
旧数组 [A, B, C] ← 读线程继续读旧版
新数组 [A, B, C, D] ← 新的读线程读新版
迭代器:快照迭代
java
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}
static final class COWIterator<E> implements ListIterator<E> {
private final Object[] snapshot; // 快照
private int cursor;
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements; // 保存当前数组引用
}
public E next() {
return (E) snapshot[cursor++]; // 读快照
}
// ❌ 不支持remove
public void remove() {
throw new UnsupportedOperationException();
}
}
特点:
- 迭代器创建时拍摄快照
- 迭代过程中不会看到修改
- 不支持
remove()
操作
四、为什么CopyOnWrite适合读多写少?📊
性能分析
读操作:
java
// 无锁,直接访问
O(1) 时间复杂度
0 次内存分配
写操作:
java
// 加锁 + 复制整个数组
O(n) 时间复杂度
O(n) 空间复杂度
1 次内存分配
性能对比测试
java
public class PerformanceTest {
private static final int READERS = 100;
private static final int WRITERS = 1;
private static final int OPERATIONS = 100_000;
public static void main(String[] args) throws InterruptedException {
System.out.println("=== CopyOnWriteArrayList ===");
testCopyOnWrite();
System.out.println("\n=== Collections.synchronizedList ===");
testSynchronized();
}
private static void testCopyOnWrite() throws InterruptedException {
List<Integer> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 100; i++) list.add(i);
long start = System.currentTimeMillis();
// 100个读线程
Thread[] readers = new Thread[READERS];
for (int i = 0; i < READERS; i++) {
readers[i] = new Thread(() -> {
for (int j = 0; j < OPERATIONS; j++) {
int sum = 0;
for (Integer num : list) {
sum += num;
}
}
});
readers[i].start();
}
// 1个写线程
Thread writer = new Thread(() -> {
for (int i = 0; i < 100; i++) {
list.add(i);
}
});
writer.start();
for (Thread t : readers) t.join();
writer.join();
long time = System.currentTimeMillis() - start;
System.out.println("耗时: " + time + "ms");
}
private static void testSynchronized() throws InterruptedException {
List<Integer> list = Collections.synchronizedList(new ArrayList<>());
for (int i = 0; i < 100; i++) list.add(i);
long start = System.currentTimeMillis();
Thread[] readers = new Thread[READERS];
for (int i = 0; i < READERS; i++) {
readers[i] = new Thread(() -> {
for (int j = 0; j < OPERATIONS; j++) {
synchronized (list) { // 必须手动加锁
int sum = 0;
for (Integer num : list) {
sum += num;
}
}
}
});
readers[i].start();
}
Thread writer = new Thread(() -> {
for (int i = 0; i < 100; i++) {
list.add(i);
}
});
writer.start();
for (Thread t : readers) t.join();
writer.join();
long time = System.currentTimeMillis() - start;
System.out.println("耗时: " + time + "ms");
}
}
测试结果:
场景 | CopyOnWriteArrayList | synchronizedList |
---|---|---|
100读+1写 | 800ms | 5000ms |
100读+10写 | 1500ms | 5200ms |
10读+10写 | 3000ms | 2000ms |
结论:
- 读多写少 :CopyOnWrite快5倍!🚀
- 写多读少:CopyOnWrite更慢(复制开销大)
五、适用场景✅ vs 不适用场景❌
✅ 适合场景
-
监听器列表
javaclass EventPublisher { private final List<EventListener> listeners = new CopyOnWriteArrayList<>(); public void addListener(EventListener listener) { listeners.add(listener); // 写少 } public void fireEvent(Event event) { for (EventListener listener : listeners) { listener.onEvent(event); // 读多 } } }
-
白名单/黑名单
javaclass IPFilter { private final List<String> blacklist = new CopyOnWriteArrayList<>(); public boolean isBlocked(String ip) { return blacklist.contains(ip); // 读多 } public void addBlacklist(String ip) { blacklist.add(ip); // 写少 } }
-
配置信息
javaclass ConfigManager { private final List<Config> configs = new CopyOnWriteArrayList<>(); public Config getConfig(String key) { // 读多 return configs.stream() .filter(c -> c.getKey().equals(key)) .findFirst() .orElse(null); } public void updateConfig(Config config) { configs.removeIf(c -> c.getKey().equals(config.getKey())); configs.add(config); // 写少 } }
-
缓存失效监听
javaclass CacheManager { private final List<CacheInvalidationListener> listeners = new CopyOnWriteArrayList<>(); public void onCacheInvalid(String key) { listeners.forEach(l -> l.onInvalidate(key)); } }
❌ 不适合场景
-
写操作频繁
java// ❌ 每秒写1000次,每次复制数组 for (int i = 0; i < 1000; i++) { list.add(i); // 太慢了! } // ✅ 改用ConcurrentLinkedQueue Queue<Integer> queue = new ConcurrentLinkedQueue<>();
-
数据量大
java// ❌ 10万条数据,每次复制10万条 List<Data> list = new CopyOnWriteArrayList<>(); for (int i = 0; i < 100_000; i++) { list.add(new Data()); } list.add(new Data()); // 复制10万条,内存爆炸! // ✅ 改用ConcurrentHashMap
-
需要实时一致性
java// ❌ 迭代器是快照,看不到新增的 Iterator<String> it = list.iterator(); list.add("new"); // 迭代器看不到! // ✅ 需要实时性,用synchronizedList
-
内存敏感
java// ❌ 每次写都复制,内存占用翻倍 list.add("item"); // 内存瞬间翻倍
六、CopyOnWriteArrayList的陷阱⚠️
陷阱1:内存泄漏
java
List<byte[]> list = new CopyOnWriteArrayList<>();
// 每次add都复制整个数组
for (int i = 0; i < 10000; i++) {
list.add(new byte[1024 * 1024]); // 每个1MB
// 内存占用:旧数组 + 新数组 = 2倍
}
问题: 写入期间,旧数组和新数组同时存在,内存翻倍!
陷阱2:迭代器的弱一致性
java
List<String> list = new CopyOnWriteArrayList<>();
list.add("A");
list.add("B");
Iterator<String> it = list.iterator(); // 拍摄快照:[A, B]
list.add("C"); // 新增元素
while (it.hasNext()) {
System.out.println(it.next()); // 只输出A、B,看不到C
}
问题: 迭代器是快照,看不到后续修改。
陷阱3:批量操作性能差
java
// ❌ 错误:逐个add
for (int i = 0; i < 10000; i++) {
list.add(i); // 复制10000次数组!
}
// ✅ 正确:批量addAll
List<Integer> temp = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
temp.add(i);
}
list.addAll(temp); // 只复制1次!
性能对比:
- 逐个add:10000次复制
- 批量addAll:1次复制
陷阱4:不支持迭代器修改
java
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String item = it.next();
if (item.equals("remove")) {
it.remove(); // ❌ UnsupportedOperationException
}
}
// ✅ 正确做法
list.removeIf(item -> item.equals("remove"));
七、实战案例:事件监听器管理🎧
java
public class EventBus {
// 监听器列表:读多写少
private final Map<Class<?>, List<EventListener>> listeners =
new ConcurrentHashMap<>();
/**
* 注册监听器(写操作,少)
*/
public <T> void register(Class<T> eventType, EventListener<T> listener) {
listeners.computeIfAbsent(eventType, k -> new CopyOnWriteArrayList<>())
.add(listener);
}
/**
* 取消注册(写操作,少)
*/
public <T> void unregister(Class<T> eventType, EventListener<T> listener) {
List<EventListener> list = listeners.get(eventType);
if (list != null) {
list.remove(listener);
}
}
/**
* 发布事件(读操作,多)
*/
public <T> void post(T event) {
Class<?> eventType = event.getClass();
List<EventListener> list = listeners.get(eventType);
if (list != null) {
// 并发安全的迭代
for (EventListener listener : list) {
try {
listener.onEvent(event);
} catch (Exception e) {
System.err.println("监听器执行失败: " + e.getMessage());
}
}
}
}
/**
* 获取监听器数量
*/
public int getListenerCount(Class<?> eventType) {
List<EventListener> list = listeners.get(eventType);
return list == null ? 0 : list.size();
}
// 监听器接口
@FunctionalInterface
public interface EventListener<T> {
void onEvent(T event);
}
}
// 使用示例
public class Test {
public static void main(String[] args) {
EventBus eventBus = new EventBus();
// 注册监听器
eventBus.register(String.class, event ->
System.out.println("监听器1: " + event)
);
eventBus.register(String.class, event ->
System.out.println("监听器2: " + event)
);
// 并发发布事件
for (int i = 0; i < 100; i++) {
final int id = i;
new Thread(() -> {
eventBus.post("事件-" + id);
}).start();
}
}
}
优势:
- 监听器注册/取消很少(写少)
- 事件发布频繁(读多)
- 迭代器安全,不会抛异常
- 无需手动加锁
八、CopyOnWrite家族👨👩👧
1. CopyOnWriteArrayList
java
List<String> list = new CopyOnWriteArrayList<>();
list.add("A");
list.get(0);
2. CopyOnWriteArraySet
java
Set<String> set = new CopyOnWriteArraySet<>();
set.add("A");
set.contains("A");
实现:
java
public class CopyOnWriteArraySet<E> extends AbstractSet<E> {
private final CopyOnWriteArrayList<E> al;
public boolean add(E e) {
return al.addIfAbsent(e); // 不重复添加
}
}
特点:
- 底层用CopyOnWriteArrayList
- 保证元素唯一性
- 无序集合
九、与其他并发集合对比📊
特性 | CopyOnWriteArrayList | synchronizedList | ConcurrentLinkedQueue |
---|---|---|---|
读性能 | ⭐⭐⭐⭐⭐ 无锁 | ⭐⭐ 加锁 | ⭐⭐⭐⭐ CAS |
写性能 | ⭐ 复制数组 | ⭐⭐ 加锁 | ⭐⭐⭐⭐ CAS |
内存占用 | ⭐ 写时翻倍 | ⭐⭐⭐⭐ 正常 | ⭐⭐⭐ 节点对象 |
迭代安全 | ✅ 快照迭代 | ❌ 需手动加锁 | ✅ 弱一致性 |
实时性 | ❌ 快照 | ✅ 实时 | ✅ 实时 |
适用场景 | 读多写少 | 读写均衡 | 队列操作 |
十、面试高频问答💯
Q1: CopyOnWriteArrayList为什么适合读多写少?
A:
- 读操作无锁,多线程并发读,性能极高
- 写操作复制整个数组,开销大,适合写少
Q2: CopyOnWrite的迭代器为什么不会抛ConcurrentModificationException?
A: 迭代器创建时拍摄快照,迭代的是不可变的旧数组,不会看到后续修改,所以不会抛异常。
Q3: CopyOnWrite的内存开销有多大?
A: 写入时内存瞬间翻倍(旧数组+新数组),GC后恢复。数据量大时要谨慎!
Q4: 如何提高CopyOnWrite的写性能?
A:
- 批量操作:用
addAll
代替多次add
- 减少写入频率:批量累积后一次性写入
Q5: CopyOnWrite适合什么场景?
A:
- 监听器列表
- 黑白名单
- 配置信息
- 读写比 > 100:1
十一、总结:选型决策🎯
需要并发List?
├─ 读多写少(100:1)?
│ └─ 是 → CopyOnWriteArrayList ⭐
├─ 读写均衡?
│ └─ 是 → Collections.synchronizedList
├─ 写多读少?
│ └─ 是 → ConcurrentLinkedQueue(如果是队列)
├─ 数据量大(>1万)?
│ └─ 是 → 避免CopyOnWrite(内存爆炸)
└─ 需要实时一致性?
└─ 是 → 避免CopyOnWrite(快照迭代)
最佳实践
- 适用场景:监听器、配置、黑白名单
- 批量操作:用addAll,不要逐个add
- 控制大小:避免存储大量数据
- 监控内存:写入时内存翻倍
- 理解快照:迭代器看不到新修改
下期预告: ThreadPoolExecutor的拒绝策略如何自定义?生产环境如何选择?🚫