一、Node 内部类
ConcurrentHashMap 内部有一个内部类 Node,实现了 Map.Entry 接口,作为 HashMap 内部的链表节点。
核心特性
- hashCode :
key.hashCode() ^ val.hashCode() - toString :
key + "=" + val
源码实现
java
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
Node(int hash, K key, V val, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.val = val;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return val; }
public final int hashCode() { return key.hashCode() ^ val.hashCode(); }
public final String toString(){ return key + "=" + val; }
public final V setValue(V value) {
throw new UnsupportedOperationException();
}
}
二、带初始大小的构造函数
核心逻辑
- 参数校验:如果初始化数小于 0,抛出非法数字异常
- 容量上限 :如果初始大小大于最大限制长度的一半,就设置为
MAXIMUM_CAPACITY - 容量优化 :分两步理解
- 步骤一 :先计算「扩容后候选值」:
initialCapacity + (initialCapacity >>> 1) + 1initialCapacity >>> 1:右移 1 位等价于「整数除法 2」(例如initialCapacity=10,10>>>1=5)- 整体计算结果:
initialCapacity * 1.5 + 1(例如initialCapacity=10,10+5+1=16;initialCapacity=20,20+10+1=31) - 目的:预留充足空间,减少扩容次数
- 步骤二 :通过
tableSizeFor调整为「2 的幂」tableSizeFor是 ConcurrentHashMap 的核心工具方法,作用是:返回大于等于输入值的最小的 2 的幂(例如输入 16,输出 16;输入 31,输出 32;输入 58,输出 64)
- 步骤一 :先计算「扩容后候选值」:
为什么必须是 2 的幂?
- 哈希分布均匀 :底层哈希表的元素定位依赖「哈希值 & (容量 - 1)」(位运算替代取模,效率更高),而只有 2 的幂的「容量 - 1」是全 1 二进制(例如容量 16,
15=0b1111),能保证哈希值均匀分布,避免哈希冲突集中 - 并发扩容基础:2 的幂容量是 ConcurrentHashMap 并发扩容(分段扩容、transfer 机制)的基础
- 高效扩容 :HashMap 在自动扩容的时候,是按两倍进行扩容的,两倍扩容的好处是原来的索引在两倍扩容之后计算新的索引,新索引只会有两种情况:
- 一种是索引不变
- 一种是索引长度加原来 HashMap 的长度
- 数据迁移的时候,分割成两部分(红黑树或者子链)头结点拼接就好
- 判断索引长度是否变化也只需要执行一条位运算语句就行(
(hash & oldCap) == 0→ 不变,== 1→ 长度加oldCap)
源码实现
java
public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0)
throw new IllegalArgumentException();
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1))
? MAXIMUM_CAPACITY
: tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
this.sizeCtl = cap;
}
三、size() 方法
核心方法:sumCount()
sumCount() 是真正实现 HashMap 长度计算的关键方法。
源码实现
java
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 : (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int)n);
}
final long sumCount() {
// 1. 拿到分段计数器数组的引用(as 是局部变量,避免遍历中数组被修改的影响)
CounterCell[] as = counterCells;
// 2. 临时变量,用于遍历中存储单个 CounterCell
CounterCell a;
// 3. 初始化总和:先累加基础计数器 baseCount 的值
long sum = baseCount;
// 4. 若分段计数器数组不为 null(说明存在高并发竞争,有分散的计数)
if (as != null) {
// 5. 遍历所有分段计数器
for (int i = 0; i < as.length; ++i) {
// 6. 若当前分段计数器不为 null,累加其 value 值
if ((a = as[i]) != null)
sum += a.value;
}
}
// 7. 返回总和(baseCount + 所有 CounterCell.value 的和)
return sum;
}
四、get(key) 方法
实现说明
- 计算哈希值:获取传入参数 key 的 hashCode 值(进行二次 hash 减少碰撞)
- 前置校验:校验 HashMap 是否进行初始化,table 中是否有 hash 值相同的节点(若是用链表或者红黑树解决冲突,整个结构中的节点 hash 值都是一样的,所以返回头节点)
- 匹配逻辑 :
- 情况一:当前节点(头节点),通过引用地址判断和 equals 内容判断符合,返回当前节点的 val
- 情况二 :
hash < 0,说明当前结构是特殊数据结构:红黑树,调用对应的e.find()方法查找是否有符合的节点(同样是引用地址一致或者内容一致) - 情况三 :普通的拉链法解决的 hash 冲突,while 循环调用
next()进行判断 - 情况四:没有匹配的返回 null
源码实现
java
public V get(Object key) {
// 局部变量:tab=哈希表数组,e=当前遍历的节点,p=特殊节点的查找结果;n=数组长度,eh=当前节点的hash,ek=当前节点的key
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
// 1. 计算key的最终哈希值(二次哈希,减少碰撞)
int h = spread(key.hashCode());
// 2. 校验哈希表是否初始化、数组长度有效,且目标索引存在节点(核心前置判断)
if ((tab = table) != null && (n = tab.length) > 0 && (e = tabAt(tab, (n - 1) & h)) != null) {
// 3. 情况1:当前节点的hash与计算的hash完全匹配(直接命中候选节点)
if ((eh = e.hash) == h) {
// 对比key:先==(地址/基本类型)再equals(内容),兼顾性能和正确性
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val; // 命中,返回value
}
// 4. 情况2:当前节点是特殊节点(eh<0,hash为负数)
else if (eh < 0)
// 调用特殊节点的find方法查找(适配扩容、红黑树等场景)
return (p = e.find(h, key)) != null ? p.val : null;
// 5. 情况3:普通链表节点,循环遍历查找
while ((e = e.next) != null) {
// 对比hash(快速排除)和key(精确匹配)
if (e.hash == h && ((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
// 6. 未找到(table未初始化/索引无节点/遍历无匹配key)
return null;
}
五、containsKey() 方法
实现说明
containsKey() 的实现很简单,就是内部调用 get(key),要是返回结果不为 null 说明存在,返回 null 说明不存在,返回 false。
六、containsValue(Object value) 方法
背景铺垫
- value 的特性 :与 key 不同,value 没有哈希值,无法通过哈希计算直接定位,只能通过「全表扫描」遍历所有节点判断,因此时间复杂度为 O(n)(n 为总键值对数量),效率远低于
get(key)(O(1) 或 O(logn)) - 并发安全基础:延续 ConcurrentHashMap 的核心设计 ------ table 数组是 volatile、Node.val 是 volatile,确保遍历过程中能读取到最新的节点数据;特殊节点(如扩容标记 ForwardingNode、红黑树容器 TreeBin)需特殊处理,避免遍历遗漏或读取脏数据
- 遍历器 Traverser:ConcurrentHashMap 的内部专用遍历器,负责「安全遍历哈希表的所有节点」,自动适配链表、红黑树、扩容等场景,无需手动处理特殊节点逻辑
实现说明
- 参数校验 :
value == null是非法参数,ConcurrentHashMap 不允许 value 为 null,否则返回空指针异常 - 获取 Traverser:全局遍历器,会从 table 的 0 序号位开始,一直遍历到最后
- 遍历匹配 :使用
Traverser.advance()获取下一个节点,通过引用地址相同和 equals 内容相等进行判断
源码实现
java
public boolean containsValue(Object value) {
// 1. 非法参数校验:ConcurrentHashMap 不允许 value 为 null,直接抛空指针异常
if (value == null)
throw new NullPointerException();
// 2. 局部变量:t = 哈希表数组
Node<K,V>[] t;
// 3. 校验哈希表是否已初始化(未初始化则直接返回 false,无任何键值对)
if ((t = table) != null) {
// 4. 创建全表遍历器 Traverser:遍历范围是整个哈希表(从索引 0 到 t.length-1)
Traverser<K,V> it = new Traverser<K,V>(t, t.length, 0, t.length);
// 5. 循环遍历所有节点:it.advance() 获取下一个有效节点,直到返回 null(遍历结束)
for (Node<K,V> p; (p = it.advance()) != null; ) {
// 6. 局部变量:v = 当前节点的 value
V v;
// 7. value 匹配逻辑:先 ==(地址/基本类型)再 equals(内容),兼顾性能和正确性
if ((v = p.val) == value || (v != null && value.equals(v)))
return true; // 匹配成功,立即返回 true
}
}
// 8. 未找到匹配的 value(table 未初始化/遍历全表无匹配)
return false;
}
七、put() 方法
说明
onlyIfAbsent 作用:
true:只有当 key 不存在的时候才能够放入false:当 key 存在的时候会替换掉旧值
源码实现
java
public V put(K key, V value) {
// key、value、onlyIfAbsent
return putVal(key, value, false);
}
八、putVal() 方法
putVal(K key, V value, boolean onlyIfAbsent) 是 ConcurrentHashMap 中并发安全的键值对插入/更新核心方法(put、putIfAbsent 等公开方法均依赖此方法),核心目标是:
- 在高并发场景下,安全地向哈希表插入键值对,避免数据竞争和脏读/写
- 支持两种语义:
onlyIfAbsent=true时「不存在则插入」(不覆盖已有值),onlyIfAbsent=false时「存在则更新、不存在则插入」 - 自动处理哈希表初始化、并发扩容、链表红黑树转换等底层细节,兼顾并发安全性和操作效率
其设计核心是「细粒度锁(桶级锁)+ 无锁 CAS + 并发协作」:通过最小粒度的锁(仅锁定单个桶的头节点)减少锁竞争,空桶插入用 CAS 避免加锁,扩容时支持多线程协作,最大化并发性能。
背景铺垫
核心约束:
- key 和 value 均不允许为 null(与 HashMap 区别),避免并发场景下的歧义;ConcurrentHashMap 是线程安全的,在 JDK 1.8 之后使用了 synchronized 锁,持有锁的对象是 Map.Entry,要是允许 key 和 value 为 null,会出现并发歧义
锁机制:Java 8+ 取消分段锁(锁的粒度大,并发效率低),改用「桶级 synchronized 锁」(锁定每个桶的头节点),锁粒度更小,并发冲突概率更低
节点类型:
- 普通链表节点(Node):
hash >= 0,存储键值对和 next 指针 - 特殊节点:
hash < 0(MOVED=-1扩容标记、TreeBin=-2红黑树容器)
并发扩容 :支持多线程协同扩容(helpTransfer),避免单线程扩容瓶颈(导致扩容的线程,扩容过程中执行 put 方法的线程都加进来)
自适应结构:链表长度超过阈值(默认 8)且表容量 ≥ 64 时,自动转为红黑树(TreeBin),优化查询效率,要是 table 的大小没有达到 64,不会升级为红黑树,而是执行扩容。
实现说明
- 参数校验:key 或者 value 为 null,抛出 NullPointer 异常
- 计算哈希值 :获取 key 的二次哈希值,设置
binCount,作用是记录链表或者红黑树节点数,用于判断是否要考虑将当前的链表转为红黑树 - 死循环重试:进入一个死循环的 for,因为并发场景下可能会出现没有初始化,或者需要扩容后重新定位,链表升级为红黑树之后的重新定位,所以设计死循环,没有出口,直到数据存储成功了才会退出
- 四个场景处理 :
- 场景一 :table 为 null 或者 table 的长度为 0,通过
initTable()方法进行初始化(线程安全的初始化方法(CAS 控制,避免重复初始化)) - 场景二 :通过
tabAt方法获取桶列表(索引所在节点),要是为 null,说明该索引目前没有使用,不存在 hash 冲突和旧值,通过casTabAt方法插入新节点(目标桶为空(通过 CAS 原子插入节点,无锁操作(乐观锁操作)),CAS 插入新节点:对比tab[i]是否为 null,是则插入新 Node,成功则跳出循环,空桶插入无锁,性能最优) - 场景三 :目标桶的头节点是扩容标记(
MOVED=-1)协助扩容,else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f);// 当前线程参与扩容,扩容后重新获取 table 重试 - 场景四 :目标桶有节点(非空、非扩容)加锁插入/更新(桶级锁)
- 通过
synchronized (f),锁定当前桶的头节点 f,锁粒度为桶,不影响其他桶的操作,并发效率高 - 通过
tabAt(tab, i) == f,进行二次验证确保加锁前,当前桶的头节点未被其他线程修改(如扩容迁移、转红黑树) - 子场景 4.1 :头节点是链表节点(
fh>=0)binCount = 1;// 初始化链表节点计数(从 1 开始,包含头节点)for (Node<K,V> e = f;; ++binCount)// 遍历链表,查找 key 是否存在- 找到相同 key:判断是否需要更新 value
- 保存前置节点,遍历到链表尾部:插入新节点到尾部
- 子场景 4.2 :头节点是红黑树容器(TreeBin,
fh=-2)binCount = 2;// 红黑树节点计数设为 2(无需精确计数,仅用于触发转树判断)- 调用红黑树的插入方法,返回已存在的节点(若 key 已存在)
- 通过
- 场景一 :table 为 null 或者 table 的长度为 0,通过
- 后续处理 :循环的最后,判断是否执行扩容或者升级为红黑树操作:
- 加锁操作后:处理链表转红黑树和返回值
- 若链表长度 ≥ 阈值(默认 8),触发链表转红黑树(或扩容)
- 若 key 已存在,返回旧 value
- 插入新节点成功,跳出循环
- 计数更新:插入成功后,更新哈希表的键值对计数(baseCount 或 CounterCell)
源码实现
java
final V putVal(K key, V value, boolean onlyIfAbsent) {
// 1. 非法参数校验:key/value 为 null 直接抛空指针(ConcurrentHashMap 禁止 null 键值)
if (key == null || value == null) throw new NullPointerException();
// 2. 二次哈希:减少哈希碰撞,保证 key 均匀分布(与 get 方法的 spread 逻辑一致)
int hash = spread(key.hashCode());
// 3. 桶内节点计数:用于判断是否需要将链表转为红黑树
int binCount = 0;
// 4. 死循环重试:并发场景下需重试(如初始化、扩容后需重新定位),直到插入/更新成功
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 场景 1:哈希表未初始化(tab==null)或容量为 0 初始化 table
if (tab == null || (n = tab.length) == 0)
tab = initTable(); // 线程安全的初始化方法(CAS 控制,避免重复初始化)
// 场景 2:目标桶为空(通过 CAS 原子插入节点,无锁操作)
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// CAS 插入新节点:对比 tab[i] 是否为 null,是则插入新 Node,成功则跳出循环
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
break; // 空桶插入无锁,性能最优
}
// 场景 3:目标桶的头节点是扩容标记(MOVED=-1) 协助扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f); // 当前线程参与扩容,扩容后重新获取 table 重试
// 场景 4:目标桶有节点(非空、非扩容) 加锁插入/更新(桶级锁)
else {
V oldVal = null;
// 锁定当前桶的头节点 f:锁粒度为桶,不影响其他桶的操作,并发效率高
synchronized (f) {
// 二次校验:确保加锁前,当前桶的头节点未被其他线程修改(如扩容迁移、转红黑树)
if (tabAt(tab, i) == f) {
// 子场景 4.1:头节点是链表节点(fh>=0)
if (fh >= 0) {
binCount = 1; // 初始化链表节点计数(从 1 开始,包含头节点)
// 遍历链表,查找 key 是否存在
for (Node<K,V> e = f;; ++binCount) {
K ek;
// 找到相同 key:判断是否需要更新 value
if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) {
oldVal = e.val; // 记录旧 value
if (!onlyIfAbsent) // onlyIfAbsent=false 时覆盖旧值
e.val = value;
break; // 跳出循环,插入/更新完成
}
Node<K,V> pred = e; // 前驱节点
// 遍历到链表尾部:插入新节点到尾部
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key, value, null);
break;
}
}
}
// 子场景 4.2:头节点是红黑树容器(TreeBin,fh=-2)
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2; // 红黑树节点计数设为 2(随便设置的,只要不大于8就可以了,目的是不触发下面的判断是否要进行链表转树操作)
// 调用红黑树的插入方法,返回已存在的节点(若 key 已存在)
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent) // 覆盖旧值
p.val = value;
}
}
}
}
// 加锁操作后:处理链表转红黑树和返回值
if (binCount != 0) {
// 若链表长度阈值(默认 8),触发链表转红黑树(或扩容)(已经是红黑树的默认设置为2,永远不会触发)
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
// 若 key 已存在,返回旧 value
if (oldVal != null)
return oldVal;
break; // 插入新节点成功,跳出循环
}
}
}
// 5. 插入成功后,更新哈希表的键值对计数(baseCount 或 CounterCell)
addCount(1L, binCount);
// 6. 插入新节点(key 不存在),返回 null
return null;
}
计数更新(addCount)
插入新节点后,调用 addCount(1L, binCount) 更新总键值对数量; 计数逻辑:延续 sumCount 的设计,低并发时更新 baseCount(CAS),高并发时分散到 CounterCell[] 数组,避免计数竞争; binCount 作用:辅助判断是否需要扩容(若总计数 ≥ sizeCtl 阈值,触发扩容)。