资讯动态

Java ConcurrentHashMap 实战:computeIfAbsent 重入死锁、原子累加与 size 为什么不准

发布时间:2026/8/14 21:38:31 来源:尧图企业网站定制
Java ConcurrentHashMap 实战:computeIfAbsent 重入死锁、原子累加与 size 为什么不准很多人把ConcurrentHashMap当成「加了锁的 HashMap」,以为随便用都线程安全。实际上它只保证单个方法调用的原子性,一旦你把「读—改—写」拆成几步,或者在computeIfAbsent里再动这个 map,坑就来了。这篇讲三个真实踩过的问题。陷阱一:先 get 再 put 不是原子操作统计词频最容易写出这种代码:// 错误:get 和 put 之间可能被其他线程插队,计数丢失ConcurrentHashMapString,IntegermapnewConcurrentHashMap();Integercmap.get(word);map.put(word,cnull?1:c1);get和put各自线程安全,但组合起来有竞态:两个线程同时读到 5,各自 1 写回 6,少算一次。ConcurrentHashMap帮不了你,因为它根本不知道这两次调用要绑在一起。正确做法是用一次原子调用完成累加。计数场景首选merge:// 正确:merge 内部对该 key 加锁,读改写是原子的map.merge(word,1,Integer::sum);或者用getOrDefault是只读的、compute系列是原子的,别把它们和裸put混着用。陷阱二:computeIfAbsent 里再碰同一个 map 死锁/异常computeIfAbsent会对当前 key 所在的桶加锁,然后执行你的 lambda。如果 lambda 里又去写同一个 map,轻则抛异常,重则活锁。经典翻车:递归建缓存。// 危险:mappingFunction 里又调了 computeIfAbsentmap.computeIfAbsent(a,k-{map.computeIfAbsent(b,k2-compute(k2));// 可能死锁/IllegalStateExceptionreturncompute(k);});Java 9 对这种自引用会抛IllegalStateException: Recursive update,更早的版本可能直接卡死。修法是把「计算」和「放入」拆开,计算过程不碰 map:// 先算好,再原子放入Valuevmap.get(key);if(vnull){ValuecomputedexpensiveCompute(key);// 纯计算,不碰 mapvmap.putIfAbsent(key,computed);// 已存在就用别人放的if(vnull)vcomputed;}putIfAbsent返回旧值:返回 null 说明是你放进去的,非 null 说明别人先放了,用它的即可,天然去重。陷阱三:size() 是估算值,别拿去做精确判断高并发下size()/isEmpty()返回的是某一瞬间的近似值,不保证和「此刻真实元素数」一致。下面这种「满了就停」的逻辑会出错:// 不可靠:size() 在并发写入时可能读到旧值if(map.size()MAX)return;// 可能已经超了,也可能还没到map.put(key,val);如果你需要「容量上限」这种强一致语义,ConcurrentHashMap本身不提供,应该用带计数的原子变量或专门的限流结构。size()只适合做监控、日志这类容忍误差的场景。顺带一提:原子累加用 LongAdder 更快如果只是全局计数(不是按 key 分组),别用ConcurrentHashMap存一个 key,直接上LongAdder,高并发下比AtomicLong少了 CAS 自旋冲突:LongAddercounternewLongAdder();counter.increment();// 内部分段累加,竞争小longtotalcounter.sum();小结ConcurrentHashMap只保证单次方法调用原子,get之后再put会丢更新;计数用merge(key, 1, Integer::sum)。computeIfAbsent的 lambda 里绝不能再写同一个 map,Java 9 会抛Recursive update;拆成「先算后 putIfAbsent」。size()/isEmpty()是估算值,不能用来做精确的容量判断。记忆点:读改写要合并成一次原子调用,计算逻辑别塞进 compute 的 lambda 里。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价