ConcurrentHashMap四大方法深度解析:put、putIfAbsent、computeIfAbsent、compute
深入理解ConcurrentHashMap核心写入方法的本质差异,掌握并发场景下的最佳实践
引言
在日常开发中,你一定用过ConcurrentHashMap,但在选择写入方法时是否犯过难?
- 为什么
put和putIfAbsent看起来很像,但适用场景完全不同? computeIfAbsent和compute到底解决了什么痛点?- 多线程环境下,这些方法的原子性保证有何区别?
本文将从源码层面深入剖析这四个方法的实现原理、适用场景和性能差异,帮助你掌握并发场景下的最佳实践。阅读本文,你将学到:
- 四个方法的底层实现机制和原子性差异
- 不同业务场景下的方法选择策略
- 避免并发陷阱的实战经验
- 性能优化的关键技巧
背景介绍
ConcurrentHashMap作为Java并发包中的核心类,提供了多种写入方法。这些方法虽然看似相似,但在并发安全性、原子性保证、返回值语义上存在本质差异。
在多线程环境下,错误选择方法可能导致:
- 数据覆盖
- 竞态条件
- 性能下降
- 逻辑错误
理解这些方法的差异,是写出高质量并发代码的基础。
核心方法对比总览
先通过一张表快速了解四个方法的核心差异:
| 方法 | 原子性 | 返回值 | 适用场景 | 线程安全 |
|---|---|---|---|---|
put | 无 | 旧值或null | 简单覆盖 | ✓ |
putIfAbsent | 原子检查并插入 | 旧值或null | 防重复插入 | ✓ |
computeIfAbsent | 原子检查并计算 | 新值 | 懒初始化 | ✓ |
compute | 原子读取并计算 | 新值 | 复杂更新逻辑 | ✓ |
一、put方法:简单直接的覆盖
基本用法
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key1", 100); // 直接放入
Integer oldValue = map.put("key1", 200); // 覆盖,返回旧值100
源码分析
put方法本质上是调用了putVal:
public V put(K key, V value) {
return putVal(key, value, false);
}
// 第三个参数 onlyIfAbsent = false,表示无论是否存在都覆盖
final V putVal(K key, V value, boolean onlyIfAbsent) {
// ... 省略hash计算和节点定位逻辑
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 1. 表未初始化则初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// 2. 桶为空,直接CAS插入
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
break;
}
// 3. 遇到扩容标志,帮助扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
// 4. 桶已有数据,加锁同步
else {
V oldVal = null;
synchronized (f) { // 锁住首节点
if (tabAt(tab, i) == f) {
// 链表处理逻辑
for (int binCount = 0; ; ++binCount) {
Node<K,V> e; K k;
// 找到相同key
if (f.hash == hash &&
((k = f.key) == key ||
(key != null && key.equals(k)))) {
e = f;
break;
}
Node<K,V> pred = f;
if ((f = f.next) == null) {
// 插入新节点
pred.next = new Node<K,V>(hash, key, value, null);
break;
}
}
if (e != null) { // 已存在
V oldValue = e.val;
// 关键:onlyIfAbsent=false时直接覆盖
if (!onlyIfAbsent || oldValue == null)
e.val = value;
oldVal = oldValue;
}
}
}
}
}
return oldVal;
}
核心特点
- 非原子操作:
put本身是线程安全的,但"检查再操作"模式需要额外同步 - 直接覆盖:
onlyIfAbsent=false,无论key是否存在都会覆盖 - 返回值:返回旧值,key不存在返回
null
适用场景
✅ 适合场景:
- 简单的赋值操作,不需要检查旧值
- 确定不会产生并发冲突的场景
- 缓存更新,允许覆盖旧数据
❌ 不适合场景:
- 需要防止数据覆盖
- 复杂的"检查再操作"逻辑
常见错误示例
// ❌ 错误:非原子的"检查再操作"
if (!map.containsKey("key")) {
map.put("key", computeExpensiveValue()); // 可能重复计算
}
// ✓ 正确:使用putIfAbsent或computeIfAbsent
map.putIfAbsent("key", computeExpensiveValue());
// 或
map.computeIfAbsent("key", k -> computeExpensiveValue());
二、putIfAbsent:原子防重复插入
基本用法
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
Integer result1 = map.putIfAbsent("key1", 100); // 返回null,插入成功
Integer result2 = map.putIfAbsent("key1", 200); // 返回100,插入失败,原值不变
源码分析
public V putIfAbsent(K key, V value) {
return putVal(key, value, true); // onlyIfAbsent = true
}
核心区别在于onlyIfAbsent=true,当key已存在时不覆盖。
核心特点
- 原子性:检查和插入是原子操作,不会被其他线程打断
- 不覆盖:key已存在时保留旧值
- 返回值:返回关联的旧值,不存在则返回
null
性能陷阱
⚠️ 重要:`value参数总是会被求值**,即使最终不会插入!
// ❌ 性能陷阱:expensiveValue()总是会被调用
map.putIfAbsent("key", expensiveComputation());
// ✓ 正确:使用computeIfAbsent实现懒计算
map.computeIfAbsent("key", k -> expensiveComputation());
适用场景
✅ 适合场景:
- 需要防止重复插入
- value对象已经创建,只需确保不重复
- 作为
put的原子替代
❌ 不适合场景:
- value的计算成本高(使用
computeIfAbsent) - 需要根据旧值计算新值(使用
compute)
三、computeIfAbsent:原子懒初始化
基本用法
ConcurrentHashMap<String, List<String>> map = new ConcurrentHashMap<>();
// 只在key不存在时才创建List
List<String> list = map.computeIfAbsent("key1", k -> new ArrayList<>());
list.add("item1");
// 多线程环境下确保只创建一次
List<String> list2 = map.computeIfAbsent("key2", k -> {
System.out.println("只调用一次");
return new CopyOnWriteArrayList<>();
});
源码分析
public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
if (mappingFunction == null)
throw new NullPointerException();
// 计算hash
int h = spread(key.hashCode());
V val = null;
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 1. 初始化表
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// 2. 空桶,直接CAS插入
else if ((f = tabAt(tab, i = (n - 1) & h)) == null) {
// 关键:创建新节点
Node<K,V> r = new ReservationNode<K,V>();
synchronized (r) { // 锁住占位节点
if (casTabAt(tab, i, null, r)) {
binCount = 1;
Node<K,V> node = null;
try {
// 在锁保护下调用mappingFunction
val = mappingFunction.apply(key);
if (val != null) // 允许存储null(ConcurrentHashMap不允许null值)
node = new Node<K,V>(h, key, val, null);
} finally {
// 无论成功失败,都要设置最终节点
setTabAt(tab, i, node);
}
}
}
if (binCount != 0)
break;
}
// 3. 帮助扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
// 4. 已有数据,加锁处理
else {
boolean added = false;
synchronized (f) {
if (tabAt(tab, i) == f) {
// 链表或红黑树处理
for (Node<K,V> e = f; ; ++binCount) {
K ek; V ev;
if (e.hash == h &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
val = e.val; // key已存在,返回旧值
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
// 调用函数计算新值
val = mappingFunction.apply(key);
if (val != null) {
pred.next = new Node<K,V>(h, key, val, null);
added = true;
}
break;
}
}
}
}
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (added)
break;
}
}
// 根据情况决定是否扩容
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
}
return val;
}
核心特点
- 原子懒计算:Function只在key不存在时调用
- 双重检查:先快速检查,再加锁确认,避免重复计算
- 占用节点:使用
ReservationNode占位,防止并发计算
性能优势
// 场景:多线程环境下需要初始化复杂的共享资源
// ❌ 使用synchronized锁整个map
synchronized(map) {
if (!map.containsKey("cache")) {
map.put("cache", createExpensiveCache()); // 所有线程串行
}
}
// ✓ 使用computeIfAbsent,细粒度锁
map.computeIfAbsent("cache", k -> createExpensiveCache());
// 只有计算该key的线程互斥,不同key可以并行计算
重要细节
⚠️ Function内部的异常会传播,且不会留下残留数据:
try {
map.computeIfAbsent("key", k -> {
// 如果这里抛出异常
if (someCondition) {
throw new RuntimeException("计算失败");
}
return new Value();
});
} catch (Exception e) {
// map中不会插入该key,也没有残留数据
e.printStackTrace();
}
适用场景
✅ 适合场景:
- 懒初始化:只在需要时创建对象
- 多例缓存:不同key对应不同的计算结果
- 集合容器:如
Map<String, List<V>>中初始化List - 昂贵计算:避免重复计算耗时操作
经典案例:
// 多值分组:String -> List<User>
ConcurrentHashMap<String, List<User>> userByCity = new ConcurrentHashMap<>();
void addUser(String city, User user) {
userByCity.computeIfAbsent(city, k -> new CopyOnWriteArrayList<>())
.add(user);
}
四、compute方法:原子读取并计算
基本用法
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("count", 0);
// 原子递增
map.compute("count", (key, oldValue) -> oldValue == null ? 1 : oldValue + 1);
// 原子更新复杂对象
map.compute("config", (key, oldConfig) -> {
if (oldConfig == null) {
return new Config();
}
oldConfig.update();
return oldConfig;
});
源码分析
public V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
if (remappingFunction == null)
throw new NullPointerException();
int h = spread(key.hashCode());
V val = null;
int delta = 0;
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & h)) == null) {
Node<K,V> r = new ReservationNode<K,V>();
synchronized (r) {
if (casTabAt(tab, i, null, r)) {
binCount = 1;
Node<K,V> node = null;
try {
// 调用remappingFunction,oldValue为null
val = remappingFunction.apply(key, null);
if (val != null)
node = new Node<K,V>(h, key, val, null);
} finally {
setTabAt(tab, i, node);
}
}
}
if (binCount != 0)
break;
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
synchronized (f) {
if (tabAt(tab, i) == f) {
for (Node<K,V> e = f; ; ++binCount) {
K ek; V ev;
if (e.hash == h &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
// key已存在,传入旧值
val = remappingFunction.apply(key, e.val);
if (val != null)
e.val = val;
else // 返回null则删除节点
e.val = null;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
// key不存在,oldValue为null
val = remappingFunction.apply(key, null);
if (val != null) {
pred.next = new Node<K,V>(h, key, val, null);
}
break;
}
}
}
}
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (binCount != 0)
break;
}
}
if (delta != 0)
addCount((long)delta, binCount);
return val;
}
核心特点
- 原子读写:读取旧值并计算新值,整个过程是原子的
- 可以删除:Function返回
null时会删除该key - 获取旧值:Function接收旧值(或null),支持基于旧值的计算
典型应用场景
1. 原子计数器
ConcurrentHashMap<String, AtomicInteger> map = new ConcurrentHashMap<>();
// ❌ 错误:非原子操作
if (map.containsKey("counter")) {
map.get("counter").incrementAndGet();
} else {
map.put("counter", new AtomicInteger(1));
}
// ✓ 正确:使用compute
map.compute("counter", (k, v) -> {
if (v == null) {
return new AtomicInteger(1);
}
v.incrementAndGet();
return v;
});
2. 复杂对象更新
// 场景:更新用户统计信息
ConcurrentHashMap<String, UserStats> statsMap = new ConcurrentHashMap<>();
statsMap.compute(userId, (id, stats) -> {
if (stats == null) {
stats = new UserStats();
}
stats.incrementLoginCount();
stats.setLastLoginTime(System.currentTimeMillis());
return stats;
});
3. 条件删除
// 当计数归零时自动删除
map.compute(key, (k, count) -> {
if (count == null || count <= 1) {
return null; // 删除
}
return count - 1;
});
性能考量
compute的性能相对较低,因为:
- Function调用总是会发生
- 无法像
computeIfAbsent那样快速跳过已存在的key - 需要持有锁的时间更长
优化建议:只在真正需要基于旧值计算时使用。
五、方法选择决策树
需要写入ConcurrentHashMap
│
├─ 是否需要基于旧值计算?
│ ├─ 是 → compute
│ └─ 否 ↓
│
├─ value是否需要懒计算?
│ ├─ 是 → computeIfAbsent
│ └─ 否 ↓
│
├─ 是否需要防止覆盖已存在的值?
│ ├─ 是 → putIfAbsent
│ └─ 否 → put
快速参考表
| 需求 | 推荐方法 | 替代方案 |
|---|---|---|
| 简单覆盖 | put | - |
| 防重复插入(value已存在) | putIfAbsent | computeIfAbsent |
| 懒初始化(按需计算) | computeIfAbsent | - |
| 基于旧值更新 | compute | - |
| 条件删除 | compute返回null | remove(key, value) |
| 原子计数 | compute | LongAdder(单key场景) |
六、实战案例分析
案例1:多级缓存实现
public class MultiLevelCache {
private final ConcurrentHashMap<String, CacheEntry> cache = new ConcurrentHashMap<>();
// ✓ 正确:使用computeIfAbsent实现懒加载
public Object get(String key) {
return cache.computeIfAbsent(key, k -> {
// 从数据库加载
Object value = loadFromDatabase(k);
return new CacheEntry(value, System.currentTimeMillis());
}).getValue();
}
// ✓ 正确:使用compute实现原子更新
public void refresh(String key) {
cache.compute(key, (k, entry) -> {
if (entry == null || entry.isExpired()) {
Object newValue = loadFromDatabase(k);
return new CacheEntry(newValue, System.currentTimeMillis());
}
return entry; // 未过期则不更新
});
}
}
案例2:并发计数器
public class ConcurrentCounter {
private final ConcurrentHashMap<String, LongAdder> counters = new ConcurrentHashMap<>();
// ✓ 使用computeIfAbsent初始化,LongAdder高效计数
public void increment(String key) {
counters.computeIfAbsent(key, k -> new LongAdder()).increment();
}
public long getCount(String key) {
LongAdder adder = counters.get(key);
return adder == null ? 0 : adder.sum();
}
}
案例3:分组操作
public class GroupingExample {
private final ConcurrentHashMap<String, Set<String>> groups = new ConcurrentHashMap<>();
// ✓ 正确:原子地添加到分组
public void addToGroup(String group, String item) {
groups.computeIfAbsent(group, g -> ConcurrentHashMap.newKeySet())
.add(item);
}
// ✓ 正确:原子地从分组移除
public void removeFromGroup(String group, String item) {
groups.computeIfPresent(group, (g, set) -> {
set.remove(item);
return set.isEmpty() ? null : set; // 空集合则删除
});
}
}
案例4:配置热更新
public class ConfigManager {
private final ConcurrentHashMap<String, Config> configs = new ConcurrentHashMap<>();
// ✓ 使用compute实现CAS更新
public boolean updateConfig(String key, int newVersion, Config newConfig) {
AtomicBoolean updated = new AtomicBoolean(false);
configs.compute(key, (k, oldConfig) -> {
if (oldConfig != null && oldConfig.getVersion() >= newVersion) {
return oldConfig; // 版本不够新,不更新
}
updated.set(true);
return newConfig;
});
return updated.get();
}
}
七、常见陷阱与避坑指南
陷阱1:computeIfAbsent的死锁风险
// ❌ 危险:在Function中访问同一map的不同key
map.computeIfAbsent("key1", k -> {
return map.get("key2"); // 可能死锁!
});
// ✓ 正确:避免嵌套访问
Object value2 = map.get("key2");
map.computeIfAbsent("key1", k -> process(value2));
陷阱2:compute返回null导致数据丢失
// ❌ 意外删除数据
map.compute("key", (k, v) -> {
if (shouldUpdate(v)) {
return update(v);
}
return null; // 忘记返回v,导致key被删除!
});
// ✓ 正确:明确所有分支
map.compute("key", (k, v) -> {
if (shouldUpdate(v)) {
return update(v);
}
return v; // 保持原值
});
陷阱3:Function抛异常导致数据残留
虽然computeIfAbsent和compute在异常时不会留下占位节点,但不完整的数据结构可能已经创建:
// ⚠️ 注意:Function内部的状态修改不会回滚
map.computeIfAbsent("key", k -> {
ComplexObject obj = new ComplexObject();
obj.setState1(); // 已执行
if (someError) {
throw new RuntimeException();
}
obj.setState2(); // 未执行
return obj;
});
建议:Function内部尽量保持幂等和可重入。
陷阱4:忽视ConcurrentHashMap的null限制
// ❌ 错误:ConcurrentHashMap不允许null值
map.put("key", null); // 抛出NPE
map.computeIfAbsent("key", k -> null); // 不会插入,但不会报错
// ✓ 正确:使用Optional或哨兵值
map.put("key", Optional.empty());
map.computeIfAbsent("key", k -> Optional.ofNullable(value));
八、总结
本文深入分析了ConcurrentHashMap四个核心写入方法的差异和应用场景:
核心要点回顾
- put:简单覆盖,适合不需要原子检查的场景
- putIfAbsent:原子防重复,注意value会立即求值
- computeIfAbsent:原子懒计算,适合昂贵的初始化操作
- compute:原子读写更新,适合基于旧值的复杂逻辑
最佳实践
- 优先简单:能用
put解决的不用复杂方法 - 按需选择:根据是否需要原子性、是否需要懒计算选择合适方法
- 注意性能:
compute系列方法有额外开销,避免滥用 - 防止死锁:不要在Function中嵌套访问同一Map
- 异常处理:Function内部尽量保持幂等
学习建议
- 实践优先:在真实项目中尝试不同方法,观察性能差异
- 阅读源码:理解底层实现有助于做出正确选择
- 性能测试:对关键路径进行基准测试,用数据说话
- 团队规范:制定统一的Map使用规范,避免误用
如果你在实践过程中遇到问题,或者有更好的使用场景,欢迎在评论区分享!
如果本文对你有帮助,别忘了点赞收藏关注~