在秒杀系统中使用 ConcurrentHashMap 时,需要注意以下关键点,以确保系统的高并发性、线程安全性和正确性:
原子操作的组合问题
问题:ConcurrentHashMap 的单个操作(如 put、get)是线程安全的,但组合操作不是原子的。
比如:
if (!map.containsKey(key)) {
map.put(key, value); // 可能被其他线程插入,导致重复或覆盖
}
怎么办?
1 使用原子方法
map.putIfAbsent(key, value); // 原子操作
2 复杂逻辑用 compute 或 merge
map.compute(key, (k, v) -> (v == null) ? initialValue : v + delta);
库存扣减的原子性
错误示例:
Integer stock = map.get(productId);
if (stock > 0) {
map.put(productId, stock - 1); // 非原子!可能被其他线程覆盖
}
正确示例:
//使用 replace 循环重试(CAS 思想)
while (true) {
Integer oldStock = map.get(productId);
if (oldStock <= 0) break; // 库存不足
if (map.replace(productId, oldStock, oldStock - 1)) {
// 扣减成功
break;
}
// 重试(其他线程已修改)
}
或者
map.compute(productId, (k, v) -> (v != null && v > 0) ? v - 1 : v);
避免死循环或性能问题
compute 的陷阱:在 compute 方法内避免嵌套操作当前 map,可能引发死锁或性能下降。(另一篇介绍)
// 危险操作!可能阻塞或死锁
map.compute(keyA, (k, v) -> map.get(keyB));
初始化与空值处理
- 空值限制:ConcurrentHashMap 不允许 Key 或 Value 为 null。
- 初始化问题:未初始化的 Key 返回 null,需显式处理。
性能优化
- 合理配置参数:
- 初始容量(initialCapacity):避免频繁扩容。
- 负载因子(loadFactor):默认 0.75。
- 并发级别(concurrencyLevel):Java 7 分段锁的段数(Java 8+ 已优化,无需关注)。
- 避免热点 Key:不同商品 ID 分散到不同桶(Bucket),减少竞争。
迭代一致性
- 弱一致性迭代器:遍历时可能反映部分并发修改(不抛 ConcurrentModificationException),但不保证实时一致性。
- 秒杀场景建议:避免在关键逻辑中使用迭代器(如统计库存),改用原子方法。
资源清理
- 下架商品处理:移除无用的 Key 释放内存。
- 使用 ConcurrentHashMap 存储库存时,需额外机制处理商品过期。
监控与诊断
- 竞争检测:监控桶(Bucket)的链表长度(或红黑树深度),过长表明哈希冲突严重。
- 工具:JProfiler、Arthas 分析热点 Key 和锁竞争。
补充:秒杀场景的额外建议
- 库存隔离:
- 用 ConcurrentHashMap 存储 内存库存,但需与数据库/缓存同步(如通过异步写回)。
- 熔断降级:
- 当检测到桶竞争激烈时,触发限流(如 Sentinel)。
- 容量规划:
- 预估商品数和并发量,设置足够初始容量(如 new ConcurrentHashMap<>(512, 0.75f))。
- 结合其他组件:
- 超卖控制:ConcurrentHashMap + Redis 分布式锁/ Lua 脚本。
- 库存预热:系统启动时加载库存到 ConcurrentHashMap。
总结
ConcurrentHashMap 适用于秒杀系统的内存库存管理,但必须:
- 严格保证组合操作的原子性(用 replace/compute)。
- 预防热点 Key 竞争(如分散商品 ID)。
- 与外围系统协同(数据库、分布式锁)。
- 监控性能瓶颈(桶冲突、GC 压力)。
在高并发场景下,建议结合 Redis 分布式锁、消息队列异步化、限流熔断等机制,构建完整的秒杀架构。