一、先说结论吧
在多线程环境下选择合适的List实现至关重要。下表总结了常见List实现的特性:
| 集合类型 | 线程安全 | 读性能 | 写性能 | 内存占用 | 使用场景与注意事项 |
|---|---|---|---|---|---|
| ArrayList | ❌ 不安全 | ⭐⭐⭐⭐⭐ 直接索引访问 | ⭐⭐⭐⭐ 尾部插入高效 | ⭐⭐⭐⭐ 连续存储 | 单线程场景 随机访问频繁,尾部插入多 |
| LinkedList | ❌ 不安全 | ⭐⭐ 需遍历节点 | ⭐⭐⭐⭐⭐ 任意位置插入高效 | ⭐⭐ 节点开销大 | 频繁插入删除 头尾操作多的队列场景 |
| Vector | ✅ 安全 | ⭐ 全局锁阻塞 | ⭐ 全局锁阻塞 | ⭐⭐⭐⭐ 连续存储 | 已过时 遗留代码兼容,不推荐新项目使用 |
| synchronizedList | ✅ 安全 | ⭐⭐ 方法级同步 | ⭐⭐ 方法级同步 | ⭐⭐⭐⭐ 包装器模式 | 通用线程安全方案 注意:复合操作需手动同步 |
| CopyOnWriteArrayList | ✅ 安全 | ⭐⭐⭐⭐⭐ 无锁读快照 | ⭐ 写时复制开销大 | ⭐ 多版本存储 | 读多写极少 迭代器安全,写操作昂贵 |
二、实战案例:数据迁移中的线程安全问题
最近在数据迁移任务中,我遇到了一个典型的并发问题。先看问题代码:
public void migrateTemplate() throws InterruptedException {
// 问题点:使用非线程安全的ArrayList
List<CustomerRotationLog> targets = new ArrayList<>();
// 从MongoDB获取待迁移数据
List<MongodbDynamicMessage> all = mongodbDynamicMessageMapper.findAll();
// 创建线程计数器,用于同步所有转换任务
CountDownLatch latch = new CountDownLatch(all.size());
for (MongodbDynamicMessage source : all) {
// 使用线程池异步转换
ThreadPoolUtil.executeSafely(() -> {
try {
// 数据转换
CustomerRotationLog target = convert(source);
targets.add(target); // 🚨 线程不安全操作!
} finally {
latch.countDown();
System.out.println("处理进度:" + latch.getCount() + "/" + all.size());
}
});
}
// 等待所有任务完成
latch.await();
// 批量保存
commonService.processInBatch(targets, baseMapper::batchInsert);
}
问题分析
这段代码表面逻辑清晰,但实际上存在严重的线程安全问题 。最终的targets.size()很可能会小于all.size()。
根本原因 :ArrayList的add()方法是非线程安全的。在多线程并发调用时,两个主要操作:
-
elementData[size] = element;// 存储元素 -
size = size + 1;// 更新大小
这两个操作不是原子的,可能导致:
-
数据覆盖:多个线程对同一位置赋值
-
大小不一致 :
size更新不及时或错误 -
ArrayIndexOutOfBoundsException:扩容过程中的竞争条件
三、解决方案:使用synchronizedList
针对上述问题,最简单的修复方案是使用Collections.synchronizedList():
public void migrateTemplate() throws InterruptedException {
// 解决方案:使用线程安全的synchronizedList
List<CustomerRotationLog> targets = Collections.synchronizedList(new ArrayList<>());
List<MongodbDynamicMessage> all = mongodbDynamicMessageMapper.findAll();
CountDownLatch latch = new CountDownLatch(all.size());
for (MongodbDynamicMessage source : all) {
ThreadPoolUtil.executeSafely(() -> {
try {
CustomerRotationLog target = convert(source);
targets.add(target); // ✅ 现在是线程安全的
} finally {
latch.countDown();
System.out.println("处理进度:" + latch.getCount() + "/" + all.size());
}
});
}
latch.await();
commonService.processInBatch(targets, baseMapper::batchInsert);
}
四、总结与最佳实践
-
线程安全是第一要务:在多线程环境下操作共享集合,必须选择线程安全的实现
-
根据场景选择方案:
-
读多写极少 :选
CopyOnWriteArrayList,迭代安全,无需额外同步 -
写多或读写均衡 :选
synchronizedList,注意复合操作的手动同步
-
-
理解实现原理:
-
synchronizedList:方法级同步锁,适合通用场景 -
CopyOnWriteArrayList:写时复制,适合读主导场景
-
-
避免使用遗留类 :
Vector已过时,其设计不适合现代并发需求