本文深入剖析在 concurrenthashmap 中使用 mutableinteger 手动实现计数时线程不安全的根源,指出“看似加锁却仍出错”的本质在于非原子的 get-put-compute 操作序列,并提供基于 merge/compute 的标准、简洁、线程安全的替代方案。
你的代码看似做了多重防护:MutableInteger 的 getValue() 和 setValue() 方法用 synchronized 加锁,ConcurrentHashMap 本身也是线程安全的容器——但最终计数结果依然错误。问题不在于“锁没起作用”,而在于你根本没有对「计数逻辑」这个整体进行原子保护。
关键缺陷出现在 saveFlow() 方法中这一典型模式:
MutableInteger newMinuteAcceptKey = new MutableInteger();
newMinuteAcceptKey.setValue(1);
MutableInteger oldMinuteAcceptKey = map.put(minuteAcceptKey, newMinuteAcceptKey); // 步骤①:写入新值
if (null != oldMinuteAcceptKey) {
newMinuteAcceptKey.setValue(oldMinuteAcceptKey.getValue() + 1); // 步骤②:读旧值 → 计算 → 写回新值
}这段逻辑看似合理,实则存在经典的 check-then-act(先检查后执行)竞态条件:
⚠️ 注意:ConcurrentHashMap 仅保证单个操作(如 get, put, remove)的线程安全性,绝不保证多个操作组合的原子性。synchronized 在 MutableInteger 上只保护该对象内部状态,但无法约束 map 容器中键值对的生命周期与引用关系。
Java 8+ 为 ConcurrentHashMap 提供了真正原子的复合操作 API,推荐优先使用 merge() 或 compute():
// ✅ 推荐:一行代码完成「若存在则累加,否则初始化为1」 map.merge(minuteAcceptKey, 1, Integer::sum); // ✅ 等价写法(显式 lambda) map.merge(minuteAcceptKey, 1, (oldVal, newVal) -> oldVal + newVal); // ✅ 或使用 compute(更灵活,可处理 null) map.compute(minuteAcceptKey, (key, oldValue) -> (oldValue == null) ? 1 : oldValue + 1 );
此时你甚至不再需要 MutableInteger —— Integer 是不可变对象,merge 内部会以原子方式完成整个读-改-写流程,无需外部同步。
public static void saveFlow(Calendar c) {
SimpleDateFormat sdfDate = new SimpleDateFormat("yyyy-MM-dd");
SimpleDateFormat sdfHour = new SimpleDateFormat("yyyy-MM-dd HH");
SimpleDateFormat sdfMinute = new SimpleDateFormat("yyyy-MM-dd HH:mm");
String dayKey = sdfDate.format(c.getTime());
String hourKey = sdfHour.format(c.getTime());
String minuteKey = sdfMinute.format(c.getTime());
// 原子递增:不存在则设为1,存在则+1
map.merge(minuteKey, 1, Integer::sum);
map.merge(hourKey, 1, Integer::sum);
map.merge(dayKey, 1, Integer::sum);
}遵循原子操作原则,才能真正驾驭并发编程的复杂性。