在电商促销秒杀系统中,商品库存的实时扣减是一个典型的高并发场景。某次大促期间,技术团队发现部分热门商品出现了"超卖"现象——实际销量超过了库存数量。经过排查,问题根源在于使用了非线程安全的ArrayList来维护库存变更记录。当多个用户同时抢购时,ArrayList的内部数组结构被并发修改,导致数据丢失或异常。这个案例揭示了在并发环境下选择合适集合容器的重要性。
ArrayList作为Java集合框架中最常用的动态数组实现,其非线程安全的特性往往被开发者忽视。让我们通过一个简化版的库存扣减示例,看看问题如何发生:
java复制public class InventorySystem {
private List<String> stockChanges = new ArrayList<>();
public void deductStock(String itemId) {
stockChanges.add("Deducted: " + itemId);
// 实际库存扣减逻辑...
}
}
当多个线程同时调用deductStock方法时,可能出现两种典型问题:
查看ArrayList的add方法源码,问题显而易见:
java复制public boolean add(E e) {
ensureCapacityInternal(size + 1); // 步骤1:容量检查
elementData[size++] = e; // 步骤2:元素插入
return true;
}
竞态条件分析:
| 线程A操作 | 线程B操作 | 导致结果 |
|---|---|---|
| 执行步骤1(size=9) | - | - |
| 时间片用完 | 执行步骤1(size=9) | 两者都认为不需要扩容 |
| 执行步骤2 | 执行步骤2 | 可能覆盖写入或数组越界 |
Java集合框架提供了简单的线程安全包装器,适用于大多数基础并发场景:
java复制List<String> syncList = Collections.synchronizedList(new ArrayList<>());
性能对比测试(10万次操作, 4线程):
| 操作类型 | synchronizedList | 普通ArrayList |
|---|---|---|
| 纯写入 | 320ms | 崩溃 |
| 读写混合 | 280ms | 崩溃 |
| 纯读取 | 120ms | 80ms |
提示:虽然读取性能有损耗,但保证了线程安全的基本要求
在订单处理系统中使用synchronizedList:
java复制public class OrderProcessingService {
private final List<Order> pendingOrders =
Collections.synchronizedList(new ArrayList<>());
public void addOrder(Order order) {
// 不需要额外同步
pendingOrders.add(order);
}
public void processOrders() {
synchronized(pendingOrders) { // 迭代时需要显式同步
for(Order order : pendingOrders) {
// 处理订单
}
pendingOrders.clear();
}
}
}
对于读多写少的场景,JDK提供了更高效的并发解决方案:
java复制List<String> cowList = new CopyOnWriteArrayList<>();
核心原理可以概括为:
add方法关键实现:
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();
}
}
典型使用场景:
内存优化技巧:
java复制// 预设容量减少扩容开销
List<String> cowList = new CopyOnWriteArrayList<>(Arrays.asList(new String[100]));
// 批量添加减少复制次数
cowList.addAll(batchItems);
根据实际业务需求选择合适方案:
code复制是否需要线程安全的List?
├── 是 → 写操作频率如何?
│ ├── 高频写 → Collections.synchronizedList
│ └── 低频写 → 数据量大小?
│ ├── 小型数据集 → CopyOnWriteArrayList
│ └── 大型数据集 → 考虑其他并发集合
└── 否 → 使用普通ArrayList
关键选择指标对比:
| 特性 | synchronizedList | CopyOnWriteArrayList |
|---|---|---|
| 读写性能 | 均衡 | 读极快,写较慢 |
| 内存消耗 | 低 | 高(写时复制) |
| 迭代器一致性 | 弱一致性 | 快照一致性 |
| 适用并发级别 | 中等(百级线程) | 高(千级线程读场景) |
在社交平台的在线用户列表维护中,我们采用了混合方案:
java复制public class OnlineUserManager {
// 读多写少的用户状态变更通知
private final CopyOnWriteArrayList<UserEventListener> listeners =
new CopyOnWriteArrayList<>();
// 写频繁的临时操作队列
private final List<Operation> operationQueue =
Collections.synchronizedList(new LinkedList<>());
public void addListener(UserEventListener listener) {
listeners.add(listener); // COW高效读
}
public void logOperation(Operation op) {
operationQueue.add(op); // 同步列表适合频繁写
}
}
性能调优发现:在8核服务器上,当读操作占比超过80%时,CopyOnWriteArrayList的吞吐量是synchronizedList的3-5倍。但在频繁修改的场景下,其内存占用可能增长到原来的2-3倍。