星云点击:星空遥控器
120.47M · 2026-02-04
在日常开发中,你一定用过ConcurrentHashMap,但在选择写入方法时是否犯过难?
put和putIfAbsent看起来很像,但适用场景完全不同?computeIfAbsent和compute到底解决了什么痛点?本文将从源码层面深入剖析这四个方法的实现原理、适用场景和性能差异,帮助你掌握并发场景下的最佳实践。阅读本文,你将学到:
ConcurrentHashMap作为Java并发包中的核心类,提供了多种写入方法。这些方法虽然看似相似,但在并发安全性、原子性保证、返回值语义上存在本质差异。
在多线程环境下,错误选择方法可能导致:
理解这些方法的差异,是写出高质量并发代码的基础。
先通过一张表快速了解四个方法的核心差异:
| 方法 | 原子性 | 返回值 | 适用场景 | 线程安全 |
|---|---|---|---|---|
put | 无 | 旧值或null | 简单覆盖 | |
putIfAbsent | 原子检查并插入 | 旧值或null | 防重复插入 | |
computeIfAbsent | 原子检查并计算 | 新值 | 懒初始化 | |
compute | 原子读取并计算 | 新值 | 复杂更新逻辑 |
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是否存在都会覆盖null适合场景:
不适合场景:
// 错误:非原子的"检查再操作"
if (!map.containsKey("key")) {
map.put("key", computeExpensiveValue()); // 可能重复计算
}
// 正确:使用putIfAbsent或computeIfAbsent
map.putIfAbsent("key", computeExpensiveValue());
// 或
map.computeIfAbsent("key", k -> computeExpensiveValue());
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已存在时不覆盖。
null️ 重要:`value参数总是会被求值**,即使最终不会插入!
// 性能陷阱:expensiveValue()总是会被调用
map.putIfAbsent("key", expensiveComputation());
// 正确:使用computeIfAbsent实现懒计算
map.computeIfAbsent("key", k -> expensiveComputation());
适合场景:
put的原子替代不适合场景:
computeIfAbsent)compute)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;
}
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();
}
适合场景:
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);
}
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;
}
null时会删除该keyConcurrentHashMap<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;
});
// 场景:更新用户统计信息
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;
});
// 当计数归零时自动删除
map.compute(key, (k, count) -> {
if (count == null || count <= 1) {
return null; // 删除
}
return count - 1;
});
compute的性能相对较低,因为:
computeIfAbsent那样快速跳过已存在的key优化建议:只在真正需要基于旧值计算时使用。
需要写入ConcurrentHashMap
│
├─ 是否需要基于旧值计算?
│ ├─ 是 → compute
│ └─ 否 ↓
│
├─ value是否需要懒计算?
│ ├─ 是 → computeIfAbsent
│ └─ 否 ↓
│
├─ 是否需要防止覆盖已存在的值?
│ ├─ 是 → putIfAbsent
│ └─ 否 → put
| 需求 | 推荐方法 | 替代方案 |
|---|---|---|
| 简单覆盖 | put | - |
| 防重复插入(value已存在) | putIfAbsent | computeIfAbsent |
| 懒初始化(按需计算) | computeIfAbsent | - |
| 基于旧值更新 | compute | - |
| 条件删除 | compute返回null | remove(key, value) |
| 原子计数 | compute | LongAdder(单key场景) |
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; // 未过期则不更新
});
}
}
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();
}
}
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; // 空集合则删除
});
}
}
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();
}
}
// 危险:在Function中访问同一map的不同key
map.computeIfAbsent("key1", k -> {
return map.get("key2"); // 可能死锁!
});
// 正确:避免嵌套访问
Object value2 = map.get("key2");
map.computeIfAbsent("key1", k -> process(value2));
// 意外删除数据
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; // 保持原值
});
虽然computeIfAbsent和compute在异常时不会留下占位节点,但不完整的数据结构可能已经创建:
// ️ 注意:Function内部的状态修改不会回滚
map.computeIfAbsent("key", k -> {
ComplexObject obj = new ComplexObject();
obj.setState1(); // 已执行
if (someError) {
throw new RuntimeException();
}
obj.setState2(); // 未执行
return obj;
});
建议:Function内部尽量保持幂等和可重入。
// 错误: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解决的不用复杂方法compute系列方法有额外开销,避免滥用如果你在实践过程中遇到问题,或者有更好的使用场景,欢迎在评论区分享!
如果本文对你有帮助,别忘了点赞收藏关注~