1. ArrayList线程安全问题的本质
ArrayList作为Java集合框架中最常用的动态数组实现,其非线程安全的特性在实际开发中经常成为隐蔽的Bug源头。我们先看一个典型的生产事故场景:
去年我们电商系统在促销活动时,订单服务突然出现大量"幽灵订单"——系统日志显示订单已生成,但数据库却查不到记录。经过通宵排查,最终定位到问题出在订单缓存使用的ArrayList上。在高并发场景下,多个线程同时操作同一个ArrayList导致元素丢失和覆盖。
1.1 底层结构分析
ArrayList内部使用Object[]数组存储元素,关键字段包括:
java复制transient Object[] elementData; // 存储数组
private int size; // 当前元素数量
当执行add操作时,典型的非原子性操作序列:
java复制public boolean add(E e) {
ensureCapacityInternal(size + 1); // 步骤1:检查扩容
elementData[size++] = e; // 步骤2:赋值并增加size
return true;
}
1.2 并发问题具体表现
- 元素覆盖:两个线程同时执行add时,可能获取到相同的size值,导致后写入的值覆盖前一个
- 大小不一致:size++非原子操作,可能导致size与实际元素数量不符
- 扩容异常:多个线程同时触发扩容,可能引发数组越界或NullPointerException
实测数据:在8核CPU上,100个线程各执行1000次add操作,普通ArrayList平均丢失23%的元素
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 线程安全解决方案对比
2.1 Vector的优劣分析
Vector通过方法级别的synchronized实现线程安全:
java复制public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
优势:
- 实现简单,所有方法自带同步
- JDK1.0就存在,兼容性极好
劣势:
- 锁粒度太粗,性能差(实测吞吐量只有ArrayList的1/10)
- 迭代器仍需要外部同步
- 部分复合操作仍需加锁(如addIfAbsent)
2.2 Collections.synchronizedList
包装器方案:
java复制List<String> syncList = Collections.synchronizedList(new ArrayList<>());
实现特点:
- 所有方法通过mutex对象加锁
- 迭代器需要手动同步
- 适合读多写少场景
性能对比(ops/ms):
| 操作类型 | ArrayList | Vector | synchronizedList |
|---|---|---|---|
| 纯写入 | 1582 | 132 | 145 |
| 读写混合 | 1243 | 98 | 112 |
2.3 CopyOnWriteArrayList深度解析
写时复制技术的实现:
java复制public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
适用场景:
- 读操作远多于写操作(配置信息缓存)
- 集合规模较小(复制成本可控)
- 需要保证迭代器弱一致性
内存消耗对比(存储10000个Integer对象):
- ArrayList:约80KB
- CopyOnWriteArrayList:写入时瞬时峰值160KB
3. 实战中的选择策略
3.1 性能敏感场景优化
对于高频更新的计数器场景,推荐组合方案:
java复制// 写优化方案
public class Counter {
private final ConcurrentHashMap<String, AtomicLong> map = new ConcurrentHashMap<>();
public void increment(String key) {
map.computeIfAbsent(key, k -> new AtomicLong()).incrementAndGet();
}
}
3.2 读多写少场景实现
配置信息缓存示例:
java复制public class ConfigCenter {
private volatile CopyOnWriteArrayList<Config> configs;
public void updateConfig(Config newConfig) {
// 写时复制更新
}
public List<Config> getConfigs() {
return configs; // 无需加锁直接返回
}
}
3.3 复合操作解决方案
即使使用线程安全集合,复合操作仍需注意:
java复制// 错误示例
if (!list.contains(item)) {
list.add(item); // 仍然存在竞态条件
}
// 正确方案1:加锁
synchronized(lock) {
if (!list.contains(item)) {
list.add(item);
}
}
// 正确方案2:使用ConcurrentHashMap
Set<String> set = ConcurrentHashMap.newKeySet();
set.add(item); // 原子性操作
4. 生产环境诊断技巧
4.1 问题定位方法
- 日志分析:查找ConcurrentModificationException
- 内存dump:分析集合size与实际元素数量
- 线程dump:检查集合操作的线程竞争
4.2 压测验证方案
使用JMeter测试脚本示例:
java复制@ThreadGroup(numThreads=100, rampUp=10)
public void testArrayList() {
List<Integer> list = new ArrayList<>();
IntStream.range(0,1000).parallel().forEach(list::add);
assert list.size() == 1000 : "元素丢失:" + (1000 - list.size());
}
4.3 监控指标建议
关键监控项:
- 集合操作耗时(P99 < 50ms)
- 并发修改异常次数(应=0)
- GC频率(CopyOnWriteArrayList可能引发频繁GC)
5. 新版JDK改进方案
5.1 Java 9的List.of()
不可变集合方案:
java复制List<String> immutableList = List.of("a", "b", "c");
特点:
- 完全线程安全
- 不允许修改(包括set操作)
- 内存优化(共享底层数组)
5.2 Java 21虚拟线程适配
虚拟线程环境下,synchronized不再是大问题:
java复制try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
List<String> list = new ArrayList<>(); // 仍不推荐!
IntStream.range(0,10_000).forEach(i ->
executor.submit(() -> list.add(String.valueOf(i)))
);
}
最佳实践:
java复制List<String> list = new CopyOnWriteArrayList<>();
// 或
List<String> list = Collections.synchronizedList(new ArrayList<>());
6. 架构层面的解决方案
对于分布式系统,还可以考虑:
- 使用Redis等分布式缓存
- 采用Actor模型(如Akka)
- 事件溯源(Event Sourcing)模式
在最近的一个物联网平台项目中,我们最终采用的分层方案:
- 前端写入:ConcurrentHashMap
- 批量处理:CopyOnWriteArrayList
- 数据展示:不可变List
