ConcurrentHashMap四大方法深度解析:put、putIfAbsent、computeIfAbsent、compute

引言

在日常开发中,你一定用过ConcurrentHashMap,但在选择写入方法时是否犯过难?

  • 为什么putputIfAbsent看起来很像,但适用场景完全不同?
  • computeIfAbsentcompute到底解决了什么痛点?
  • 多线程环境下,这些方法的原子性保证有何区别?

本文将从源码层面深入剖析这四个方法的实现原理、适用场景和性能差异,帮助你掌握并发场景下的最佳实践。阅读本文,你将学到:

  • 四个方法的底层实现机制和原子性差异
  • 不同业务场景下的方法选择策略
  • 避免并发陷阱的实战经验
  • 性能优化的关键技巧

背景介绍

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;
}

核心特点

  1. 非原子操作put本身是线程安全的,但"检查再操作"模式需要额外同步
  2. 直接覆盖onlyIfAbsent=false,无论key是否存在都会覆盖
  3. 返回值:返回旧值,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已存在时不覆盖

核心特点

  1. 原子性:检查和插入是原子操作,不会被其他线程打断
  2. 不覆盖:key已存在时保留旧值
  3. 返回值:返回关联的旧值,不存在则返回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;
}

核心特点

  1. 原子懒计算:Function只在key不存在时调用
  2. 双重检查:先快速检查,再加锁确认,避免重复计算
  3. 占用节点:使用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;
}

核心特点

  1. 原子读写:读取旧值并计算新值,整个过程是原子的
  2. 可以删除:Function返回null时会删除该key
  3. 获取旧值: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已存在)putIfAbsentcomputeIfAbsent
懒初始化(按需计算)computeIfAbsent-
基于旧值更新compute-
条件删除compute返回nullremove(key, value)
原子计数computeLongAdder(单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抛异常导致数据残留

虽然computeIfAbsentcompute在异常时不会留下占位节点,但不完整的数据结构可能已经创建:

// ️ 注意: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四个核心写入方法的差异和应用场景:

核心要点回顾

  1. put:简单覆盖,适合不需要原子检查的场景
  2. putIfAbsent:原子防重复,注意value会立即求值
  3. computeIfAbsent:原子懒计算,适合昂贵的初始化操作
  4. compute:原子读写更新,适合基于旧值的复杂逻辑

最佳实践

  • 优先简单:能用put解决的不用复杂方法
  • 按需选择:根据是否需要原子性、是否需要懒计算选择合适方法
  • 注意性能compute系列方法有额外开销,避免滥用
  • 防止死锁:不要在Function中嵌套访问同一Map
  • 异常处理:Function内部尽量保持幂等

学习建议

  1. 实践优先:在真实项目中尝试不同方法,观察性能差异
  2. 阅读源码:理解底层实现有助于做出正确选择
  3. 性能测试:对关键路径进行基准测试,用数据说话
  4. 团队规范:制定统一的Map使用规范,避免误用

如果你在实践过程中遇到问题,或者有更好的使用场景,欢迎在评论区分享!

如果本文对你有帮助,别忘了点赞收藏关注~

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:alixiixcom@163.com