导读:本期聚焦于小伙伴创作的《Java中ConcurrentHashMap如何保证线程安全?深入解析并发映射机制》,敬请观看详情。为什么高并发场景下用HashMap会丢数据甚至死循环,而ConcurrentHashMap却能稳稳支撑数千线程同时读写?它的底层并不是简单加一把全局锁。在JDK 1.7里,它采用分段锁思路,把数据切到多个Segment中,每个Segment独立加锁,写操作只阻塞当前段。到了JDK 1.8,实现改为数组加链表加红黑树,并用CAS与synchronized锁定单个桶头节点,粒度更细。本文讲清size统计、扩容迁移以及弱一致性迭代器的设计,帮你避开误用computeIfAbsent造成的死锁坑,写出真正安全的并发缓存代码。

ConcurrentHashMap是Java并发包里最常用的线程安全映射实现。它不像Collections.synchronizedMap那样给整个表加锁,而是通过更细粒度的控制,在多线程读写时既保证数据一致性,又维持较高吞吐量。理解它的安全机制,对写高并发缓存、计数器、会话存储非常关键。

Java中ConcurrentHashMap如何保证线程安全?深入解析并发映射机制

JDK 1.7中的分段锁设计

在JDK 1.7及更早版本中,ConcurrentHashMap内部由多个Segment组成,每个Segment本质是一个可重入锁(继承ReentrantLock)并持有一个HashEntry数组。整体结构相当于把一张大表分成若干子表,写操作只需要获取对应Segment的锁,不同Segment之间的读写互不阻塞。

这种设计降低了锁竞争概率。例如,线程A修改Segment 1,线程B修改Segment 2,两者可完全并行。但如果多个线程恰好哈希到同一个Segment,仍然会串行执行。下面是简化版的Segment结构示意:

// JDK 1.7简化示意,非完整源码
static final class Segment<K,V> extends ReentrantLock {
    transient volatile HashEntry<K,V>[] table;
    // put方法先尝试获取锁
    final V put(K key, int hash, V value, boolean onlyIfAbsent) {
        if (!tryLock()) {
            // 未获取到锁时可能自旋或阻塞
            lock();
        }
        try {
            // 实际写入逻辑
        } finally {
            unlock();
        }
    }
}

分段锁的缺点是默认并发度固定(如16个Segment),若业务哈希分布极不均匀,部分Segment会成为热点。此外,size()方法需要尝试不加锁统计,失败后再加锁,有一定开销。

JDK 1.8的Node数组与CAS加锁

JDK 1.8彻底废弃Segment,改为与HashMap类似的数组加链表加红黑树结构。线程安全依靠三个手段:volatile修饰的Node数组与节点值保证可见性;CAS操作处理空桶插入;对非空桶头节点使用synchronized块锁定,粒度缩小到单个哈希桶。

当发生哈希冲突且桶首个节点已存在时,ConcurrentHashMap会对该头节点对象加锁,再执行链表或红黑树的修改。由于不同桶的头节点不同,绝大多数写操作不会互相阻塞。以下代码展示putVal核心思路:

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    // 省略部分变量
    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) & hash)) == null) {
            // 空桶用CAS插入
            if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
                break;
        }
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            synchronized (f) { // 锁定桶头节点
                if (tabAt(tab, i) == f) {
                    // 链表或树插入逻辑
                }
            }
        }
    }
    return null;
}

相比分段锁,1.8方案在高并发下表现更好,因为锁的粒度随哈希分布自然分散。同时,扩容时采用多线程协同迁移(helpTransfer),加快大表重建速度。但需注意,synchronized虽经锁升级优化,若某桶链表过长且未转树,仍可能短暂停顿。

size统计与弱一致性迭代

ConcurrentHashMap的size()和mappingCount()不返回精确瞬态值,而是基于分桶计数器(CounterCell)累加的近似结果。这是因为在高并发下维持全局精确计数需要昂贵同步,而多数业务只关心量级。下面代码说明计数器使用:

// 累加各CounterCell与baseCount
public int size() {
    long n = sumCount();
    return ((n < 0L) ? 0 : (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int)n);
}

迭代器被设计为弱一致性:创建后,原表的增删改可能不会完全反映,但不会抛出ConcurrentModificationException。这对避免遍历时锁住全表很重要。若业务必须强一致快照,应考虑复制或加外部锁,而非依赖迭代器。

常见误用与注意事项

computeIfAbsent在1.8里若映射函数又调用同一表的其他compute方法,可能在特定递归场景造成死锁或阻塞,因为函数执行期间持有桶锁。因此映射函数应保持轻量,禁止在其中做远程调用或复杂递归写操作。

另外,不要用null作键或值,否则会抛NullPointerException。若要缓存“无值”状态,可用Optional或特殊哨兵对象。以下示例展示安全用法:

ConcurrentHashMap<String, String> cache = new ConcurrentHashMap<>();
// 正确:函数内仅做本地计算
String val = cache.computeIfAbsent("key", k -> "default-" + k.length());
// 错误示范:函数内再写同一map可能导致问题
// cache.computeIfAbsent("a", k -> cache.computeIfAbsent("b", x -> "bad"));

总之,ConcurrentHashMap通过细粒度锁、CAS与volatile语义,在安全和性能间取得平衡。选型时若需跨节点事务或精确顺序,它并不适用,应转向外部协调组件。

ConcurrentHashMapJava并发分段锁修改时间:2026-08-05 00:12:45

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。