去年双十一大促期间,我们的商品推荐服务突然出现响应延迟飙升。监控系统显示,某个核心接口的TP99从平时的50ms暴涨到800ms,更诡异的是部分用户看到的推荐列表竟然出现重复商品。经过紧急排查,最终定位到问题根源——一个被多个线程共享的ArrayList。这个看似简单的集合类型选择失误,差点让我们损失数百万销售额。本文将完整还原这次事故的诊断过程,并通过JMeter压测对比两种线程安全List的实际表现,为高并发场景下的集合选型提供数据支撑。
那天的故障从下午3点开始,正值流量高峰。运维团队最先收到报警,发现10台服务器中有3台的CPU利用率突破90%。通过Arthas实时诊断,我们很快锁定了一个名为getHotProducts()的方法。该方法内部维护了一个存放热门商品ID的ArrayList,会被多个推荐线程频繁读取和更新。
关键问题复现代码片段:
java复制// 问题代码示例
List<String> hotProductIds = new ArrayList<>();
// 线程A(更新线程)
public void updateHotProducts(List<Product> newProducts) {
hotProductIds.clear();
newProducts.forEach(p -> hotProductIds.add(p.getId()));
}
// 线程B(读取线程)
public List<String> getRecommendations() {
return hotProductIds.stream()
.limit(10)
.collect(Collectors.toList());
}
当用JConsole监控线程状态时,我们观察到了典型的多线程问题特征:
java.util.ArrayList.add()方法上ArrayIndexOutOfBoundsException异常通过Thread Dump分析,确认了多个线程同时执行add操作时的竞争情况。这正符合ArrayList在并发环境下的经典问题:丢失更新和数据不一致。
解决ArrayList并发问题主要有两种标准方案,它们在实现原理和适用场景上存在显著差异:
synchronizedList通过在方法级别加锁实现线程安全,其核心机制包括:
synchronized修饰性能特点:
| 操作类型 | 锁粒度 | 并发性能 |
|---|---|---|
| 读操作 | 方法级 | 中等(读读互斥) |
| 写操作 | 方法级 | 中等(写写互斥) |
| 读写操作 | 方法级 | 差(读写互斥) |
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); // volatile写保证可见性
return true;
} finally {
lock.unlock();
}
}
// 读操作实现
public E get(int index) {
return (E) getArray()[index]; // 直接访问数组无需同步
}
为了量化两种方案的性能差异,我们设计了完整的压测方案:
properties复制threads=200
ramp-up=30s
duration=300s
JMeter测试计划关键配置:
xml复制<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="List压测">
<intProp name="ThreadGroup.num_threads">200</intProp>
<stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
<elementProp name="ThreadGroup.main_controller" elementType="LoopController">
<boolProp name="LoopController.continue_forever">false</boolProp>
<intProp name="LoopController.loops">-1</intProp>
</elementProp>
</ThreadGroup>
经过连续5分钟的压测,我们得到了以下关键数据:
| 场景类型 | synchronizedList | CopyOnWriteArrayList |
|---|---|---|
| 纯读 | 12,345 | 45,678 |
| 读写混合 | 8,901 | 32,456 |
| 写密集型 | 6,789 | 5,432 |
| 场景类型 | synchronizedList | CopyOnWriteArrayList |
|---|---|---|
| 纯读 | 16 | 4 |
| 读写混合 | 22 | 6 |
| 写密集型 | 29 | 35 |
synchronizedList内存稳定,与数据量线性相关CopyOnWriteArrayList在写入时会出现短暂内存翻倍选型决策矩阵:
那次事故后,我们将推荐服务的核心集合全部替换为CopyOnWriteArrayList,并在后续的大促中保持了稳定的毫秒级响应。这个案例让我深刻体会到,在高并发系统中,即使是最基础的数据结构选择,也需要用真实的性能数据来说话。