ConcurrentHashMap 核心方法源码注释

ConcurrentHashMap 核心方法源码注释文档

一、文档说明

本文档基于 JDK 1.8 版本的 ConcurrentHashMap,对其核心方法进行源码级别的注释解析。ConcurrentHashMap 是 Java 并发包(java.util.concurrent)中的重要容器,旨在解决 HashMap 线程不安全和 Hashtable 效率低下的问题,通过 CAS 操作synchronized 锁(分段锁思想的优化) 实现了高效的并发访问。

本文档选取的核心方法涵盖初始化、元素添加、元素获取、元素删除、扩容等关键操作,注释将详细解释方法的设计思路、并发安全保障机制及关键逻辑细节。

二、ConcurrentHashMap 核心方法源码注释

2.1 类定义与核心成员变量

在分析方法前,先明确 ConcurrentHashMap 的类结构和核心成员变量,为后续方法理解奠定基础。

package java.util.concurrent;

import java.util.*;

import java.util.function.BiConsumer;

import java.util.function.BiFunction;

import java.util.function.Function;

/**

* 基于哈希表的并发哈希映射,支持全并发的检索操作和高预期并发的更新操作。

* JDK 1.8 中摒弃了 JDK 1.7 的分段锁(Segment)机制,改用「数组 + 链表/红黑树」结构,

* 通过对链表头节点/红黑树根节点加 synchronized 锁,结合 CAS 操作实现细粒度并发控制,

* 提升并发效率。

*/

public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>

implements ConcurrentMap<K, V>, Serializable {

private static final long serialVersionUID = 7249069246763182397L;

// ===================== 核心成员变量 =====================

/**

* 哈希表数组的默认初始容量(16),必须是 2 的幂

*/

private static final int DEFAULT_CAPACITY = 16;

/**

* 哈希表数组的最大容量(2^30),防止容量溢出

*/

private static final int MAXIMUM_CAPACITY = 1 << 30;

/**

* 默认的负载因子(0.75),平衡空间利用率和查询效率

* 当元素数量 > 容量 * 负载因子时,触发扩容

*/

private static final float LOAD_FACTOR = 0.75f;

/**

* 链表转红黑树的阈值(8):当链表长度超过此值,且数组容量 >= 64 时,将链表转为红黑树

* (红黑树查询时间复杂度 O(logn),优于链表 O(n))

*/

static final int TREEIFY_THRESHOLD = 8;

/**

* 红黑树转链表的阈值(6):当红黑树节点数少于此值时,将红黑树转回链表

*/

static final int UNTREEIFY_THRESHOLD = 6;

/**

* 触发链表转红黑树的最小数组容量(64):若数组容量 < 64,即使链表长度超 8,也优先扩容而非转树

*/

static final int MIN_TREEIFY_CAPACITY = 64;

/**

* 哈希表的核心数组(存储链表/红黑树的头节点),volatile 修饰确保数组修改对其他线程可见

*/

transient volatile Node<K, V>[] table;

/**

* 扩容时使用的临时数组(扩容过程中,部分元素会迁移到此处),volatile 修饰

*/

private transient volatile Node<K, V>[] nextTable;

/**

* 哈希表中的元素总数(通过 CAS 操作更新,避免 synchronized 锁开销)

*/

private transient volatile long baseCount;

/**

* 扩容控制变量:

* - 当为负数时,表示当前正在扩容(-1 为初始扩容状态,其他负数为扩容线程数标识)

* - 当为 0 时,表示未初始化或未触发扩容

* - 当为正数时,表示下一次扩容的阈值(元素数达到此值时触发扩容)

*/

private transient volatile int sizeCtl;

// ===================== 核心内部类:Node(哈希表节点) =====================

/**

* 哈希表的基本节点,存储 key、value、hash 值和下一个节点(链表结构)

* value 用 volatile 修饰,确保多线程下 value 读写可见

*/

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;

}

// 以下为 Map.Entry 接口方法的实现(getKey、getValue、setValue 等),略

}

// ===================== 核心内部类:TreeNode(红黑树节点) =====================

/**

* 红黑树节点,继承自 Node,额外存储红黑树的父子节点和颜色信息

* 用于链表转树后的节点存储,提升查询效率

*/

static final class TreeNode<K, V> extends Node<K, V> {

TreeNode<K, V> parent; // 父节点

TreeNode<K, V> left; // 左子节点

TreeNode<K, V> right; // 右子节点

TreeNode<K, V> prev; // 链表前驱节点(用于红黑树转链表时快速遍历)

boolean red; // 节点颜色(红/黑,红黑树的核心属性)

TreeNode(int hash, K key, V val, Node<K, V> next, TreeNode<K, V> parent) {

super(hash, key, val, next);

this.parent = parent;

}

// 红黑树的旋转、着色等操作方法,略

}

}

 

2.2 核心方法 1:构造方法(初始化)

ConcurrentHashMap 提供多个构造方法,核心是初始化 sizeCtl(扩容控制变量),延迟初始化哈希表数组(table),避免初始化时的线程安全问题。

/**

* 无参构造方法:使用默认初始容量(16)和默认负载因子(0.75)

*/

public ConcurrentHashMap() {

// 无参构造仅初始化 sizeCtl 为默认容量,table 延迟到第一次 put 时初始化

// sizeCtl = DEFAULT_CAPACITY(16),表示下一次扩容阈值为 16(未初始化时,sizeCtl 也代表初始容量)

}

/**

* 指定初始容量的构造方法

* @param initialCapacity 期望的初始容量(最终会调整为最近的 2 的幂,确保哈希表高效寻址)

*/

public ConcurrentHashMap(int initialCapacity) {

if (initialCapacity < 0)

throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);

// 若初始容量超过最大容量(2^30),则取最大容量

int cap = (initialCapacity >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : tableSizeFor(initialCapacity);

// sizeCtl 设为 cap(此时表示未初始化,初始容量为 cap)

this.sizeCtl = cap;

}

/**

* 将输入的容量调整为最近的 2 的幂(向上取整)

* 例:输入 17 → 输出 32,输入 30 → 输出 32

* @param c 原始容量

* @return 调整后的 2 的幂容量

*/

private static final int tableSizeFor(int c) {

// 计算逻辑:通过位运算将 c 的二进制所有低位设为 1,再 +1 得到 2 的幂

int n = c - 1;

n |= n >>> 1; // 右移 1 位,或运算,将次高位设为 1

n |= n >>> 2; // 右移 2 位,或运算,将后两位设为 1

n |= n >>> 4; // 右移 4 位,或运算,将后四位设为 1

n |= n >>> 8; // 右移 8 位,或运算,将后八位设为 1

n |= n >>> 16; // 右移 16 位,或运算,将后十六位设为 1

// 若结果超过最大容量,则返回最大容量;否则返回 n+1(此时 n+1 是 2 的幂)

return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;

}

 

2.3 核心方法 2:put (K key, V value)(添加元素)

put 方法是 ConcurrentHashMap 最核心的方法之一,负责将键值对存入哈希表,核心逻辑包括:延迟初始化 table计算哈希索引无锁 CAS 尝试插入synchronized 锁保证并发安全链表 / 红黑树插入扩容触发

/**

* 将指定的键值对存入哈希表,若 key 已存在则覆盖旧 value

* @param key 键(不可为 null)

* @param value 值(不可为 null)

* @return 若 key 已存在,返回旧 value;否则返回 null

*/

public V put(K key, V value) {

return putVal(key, value, false);

}

/**

* put 方法的核心实现(内部方法)

* @param key 键

* @param value 值

* @param onlyIfAbsent 若为 true,则仅当 key 不存在时才插入(对应 putIfAbsent 方法);若为 false,则覆盖旧值

* @return 旧 value(若存在)或 null

*/

final V putVal(K key, V value, boolean onlyIfAbsent) {

// 1. 校验 key 和 value 不可为 null(与 HashMap 不同,ConcurrentHashMap 不允许 null 键值)

if (key == null || value == null) throw new NullPointerException();



// 2. 计算 key 的哈希值:通过 spread 方法优化哈希分布,减少哈希冲突

int hash = spread(key.hashCode());

int binCount = 0; // 记录当前链表/红黑树的节点数(用于判断是否需要转树)



// 3. 循环遍历哈希表(自旋,确保并发场景下插入成功)

for (Node<K, V>[] tab = table; ; ) {

Node<K, V> f; // 当前哈希桶的头节点(f = tab[i])

int n, i, fh; // n = 哈希表长度,i = 哈希索引,fh = 头节点的 hash 值



// 3.1 若哈希表未初始化(tab == null),先初始化 table

if (tab == null || (n = tab.length) == 0)

tab = initTable(); // 初始化 table(线程安全,通过 CAS 控制)



// 3.2 计算当前 key 的哈希桶索引 i,并获取桶头节点 f(volatile 读,确保可见性)

else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {

// 3.2.1 若桶为空(f == null),尝试用 CAS 插入新节点(无锁操作,高效)

// 若 CAS 成功,跳出循环;若失败(其他线程已插入),重新进入循环

if (casTabAt(tab, i, null, new Node<K, V>(hash, key, value, null)))

break; // no lock when adding to empty bin

}



// 3.3 若当前桶的头节点 f 的 hash 为 MOVED(-1),表示当前正在扩容,当前线程协助扩容

else if ((fh = f.hash) == MOVED)

tab = helpTransfer(tab, f); // 协助扩容,返回扩容后的新 table



// 3.4 桶不为空且未扩容,对桶头节点 f 加 synchronized 锁,确保并发安全

else {

V oldVal = null; // 存储旧 value

synchronized (f) {

// 二次校验:确保 f 仍是当前桶的头节点(防止其他线程修改桶结构)

if (tabAt(tab, i) == f) {

// 3.4.1 若头节点 hash >= 0,表示当前桶是链表结构

if (fh >= 0) {

binCount = 1; // 初始化节点计数

// 遍历链表,查找 key 对应的节点

for (Node<K, V> e = f; ; ++binCount) {

K ek;

// 若找到 key 相同的节点(hash 相同且 key 相等)

if (e.hash == hash &&

((ek = e.key) == key || (ek != null && key.equals(ek)))) {

oldVal = e.val; // 记录旧 value

// 若 onlyIfAbsent 为 false(允许覆盖),则更新 value

if (!onlyIfAbsent)

e.val = value; // volatile 写,确保其他线程可见

break; // 跳出循环,插入完成

}

// 若未找到,遍历到链表尾部,插入新节点

Node<K, V> pred = e;

if ((e = e.next) == null) {

pred.next = new Node<K, V>(hash, key, value, null);

break;

}

}

}

// 3.4.2 若头节点是红黑树节点(TreeNode),则调用红黑树的插入方法

else if (f instanceof TreeNode)

oldVal = ((TreeNode<K, V>)f).putTreeVal(this, tab, hash, key, value);

}

}



// 3.5 插入完成后,判断是否需要将链表转为红黑树

if (binCount != 0) {

// 若节点数 >= 转树阈值(8),调用 treeifyBin 尝试转树

if (binCount >= TREEIFY_THRESHOLD)

treeifyBin(tab, i);

// 若存在旧 value,返回旧值(无需更新元素总数)

if (oldVal != null)

return oldVal;

break;

}

}

}



// 4. 插入新节点后,更新元素总数,并判断是否需要触发扩容

addCount(1L, binCount);

return null;

}

/**

* 优化哈希值:将 key 的 hashCode 高位与低位混合,减少哈希冲突(因为 (n-1)&hash 仅用低位)

* @param h 原始 hashCode

* @return 优化后的哈希值

*/

static final int spread(int h) {

// h ^ (h >>> 16):将高 16 位与低 16 位异或,混合高位信息

// & HASH_BITS(0x7fffffff):将符号位设为 0,确保哈希值为正数(MOVED 为 -1,用于标识扩容)

return (h ^ (h >>> 16)) & HASH_BITS;

}

/**

* 延迟初始化哈希表 table(线程安全)

* @return 初始化后的 table

*/

private final Node<K, V>[] initTable() {

Node<K, V>[] tab;

int sc;

// 循环判断 table 是否未初始化(volatile 读,确保可见性)

while ((tab = table) == null || tab.length == 0) {

// 若 sizeCtl < 0,表示其他线程正在初始化,当前线程让出 CPU(yield),避免忙等

if ((sc = sizeCtl) < 0)

Thread.yield(); // lost initialization race; just spin

// 若 sizeCtl >= 0,尝试用 CAS 将 sizeCtl 设为 -1(标识当前线程正在初始化)

else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {

try {

// 二次校验:确保 table 仍未初始化(防止其他线程已初始化)

if ((tab = table) == null || tab.length == 0) {

// sc 是初始容量(构造方法中设置),若为 0 则用默认容量 16

int n = (sc > 0) ? sc : DEFAULT_CAPACITY;

// 创建容量为 n 的 Node 数组(table)

@SuppressWarnings("unchecked")

Node<K, V>[] nt = (Node<K, V>[])new Node<?, ?>[n];

table = tab = nt;

// 计算下一次扩容阈值:n * LOAD_FACTOR(0.75),用 sc 存储

sc = n - (n >>> 2); // n >>> 2 = n/4,n - n/4 = 3n/4 = n*0.75

}

} finally {

// 初始化完成后,将 sizeCtl 设为扩容阈值(sc)

sizeCtl = sc;

}

 

posted @ 2025-09-02 15:43  诸葛匹夫  阅读(19)  评论(0)    收藏  举报