ConcurrentHashMap 源码全解一
该篇主要讲解了数据插入操作流程以及数组扩容流程,插入过程通过 synchronized 和 CAS 保证多线程安全
核心成员及变量
transient volatile Node<K,V>[] table; // 保存数据的数组,容量总是 2 的 N 次方(由tableSizeFor方法控制)
private transient volatile Node<K,V>[] nextTable;// 该数组用于扩容时使用
// 下述 3 个变量的作用:在对 hash 表中元素计数加 1 的时候,若没有多线程竞争直接通过 baseCount 进行增加,若出现多线程竞争,将累加操作分散到 CounterCell 数组中进行,cellsBusy 用于控制 CounterCell 数组项是否也存在竞争。具体原理请看【[JUC-LongAdder](https://blog.csdn.net/qq_24931785/article/details/129429941)】一篇的讲解
private transient volatile long baseCount;
private transient volatile int cellsBusy;
private transient volatile CounterCell[] counterCells;
private transient volatile int sizeCtl;// 用于控制数组初始化于扩容操作
private transient volatile int transferIndex;// 在扩容时通过改变量对每个线程负责转移元素的范围
private static final int MAXIMUM_CAPACITY = 1 << 30;// hash 表最大容量
private static final int DEFAULT_CAPACITY = 16; // 默认容量
static final int TREEIFY_THRESHOLD = 8; // 链表转为红黑树的阈值,前提是数组长度大于 MIN_TREEIFY_CAPACITY
static final int UNTREEIFY_THRESHOLD = 6;// 红黑树转为链表的阈值
static final int MIN_TREEIFY_CAPACITY = 64; // 控制链表转为红黑树的前提
private static final int MIN_TRANSFER_STRIDE = 16;// 扩容时每个线程默认帮助数据转移的个数
// 下述三个变量在数组扩容时使用,低 16 位控制扩容时最大参与线程数
private static int RESIZE_STAMP_BITS = 16;
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
static final int MOVED = -1; // 标识节点已被转移
static final int TREEBIN = -2; // 标识节点是红黑树节点
static final int RESERVED = -3; // computeIfAbsent 方法中使用,创建ReservationNode节点的 hash 值
static final int HASH_BITS = 0x7fffffff; // 计算 hash 值时用
// 保存 key-value 的节点类
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;// key 的hash 值
final K key;
volatile V val;
volatile Node<K,V> next;// 发生 hash 冲突时链接到下一个节点
}
// 占位符节点,扩容时数组元素转移成功时的填充节点
static final class ForwardingNode<K,V> extends Node<K,V> {
final Node<K,V>[] nextTable;
ForwardingNode(Node<K,V>[] tab) {
super(MOVED, null, null, null);
this.nextTable = tab;
}
}
// 红黑树的树节点
static final class TreeNode<K,V> extends Node<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
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;
}
}
// 链表转为红黑树时,放在 数组项中的头结点
static final class TreeBin<K,V> extends Node<K,V> {
TreeNode<K,V> root; // 根节点
volatile TreeNode<K,V> first;// 链表的第一个节点
volatile Thread waiter; // 等待线程
volatile int lockState; //锁状态
// values for lockState
static final int WRITER = 1; // 写锁
static final int WAITER = 2; // 等待写锁
static final int READER = 4; // 读锁
}
put 添加方法
1、计算 hash 值,用于计算数组下标
2、for 循环保证插入成功
3、数组未初始化,进行初始化
4、通过 hash 算出的下标处没有值时,封装Node节点 CAS 放入该下标处
5、hash 冲突时,通过 synchronized 对该节点元素上锁,封装Node节点插入链表或红黑树,插入时如果存在相同的 key 则 根据 onlyIfAbsent 判断是否覆盖旧值
6、增加hash表元素计数
public V put(K key, V value) {
return putVal(key, value, false);
}
// onlyIfAbsent 是否替换旧值
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode()); // 计算 hash 值
int binCount = 0; // 记录同一个数组槽(slot)中的元素个数,也即链表中的元素个数
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable(); // 初始化 hash 表(存放数据的 Node 数组)
// tabAt方法 U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE) 通过偏移量的方式获取对应下标处的值
// 数组基地址 + 偏移量(数组下标 * 数组项的宽度(也即数组项类型的大小,如 int 类型,其大小为 4 byte))
// Unsafe 类的 xxxVolatile方法 使当前数组项具有 volatile 的语义
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
break;
}
// 执行到这一步,意味着 hash 表正在进行扩容,通过 MOVED 来标识,如果正在迁移,那么调用 helpTransfer 帮助进行迁移,算法实现同下面的 transfer 方法
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) { // f 不为 null 代表发生了hash 冲突,那么对 f 进行上锁(链表的头结点上锁,避免其它线程的并发操作)
if (tabAt(tab, i) == f) {// 当前 数组下标处的元素未发生变更
if (fh >= 0) { // 正常链表节点
binCount = 1; // 数组槽中已经存在一个节点 f ,所以 binCount 为 1
for (Node<K,V> e = f;; ++binCount) { // 循环插入链表
K ek;
// 表明链表中存在相同 key ,根据 onlyIfAbsent 判断替换原值
if (e.hash == hash &&
((ek = e.key) == key || (ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent) e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) { // 创建 Node 节点链在链表尾部
pred.next = new Node<K,V>(hash, key, value, null);
break;
}
}
}
else if (f instanceof TreeBin) {// 链表已经转换为红黑树
Node<K,V> p;
binCount = 2; // 红黑树不可能只有一个根节点,因为会在 数组项中放置一个 TreeBin 对象,然后才是根节点,所以设置 binCount = 2
// 将节点插入红黑树,此处不对红黑树结构做过多讲解,详细算法实现将在后续篇章讲解,刚兴趣的同学可以先自行研究
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) {// 返回不为空表明红黑树已经存在 相同 key 的节点,根据 onlyIfAbsent 判断是否替换旧值
oldVal = p.val;
if (!onlyIfAbsent) p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD) // 节点个数 超过阈值 8 ,尝试转换为红黑树。稍后讲解该方法具体实现
treeifyBin(tab, i);
if (oldVal != null) return oldVal;
break;
}
}
}
addCount(1L, binCount);// 增加节点个数
return null;
}
initTable hash表初始化
通过 sizeCtl 控制 hash 表的初始化过程,只有一个线程对其进行初始化,初始化完成后保存 sizeCtl = hash 表容量的 0.75 倍,用于后续控制 hash 表的扩容
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0)
Thread.yield(); // 自旋,等待初始化完成,为了优化,暂时让出 CPU 执行权
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { // 将 sizeCtl 设置为 -1 成功,进行 hash 表的初始化工作
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2); // sc 保存 hash 表中存放元素的最大值,达到该值时需要进行扩容(默认为 hash 表容量的 0.75 倍,也即 tab.length * 0.75,例如 tab.length = 16,那么 sizeCtl = sc = 12)
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
addCount 增加计数
增加hash表元素计数,根据 check 判断是否需要对 hash 表进行扩容,扩容时通过 rs 控制参与扩容的线程数
// addCount(1L, binCount);
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
// 增加 counter 的算法同 LongAdder 的实现原理一样,具体请看【JUC-LongAdder】一篇的讲解
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
// 省略代码 ...
s = sumCount();// 算出 hash 表中元素个数
}
if (check >= 0) { // 检查 hash 表是否需要扩容
Node<K,V>[] tab, nt; int n, sc;
// hash 表元素个数 大于 hash 表的扩容阈值 并且 hash 表长度为达到最大限制范围
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
// resizeStamp(n) 计算扩容时使用的 stamp 值,将其左移到高 16 位,因为低 16 位用于表示 帮助扩容的线程数
int rs = resizeStamp(n) << RESIZE_STAMP_SHIFT;
// sizeCtl < 0 表示 hash 表正在扩容(此时 sizeCtl 值已被设置为 rs + 2 的值),那么该线程将帮助其完成扩容动作
if (sc < 0) {
// MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1 最大帮助扩容线程个数(默认低 16 位)
if (sc == rs + MAX_RESIZERS || sc == rs + 1 // 表示已达到最大帮助线程个数
|| (nt = nextTable) == null || transferIndex <= 0) // 扩容已完成
break;
// CAS 增加帮助线程个数,CAS 成功 调用 transfer 帮助扩容
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
// 将 sizeCtl 设置为 rs + 2,设置成功,开始扩容
else if (U.compareAndSwapInt(this, SIZECTL, sc, rs + 2))
transfer(tab, null);
s = sumCount(); // 获取最新元素个数
}
}
}
treeifyBin 转换红黑树
将链表转换为红黑树
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
if (tab != null) {
// hash 表元素个数 小于 MIN_TREEIFY_CAPACITY = 64 时,尝试扩容 hash 表
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
tryPresize(n << 1);
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
synchronized (b) {
if (tabAt(tab, index) == b) {
TreeNode<K,V> hd = null, tl = null;
for (Node<K,V> e = b; e != null; e = e.next) {
TreeNode<K,V> p = new TreeNode<K,V>(e.hash, e.key, e.val, null, null);
if ((p.prev = tl) == null) hd = p;
else tl.next = p;
tl = p;
}
setTabAt(tab, index, new TreeBin<K,V>(hd)); // 在 TreeBin 构造函数中完成红黑树构建
}
}
}
}
}
tryPresize 扩容
扩容时通过计算一个 stamp 值来记录标记当前扩容周期,resizeStamp 方发原理:numberOfLeadingZeros方法算出 n 的二进制前面有几个 0 ,或 上 (1 << 15) 保证整型的 低16位 的高位 为 1 ,当下面执行 rs << RESIZE_STAMP_SHIFT 时 整型的高位变为 1 ,即为负数。只要 sc 为负数,就表示正在进行扩容
private final void tryPresize(int size) {
int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
tableSizeFor(size + (size >>> 1) + 1); // 计算新数组大小,原数组的 2 倍
int sc;
while ((sc = sizeCtl) >= 0) {
Node<K,V>[] tab = table; int n;
if (tab == null || (n = tab.length) == 0) {
// 此处为数组初始化流程 请看 initTable 方法,此时我们关注下面的扩容操作
}
else if (c <= sc // 容量 小于 扩容阈值,无需扩容
|| n >= MAXIMUM_CAPACITY) break; // 原容量以达到最大值,无需扩容
else if (tab == table) { // 原 hash 表为发生更改,进行扩容
// resizeStamp 方法 Integer.numberOfLeadingZeros(n) | (1 << 15),numberOfLeadingZeros方法算出 n 的二进制前面有几个 0 ,或 上 (1 << 15) 保证整型的 低16位 的高位 为 1 ,当下面执行 rs << RESIZE_STAMP_SHIFT 时 整型的高位变为 1 ,即为负数
int rs = resizeStamp(n);// 计算扩容时使用的 stamp 值
// sizeCtl < 0 表示 hash 表正在扩容(此时 sizeCtl 值已被设置为 rs + 2 的值),那么该线程将帮助其完成扩容动作
// 下述逻辑同上述 addCount 方法,详情请看 addCount 方法
if (sc < 0) {
Node<K,V>[] nt;
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
// 将 sizeCtl 设置为 (rs << RESIZE_STAMP_SHIFT) + 2 此值为负数,设置成功,开始扩容
else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
}
}
}
transfer 转移数据
1、计算每个线程负责转移数据的范围值大小 stride,用于将原始数组切割为多个区间,每个线程负责其中一个区间
2、转移数组未初始化,进行初始化
3、创建一个ForwardingNode(该节点的 hash 值等于 -1)节点,用于填充原始数组中已被转移数组项(转移到新数组后将原数组的数据替换为 ForwardingNode 节点,标识数据已被转移,也即数组正在进行扩容,当其它线程发现节点是 ForwardingNode 节点时,将会参与到数组扩容转移数据的操作中),转移过程中如果发现数组项为空,也将其填充 ForwardingNode 节点
4、通过第一步计算的 stride 值计算出该线程所负责转移的区间,通过 i 和 bound 标识;另外 通过 advance 来标识是否需要向前继续查找需要转移的数据,通过 finishing 标识转移是否完成,只有当所有参与线程都完成各自的转移操作 finishing 才会被设置为 true
5、如果当前转移节点是 列表结构,遍历链表元素通过 节点的 hash 值 & 上 原数组长度 (fh & n),根据返回结果是否为 0 将原链表拆分为 ln 和 hn 两个链表,ln 链表在新数组中的位置保持为原始下标处,hn 链表将会保存在 原始下标 + 原始数组长度 的 下标处
6、如果当前转移节点是 红黑树节点,原理同第五步,将红黑树转为 ln 和 hn 的两个链表(此时是双向链表结构),然后再根据每个链表中的元素个数决定是否转为红黑树保存
读者可能对 ln 和 hn 两个链表结构的划分有疑虑,在此我已一个简单的示例来说明一下,希望对大家理解源码有所帮助
如 数组长度 length = 8,最大下标为7,扩容后 length = 16,最大索引下标为 15
8 >>>> 0000 1000 7 >>>> 0000 0111 16 >>>> 0001 0000 15 >>>> 0000 1111
加入数据保存在 index = 4 的位置,那么 该数据的hash值二进制的后3位 一定是 100,其他位是啥不会影响到下标的计算 如
hash1 = 0101 1100 ,hash1 & 0000 0111 = 0000 0100 = 4
hash2 = 1011 0100 ,hash2 & 0000 0111 = 0000 0100 = 4
hash1 & 0000 1000 = 0000 1000 hash1 & 0000 1111 = 0000 1100 = 12 = 4 + 8 (原index + 原length)
hash2 & 0000 1000 = 0000 0000 hash2 & 0000 1111 = 0000 0100 = 4 (原index)
由此可见当 hash 值与上 原length 的值等于 原length 时,此数据将被分散到新数组的 (原 index + 原length)处,
hash 值与上 原length 的值等于 0 时,此数据将被保存在新数组对应的index 处
这就是打散 ln 和 hn 的原理
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
if (nextTab == null) { // 创建新数组
try {
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) {}
nextTable = nextTab;
transferIndex = n;// 记录转移元素开始索引位置,初始为 原数组长度
}
int nextn = nextTab.length;
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab); // 创建一个转移节点
boolean advance = true; // 标志是否继续向前寻找转移节点
boolean finishing = false; // 标识转移是否完成
// i(高位) 标识开始转移元素的下标,bound(低位) 标识该线程负责转移的范围,转移顺序是从数组尾部开始先前进行
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
while (advance) {
int nextIndex, nextBound;
if (--i >= bound // 表示还有元素需要转移
|| finishing) advance = false;
else if ((nextIndex = transferIndex) <= 0) {// 没有可分配的转移区间了,不需要向前查找了
i = -1;
advance = false;
}
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) { // 分割该线程负责转移的数组区间
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
if (finishing) { // 转移完成
nextTable = null;
table = nextTab; // 将 新数组 赋值给 table
sizeCtl = (n << 1) - (n >>> 1);// 保存下一次需要扩容的阈值
return;
}
// 帮助转移线程数 减 1 (进入transfer方法前加了 1)
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
// 两者不等表示当前线程不是最后一个完成的线程,所以直接退出即可;当是最后一个线程时,设置 finishing = ture 标识扩容完成
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
finishing = advance = true;
i = n; // recheck before commit
}
}
else if ((f = tabAt(tab, i)) == null) // 如果当前下标处没有存放元素,那么将改下标出存入 ForwardingNode 节点,也用于标识当前正在对 hash 表进行扩容
advance = casTabAt(tab, i, null, fwd);
else if ((fh = f.hash) == MOVED)// 表示当前节点已被转移
advance = true; // already processed
else {
synchronized (f) {// 对头结点进行上锁,避免在转移期间其它线程进行插入操作
if (tabAt(tab, i) == f) {// 节点未发生改变
Node<K,V> ln, hn; // ln 表示 低位节点;hn 表示 高位节点
if (fh >= 0) {// 表示当前是链表节点
// runBit == 0 表示当前节点为 ln (在新数组中保存的下标等于原始数组的下标),否则为 hn (保存在新的下标处)
// 例如原始数组长度是 16,扩容后长度是 32(切分为 高 16 位和 低 16 位),ln 节点将继续保存在 低16位中,hn 将被散列到 高 16 位中
int runBit = fh & n;
Node<K,V> lastRun = f; // lastRun 默认为 头结点
// 遍历找到最后一个节点,lastRun 指向最后一个节点 p,ln 和 hn 取决于 runBit 位是否为 0
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
if (runBit == 0) {
ln = lastRun;
hn = null;
}
else {
hn = lastRun;
ln = null;
}
// 从头结点遍历,知道lastRun节点,此循环的目的就是为了将原先的一个链表拆分为 ln 和 hn 两个链表
for (Node<K,V> p = f; p != lastRun; p = p.next) {
int ph = p.hash; K pk = p.key; V pv = p.val;
if ((ph & n) == 0)
ln = new Node<K,V>(ph, pk, pv, ln);
else
hn = new Node<K,V>(ph, pk, pv, hn);
}
setTabAt(nextTab, i, ln); // 将 ln 链表存入 新数组 原来的 index 处
setTabAt(nextTab, i + n, hn);// hn 将被散列到(原 index + 原数组长度) 的下标处
setTabAt(tab, i, fwd);// 将原数组的 i 下标处设置为 ForwardingNode 节点,标识节点已转移
advance = true;// 向前递进查找需要转移的节点
}
else if (f instanceof TreeBin) { // 当前节点是红黑树节点
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> lo = null, loTail = null;
TreeNode<K,V> hi = null, hiTail = null;
int lc = 0, hc = 0;
// 此 for 循环原理同 上述链表节点的原理一样,将原先的红黑树切分为两个 链表(双向链表)
for (Node<K,V> e = t.first; e != null; e = e.next) {
int h = e.hash;
TreeNode<K,V> p = new TreeNode<K,V>
(h, e.key, e.val, null, null);
if ((h & n) == 0) {
if ((p.prev = loTail) == null)
lo = p;
else
loTail.next = p;
loTail = p;
++lc;
}
else {
if ((p.prev = hiTail) == null)
hi = p;
else
hiTail.next = p;
hiTail = p;
++hc;
}
}
// 当 lc 计数小于等于 6 的时候,调用untreeify方法转为普通链表保存,当 hc 计数等于 0 时,直接将原先的红黑树节点赋值给 ln 即可,否则将 lo 转为新的红黑树保存;hn 处理方式也一样
ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
(hc != 0) ? new TreeBin<K,V>(lo) : t;
hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
(lc != 0) ? new TreeBin<K,V>(hi) : t;
setTabAt(nextTab, i, ln);// 将 ln 链表存入 新数组 原来的 index 处
setTabAt(nextTab, i + n, hn);// hn 将被散列到(原 index + 原数组长度) 的下标处
setTabAt(tab, i, fwd);// 将原数组的 i 下标处设置为 ForwardingNode 节点,标识节点已转移
advance = true;// 向前递进查找需要转移的节点
}
}
}
}
}
}