ConcurrentMap.putIfAbsent不生效主因是未检查返回值或equals/hashCode不合规;推荐用computeIfAbsent实现懒加载,但需避免其lambda内操作map或耗时操作。
ConcurrentMap.putIfAbsent 为什么有时不生效
它确实能保证「键不存在时才写入」的原子性,但很多人误以为它能解决所有并发写冲突。实际中失效往往是因为
的返回值没被检查,或对象引用/equals 逻辑有问题。
常见错误现象:
调用后,多个线程还是同时初始化了同一个 key 对应的 value(比如缓存加载、单例构建)。
必须用返回值判断是否真正插入成功:
—— 注意是
,不是
value 对象若重写了
和
,要确保逻辑正确;否则
内部可能误判重复
key 是自定义对象时同理:
/
必须稳定且符合约定,否则
可能查不到已存在的 key
putIfAbsent 和 computeIfAbsent 哪个更适合初始化场景
绝大多数初始化场景该用
,而不是
。前者在 key 不存在时才执行函数生成 value,天然避免「先查再 put」的竞态;后者要求你提前构造好 value 实例,可能白做功甚至引发副作用。
使用场景举例:缓存未命中时从数据库加载对象。
立即学习
“
Java免费学习笔记(深入)
”;
Eclipse导入Android或其他的JAVA项目的正确方法 WORD版
本文档主要讲述的是Eclipse导入Android或其他的JAVA项目的正确方法;希望本文档会给有需要的朋友带来帮助;感兴趣的朋友可以过来看看
下载
❌ ——
总是执行,不管 key 是否已存在
✅ —— 仅当 key 缺失时才调用
的 lambda 是线程安全的:同一 key 多个线程并发调用,最多只有一个会执行 lambda,其余阻塞等待其结果
ConcurrentHashMap.computeIfAbsent 的隐藏限制
它不是万能的「懒加载锁」。JDK 8 中,如果 lambda 里又去读写同一个 map(比如递归调用或触发 rehash),会直接死锁 —— 这是明确的 Javadoc 警告行为,不是 bug。
错误典型表现:
,堆栈里反复出现 map 操作。
lambda 内禁止调用任何
或
方法,也避免在其中修改当前 key 对应的桶(如 put/remove)
不要在 lambda 里做耗时操作(如远程调用、文件读写),它会阻塞其他线程对该 key 所在 bin 的所有操作
JDK 9+ 放宽了部分限制,但仍不建议在 compute 函数中再操作 map,兼容性和可读性都差
替代方案:什么时候该放弃 putIfAbsent / computeIfAbsent
当 value 初始化成本高、失败率高、或需要更精细的失败重试控制时,这两个方法就力不从心了。它们只提供「存在则跳过,不存在则设值」这一层语义,没法处理「初始化失败后要不要重试」「要不要降级」「要不要记录日志」等逻辑。
高频 key + 低成功率初始化(如依赖不稳定的外部服务):改用带锁的 double-checked locking,配合
引用和显式异常处理
需要 fallback 逻辑(如缓存加载失败返回默认值):自己封装一个
工具方法,内部用
或
跨多个 map 或资源协调(如「缓存 + 数据库 + 本地文件」三者一致性):别硬套
,老实用分布式锁或状态机
最常被忽略的一点:
的函数参数是 key,但很多人传进去的是闭包捕获的可变变量,导致不同 key 共享同一份计算逻辑,结果错乱。务必确认 lambda 里用的都是 key 或其派生值。
putIfAbsentputIfAbsentif (map.putIfAbsent(key, value) == null)== null!= nullequalshashCodeConcurrentHashMapequalshashCodeputIfAbsentcomputeIfAbsentputIfAbsentputIfAbsent(key, loadFromDB(key))loadFromDB(key)computeIfAbsent(key, k -> loadFromDB(k))loadFromDBcomputeIfAbsentThread blocked on java.util.concurrent.ConcurrentHashMap.computeIfAbsentcompute*mergevolatilecomputeIfAbsentWithFallbacksynchronizedStampedLockcomputeIfAbsentcomputeIfAbsent