putIfAbsent能避免重复初始化,因其是原子操作,仅当key不存在时才插入value,底层通过CAS+synchronized保证线程安全,杜绝了“get-then-put”竞态窗口。

putIfAbsent 为什么能避免重复初始化
putIfAbsent 是 ConcurrentHashMap 提供的原子操作:仅当 key 不存在时才插入 value,返回 null;否则返回已存在的旧值。它底层依赖 CAS + synchronized(JDK 8 后是 synchronized 锁住对应 bin 链表头节点),不是简单地“先 get 再 put”,因此不会因竞态导致两次初始化。
常见错误是用 get 判断后再调用 put —— 这中间存在时间窗口,多个线程都可能通过判断并执行初始化逻辑。
典型误用:用 get + put 模拟 putIfAbsent
下面这种写法看似合理,实则线程不安全:
if (map.get(key) == null) {
map.put(key, expensiveInit()); // ❌ 两个线程都可能进这里
}
即使 expensiveInit() 很慢,也可能被多次调用。而正确做法是把初始化逻辑放在 putIfAbsent 的调用之外,或用更稳妥的 double-check 模式(见下一条)。
- 不要在
putIfAbsent外部做条件判断后初始化 - 不要把耗时初始化逻辑直接塞进
putIfAbsent的 value 参数里(比如map.putIfAbsent(k, init())),因为 init() 会在每个竞争线程中执行,只是最终只保留一个结果 - 如果初始化成本极高(如加载大配置、建连接池),应确保 init() 只执行一次
安全初始化模式:computeIfAbsent 更合适
对于“按需初始化单例对象”的场景,computeIfAbsent 是更自然的选择。它只在 key 不存在时才执行 mappingFunction,且该函数对每个 key 最多执行一次(即使多个线程同时触发)。
MyService service = map.computeIfAbsent(key, k -> new MyService()); // ✅ 安全
注意:computeIfAbsent 的 lambda 不会被并发重复执行;而 putIfAbsent(key, new MyService()) 中的 new MyService() 会在每个线程里先构造一次,再由 map 丢弃多余实例 —— 浪费资源且可能引发副作用(如提前打开文件句柄)。
-
computeIfAbsent是懒初始化 + 原子绑定的组合,语义更清晰 - lambda 内不应有阻塞或长时操作,否则会阻塞其他 key 的 compute 操作(锁粒度是 bin,但函数执行期间持有该 bin 锁)
- JDK 9+ 对
computeIfAbsent做了优化,支持更宽松的非阻塞重试,但老版本仍需留意锁竞争
putIfAbsent 在什么场景下反而更合适
当你已经有一个确定的、无副作用的 value 实例,只是想“尝试注册”,比如缓存一个预创建的默认对象、设置一个初始计数器值,这时 putIfAbsent 更轻量、意图明确:
map.putIfAbsent("config", defaultConfig); // ✅ value 已就绪,无副作用
对比 computeIfAbsent,它不涉及函数调用开销,也不需要处理函数抛异常的边界(computeIfAbsent 遇到 unchecked exception 会传播,可能导致 map 状态未更新但调用者已失败)。
- value 是常量、静态实例或已构造好的对象 → 优先
putIfAbsent - value 需要动态计算且计算昂贵/有副作用 → 必须用
computeIfAbsent - 两者都不适合做 I/O 或同步阻塞操作;真有这类需求,应外包给
CompletableFuture或专用初始化器
真正容易被忽略的是:putIfAbsent 和 computeIfAbsent 的“唯一性”都只针对 key,不保证 value 构造过程的全局唯一——构造逻辑是否被多次执行,取决于你把它放在哪一层。

















