ConcurrentHashMap 源码解析(JDK8)高并发哈希表的终极实现
一、先一句话抓住核心(JDK7 vs JDK8)
要深入理解JDK8 ConcurrentHashMap的精妙设计,必须从其演进历程入手。
免费影视、动漫、音乐、游戏、小说资源长期稳定更新! 👉 点此立即查看 👈
在JDK7中,其核心设计是“分段锁”(Segment Locking):将整个哈希表划分为多个独立的Segment,每个Segment独立加锁。这种设计虽然比Hashtable的全局锁粒度更细,提升了部分并发能力,但锁的粒度依然是Segment级别,并发度受限于Segment数量,在高并发场景下仍有瓶颈。
JDK8的设计理念发生了革命性转变:数据结构回归经典的数组+链表/红黑树组合;锁机制则升级为CAS无锁算法配合synchronized锁定单个哈希桶(桶的头节点)。锁的粒度被极致细化至单个桶。这一改变带来了并发性能的质的飞跃。
因此,核心总结是:JDK8 ConcurrentHashMap彻底摒弃了分段锁,采用更先进的桶级别锁与无锁CAS操作相结合的设计,是真正为现代高并发应用而生的数据结构。

二、JDK8 ConcurrentHashMap 核心结构
深入源码,其核心数据结构清晰而高效:
public class ConcurrentHashMap {
// 核心哈希表数组,volatile保证引用可见性
transient volatile Node[] table;
// 链表转换为红黑树的阈值
static final int TREEIFY_THRESHOLD = 8;
// 红黑树退化为链表的阈值
static final int UNTREEIFY_THRESHOLD = 6;
// 允许进行树化的最小表容量
static final int MIN_TREEIFY_CAPACITY = 64;
// 核心控制变量,控制初始化、扩容与计数
transient volatile int sizeCtl;
}
三、核心 Node 节点(JDK8 真实结构)
底层存储单元Node节点的定义,是支撑高并发读写的基础:
static class Node implements Map.Entry {
final int hash;
final K key;
volatile V val;
volatile Node next;
// ...
}
这里有三个至关重要的设计要点:
首先,table数组本身被volatile关键字修饰,这确保了数组引用本身的可见性,任何线程都能立即感知到数组的扩容或重建。
其次,Node节点中的val(值)和next(下一个节点引用)也被声明为volatile,这为完全无锁的读操作奠定了内存语义基础。
最后,sizeCtl这个变量是整个ConcurrentHashMap的“控制中枢”。它身兼多职:既是表初始化的控制标志,又是扩容的触发阈值和协调器。透彻理解sizeCtl,就掌握了ConcurrentHashMap并发逻辑的核心。
四、核心流程:put 方法源码(最精简准确版)
理论结合实践,下面这个精简的putVal流程,涵盖了所有关键并发场景:
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null)
throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node[] tab = table;;) {
Node 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<>(hash, key, value, null)))
break;
}
// 场景3:桶头节点为特殊标记(MOVED)→ 正在扩容,协助扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
// 场景4:发生哈希冲突 → 锁住桶头节点进行处理
else {
V oldVal = null;
synchronized (f) { // 锁粒度仅为当前桶!
if (tabAt(tab, i) == f) {
if (fh >= 0) {
// 在链表中插入或更新
} else {
// 在红黑树中插入或更新
}
}
}
}
}
addCount(1L, binCount);
return oldVal;
}
掌握此流程,足以应对大部分面试提问。它主要执行以下几步:
第一步,参数校验。key和value均不允许为null,这是保证并发环境下语义清晰性的铁律。
第二步,计算哈希。通过spread方法对键的hashCode进行二次扰动,使哈希分布更均匀,减少冲突。
第三步,进入自旋主循环。处理四个核心分支:若数组未初始化,则进行初始化;若目标桶为空,则使用CAS操作无锁插入新节点,这是性能最优路径;若检测到桶头节点为特殊转发节点(MOVED),说明正处于扩容阶段,当前线程会协助进行数据迁移;若发生常规哈希冲突,则使用synchronized锁住该桶的头节点,然后在链表或红黑树中执行插入或更新操作。
第四步,后续处理。成功插入后,通过addCount增加元素计数,并检查是否触发扩容。若链表长度达到阈值(8)且表容量达到最小树化容量(64),则将链表转换为红黑树以优化查询性能。
五、为什么 JDK8 这么强?
分析完核心流程,其强大之处可归纳为以下四点:
锁粒度极致细化。 仅锁定发生冲突的单个哈希桶,其他桶的读写操作完全不受影响,并行度达到理论最高。
无冲突则无锁。 在大多数情况下(桶为空),直接使用CAS操作完成插入,性能开销与HashMap几乎无异。
多线程协同扩容。 这是JDK8设计的精髓。扩容时,数据迁移任务被拆分为多个小任务(桶区间),允许多个线程并发参与迁移工作,极大提升了扩容效率,避免了长时间阻塞。
自适应数据结构。 根据冲突程度,在链表和红黑树之间自动转换。在遭遇极端哈希冲突或攻击时,能将操作时间复杂度从O(n)降至O(log n),保障了性能下限。
六、必须搞懂的关键细节(面试高频)
掌握宏观设计后,以下细节是区分“了解”与“精通”的关键。
1. 为什么 key 和 value 不能为 null?
HashMap允许存储null键和null值,但ConcurrentHashMap明确禁止。根本原因在于并发语义的清晰性:在并发环境中,如果允许null值,当调用get(key)返回null时,无法区分是该key不存在,还是该key对应的value本身就是null。这种二义性会破坏线程安全的约定,因此采取最严格策略,禁止null。
2. get 方法真・完全无锁
这是ConcurrentHashMap读性能极高的根本。get操作全程无需任何锁。其安全性依赖于volatile的内存可见性保证:table引用、Node的val和next均为volatile。只要写操作遵循规则更新这些volatile变量,读线程就能安全地看到最新结果。tabAt方法保证了读取桶头节点的原子性。
3. sizeCtl 的 4 种含义(超级高频)
此变量是理解整个Map并发控制的总开关,其值代表四种不同状态:
等于 -1:表示哈希表正在初始化中。
小于 -1:表示正在扩容。其数值的低位记录着正在参与扩容的线程数量。
等于 0:表示创建时未指定初始容量,将使用默认容量。
大于 0:这是最常见状态,表示下一次触发扩容的阈值(当前容量 * 负载因子,默认为0.75)。
4. 为什么链表长度 ≥8 才树化?
这个阈值基于严格的概率统计(泊松分布)。在理想的哈希函数下,单个桶中链表长度达到8的概率极低(约为千万分之六)。如果达到此长度,极有可能遇到了哈希碰撞攻击,或是hashCode实现质量极差。此时将链表转为红黑树,是为了在极端情况下将操作复杂度从O(n)降为O(log n),保障性能。
5. 树化必须满足两个条件
链表长度≥8仅是必要条件之一。另一个条件是当前哈希表的容量必须达到MIN_TREEIFY_CAPACITY(默认64)。如果表容量还很小,优先选择扩容(resize)来分散节点,而不是立即树化。因为扩容能从根源上减少冲突,而树化会引入额外的内存和性能开销。
6. 扩容机制(JDK8 灵魂)
扩容是ConcurrentHashMap并发设计的集中体现。触发条件是元素数量达到阈值(sizeCtl)。新容量为旧容量的两倍。迁移过程采用“分而治之”策略:将旧数组划分为多个迁移区间(stride),每个线程负责迁移一个区间内的桶。迁移过程中,已处理桶的头节点会被替换为一个特殊的ForwardingNode(其hash值为MOVED),其他线程在读写时遇到此标记,会主动加入协助迁移。直至所有数据迁移完成,才会将引用指向新table。整个过程高效、协同,避免了单线程扩容的瓶颈。
7. size () 是弱一致性
size()方法返回的数值不是实时精确值。因为CHM并未为了获取一个绝对准确的计数而全局加锁,那样代价过高。它是通过累加各个分段计数单元(CounterCell)的值来估算的。因此,在高并发场景下,它提供的是一个弱一致性的视图。若业务仅需大致数量,size()完全适用;若强依赖精确计数,则需考虑其他方案。官方推荐使用mappingCount()方法,它返回long类型,更不易溢出。
七、JDK7 vs JDK8 一张表总结
(此处保留原文结构,内容已整合至第一部分“先一句话抓住核心”)
八、总结(可直接背)
总而言之,JDK8的ConcurrentHashMap通过一系列精妙绝伦的设计,实现了业界领先的高并发性能:它摒弃了分段锁,采用数组+链表/红黑树的基础结构;读操作完全无锁,写操作在无冲突时使用CAS,有冲突时仅锁住单个桶;扩容过程支持多线程协同工作,极大提升了效率。同时,通过禁止null键值保证了并发语义的清晰性,通过弱一致性的size()优先保障了性能。
因此,在高并发场景下的选择非常明确:首选ConcurrentHashMap。而传统的HashMap(非线程安全)或Hashtable(性能低下)已不在考虑之列。其设计思想,堪称Java并发编程与数据结构结合的典范之作。
相关攻略
一、进程是什么:不只是 "一个程序 " 教科书上那句“进程是程序的一次执行”,听起来总有点隔靴搔痒,不够透彻。 在内核的视角里,事情要具体得多。一个进程,本质上就是一个名为 task_struct 的结构体。你可以把它想象成一张记录了这个执行单元所有家当的“户口本”或“档案表”。里面都记了些什么呢? 进
要查看系统处理器信息,psrinfo(1M)命令是标准工具。不过,在多内核与多线程技术普及的今天,直接运行命令看到的可能并非物理CPU的实际数量。这时,加上-vp参数就非常关键了。 来看一个典型的例子: psrinfo -vp The physical processor has 32 virt
一、先一句话抓住核心(JDK7 vs JDK8) 要深入理解JDK8 ConcurrentHashMap的精妙设计,必须从其演进历程入手。 在JDK7中,其核心设计是“分段锁”(Segment Locking):将整个哈希表划分为多个独立的Segment,每个Segment独立加锁。这种设计虽然比H
IT之家 2 月 26 日消息,机械革命现已官宣 2026 款耀世 16 Pro 游戏本,该机将于 2 月 26 日开启预售,3 月 1 日正式开售,首发价 8699 元。该机整体延续机械革命家族式
IT之家 2 月 12 日消息,2 月 11 日,智谱正式发布新一代大模型 GLM-5。摩尔线程基于 SGLang 推理框架,在旗舰级 AI 训推一体全功能 GPU MTT S5000 上,Day-
热门专题
热门推荐
《字里人间》第二关全方位图文攻略:掌握核心技巧与观察秘诀 欢迎来到《字里人间》第二关。这一关卡的挑战与乐趣,常常隐藏在看似平常的汉字布局之中,需要玩家充分调动“文本洞察力”与“逻辑联想能力”。接下来的内容,将为您提供一套完整且高效的闯关思路。 字里人间第二关通关攻略详解 成功通过此关卡的秘诀,始于“
Binance币安 欧易OKX ️ Huobi火币️ gateio芝麻 很多交易者都遇到过这样的困惑:明明刚按下开仓键,账户却立刻显示浮亏。这并非系统错误,而是滑点与点差在成交瞬间共同作用的结果。简单来说,这两者并非独立事件,而是共同构成了你实际入场成本的核心部分。 一、点差导致开仓即亏损的机制 点
2026年白银投资深度解析:工业与金融双引擎驱动下的领跑机遇 在2026年的全球资产版图中,白银正从贵金属家族的“配角”跃升为最受瞩目的明星。其价格表现所展现出的惊人弹性,并非偶然,而是工业需求结构性爆发与金融属性周期性强化共同作用的结果。本文将深入剖析光伏与AI产业对白银的刚性消耗、金银比值的修复
CentOS下载渠道与版本选择对于初次接触CentOS的用户而言,获取官方系统镜像文件是首要步骤。CentOS项目官网是最权威、最可靠的下载来源。用户应访问其官方镜像列表或下载页面,根据实际应用场景选择最合适的版本。当前,CentOS主要提供两大分支:经典的CentOS Linux和面向未来的Cen
CentOS系统镜像获取全渠道详解与选择指南对于寻求构建稳定服务器环境的用户来说,获取正确的CentOS系统镜像是首要且关键的一步。随着CentOS项目战略的调整,其镜像的获取来源也变得更加多样化。当前,用户主要可以从CentOS官方历史存档、国内外各大开源镜像站,以及活跃的替代发行版社区等渠道进行





