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; }
本文来自博客园,作者:诸葛匹夫,转载请注明原文链接:https://www.cnblogs.com/shenxingzhuge/p/19070131
卡里离冰冷的40个亿还差39多个亿
浙公网安备 33010602011771号