put()
方法是并发 HashMap 源码分析的重点方法,这里涉及到并发扩容,桶位寻址等等…- JDK1.8 ConcurrentHashMap 的结构图:
1、put(K key, V value) 方法源码解析
// 向并发 HasHMap 中put一个数据
public V put(K key, V value) {
return putVal(key, value, false);
}
// 向并发 HashMap 中put一个数据
// key:数据的key
// value:数据的值
// onlyIfAbsent:是否替换数据
// 如果为false,则当put数据的时候,遇到Map中有相同 K,V的数据,则将其替换
// 如果为true,则当put数据的时候,遇到Map中有相同K,V的数据,则不做替换,不插入
final V putVal(K key, V value, boolean onlyIfAbsent) {
// 保证 key 和 value 不为null
if (key == null || value == null) throw new NullPointerException();
// 通过spread方法计算出来的hash值让高位也能参与进寻址运算
int hash = spread(key.hashCode());
// binCount 表示当前K-V 封装成node对象后插入到指定桶位后,在桶位中的所属链表的下标位置
// 0:表示当前桶位为null,node 可以直接插入
// 2:表示当前桶位已经树化为了红黑树
int binCount = 0;
// tab:引用map对象的table
// 自旋
for (Node<K,V>[] tab = table;;) {
// f:表示桶位的头结点
// n:表示散列表数组的长度
// i:表示key通过寻址计算后,所命中的桶位下标
// fh:表示桶位头结点的hash值
Node<K,V> f; int n, i, fh;
// CASE1:条件成立:表示当前 map 中的 table 尚未初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// CASE2:i:表示key使用路由寻址算法得到 key 对应 table数组的下标位置,tabAt方法获取指定桶位的头结点f
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 进入到CAS2代码块的前置条件:当前table数组的 i 桶位一定是null的时候
// 通过使用 CAS 方式设置指定数组i桶位为 new Node<K,V>(hash, key, value, null),并且期望值是null的话
// cas操作成功,表示ok,直接break 自旋操作即可
// cas操作失败,表示在当前线程之前有其他线程先一步向指定 i 桶位设置值了
// 当前线程只能再次自旋,去走其他逻辑
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
// CASE3:前置条件:对应桶位的头结点一定不是null
// 条件成立,表示当前桶位的头结点为 FWD 节点,表示当前map正处于扩容过程中
else if ((fh = f.hash) == MOVED)
// 看到 FWD 节点后,当前节点有义务帮助当前map对象完成迁移数据的工作
// 学完扩容后再来看该方法
tab = helpTransfer(tab, f);
// 来到 CASE4的前置条件:
// 当前table数组不为空且长度不为0,当前插入元素所命中的桶位节点不为null,桶位节点也不是FWD节点
// CASE4:当前桶位可能是链表,也可能是红黑树代理节点TreeBin节点
else {
// 当插入 key 存在的时候,会将旧值赋值给oldVal,返回给put方法调用处
V oldVal = null;
// 通过synchronized 加锁给对应桶位的头结点,理论上是`头节点`
synchronized (f) {
// -----------------------------------------------------------------------
// 获取一下当前桶位的头结点并跟CASE2获取的桶位头结点进行对比
// 为什么又要对比一下,看看当前桶位的头结点是否为之前桶位的头结点?
// 如果其他线程再当前线程之前将该桶位的头结点给修改掉了,当前线程再使用synchronized对该节点f加锁就有问题了(锁本身加错了地方)
if (tabAt(tab, i) == f) { // 如果条件成立,说明加锁的对象f没有问题,持有锁
// 条件成立:说明当前桶位就是普通链表桶位
if (fh >= 0) {
// binCount在是链表节点的情况下表示两种情况:
// 1.当前插入key与链表当中的所有元素的key都不一致的时候,当前的插入操作是追加到链表的末尾,binCount表示链表长度
// 2.当前插入key与链表当中的某个元素的key一致的时候,当前插入操作可能就是替换了,binCount表示冲突位置(binCount - 1)
binCount = 1;
// 迭代循环当前桶位的链表,e是每次循环处理的节点
for (Node<K,V> e = f;; ++binCount) {
// 当前循环节点的key
K ek;
// 条件一:e.hash == hash 表示循环的当前元素的hash值与插入节点的hash值一致,需要进一步判断
// 条件二:((ek = e.key) == key || (ek != null && key.equals(ek)))
// 成立:说明循环的当前节点与插入节点的key一致,发生冲突了
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
// 将当前循环的元素的值赋值给oldVal
oldVal = e.val;
if (!onlyIfAbsent)
// 默认对冲突的桶位节点做替换操作
e.val = value;
break;
}
// 当前元素与插入元素的key不一致的时候,会走下面的程序
// 1.更新循环处理节点为当前节点的下一个节点
// 2.判断下一个节点是否为 null,如果是null,说明当前节点已经是队尾了,插入数据需要追加到队尾节点的后面
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
// 前置条件:该桶位一定不是链表
// 条件成立:表示当前桶位是红黑树代理节点TreeBin节点
else if (f instanceof TreeBin) {
// p:表示红黑树中如果与你插入节点的key有冲突节点的话,则putTreeVal方法会返回冲突节点的引用
Node<K,V> p;
// 强制设置binCount 为2,因为binCount<=1的时候有其他含义,所以这里设置为了2,回头讲addCount方法的时候会说
binCount = 2;
// 条件一:条件成立,说明当前插入节点的key与红黑树中的某个节点的key一致,说明冲突了
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
// 将冲突的节点的值赋值给oldVal
oldVal = p.val;
if (!onlyIfAbsent)
// 重写冲突节点的值
p.val = value;
}
}
}
} // synchronized结束
// 条件成立:说明当前桶位不为null,可能是红黑树,也可能是链表
if (binCount != 0) {
// 如果binCount>=8,表示插入后的对应桶位是链表需要转化为红黑树
if (binCount >= TREEIFY_THRESHOLD)
// 调用转化链表为红黑树的方法(转化为双向链表)
treeifyBin(tab, i);
// 说明当前线程插入的数据key与原油key-value发生冲突了,需要将原数据返回该调用者
if (oldVal != null)
return oldVal;
break;
}
}
} // 退出自旋操作
// 1.统计当前散列表一共有多少数据
// 2.判断是否达到扩容阙值标准触发扩容
addCount(1L, binCount);
// 如果插入过程没有与桶位中的元素发生冲突,则返回null
return null;
}
2、initTable() 方法源码分析
第一次放元素的时候,初始化桶数组:
// 散列表数组table的初始化方法
private final Node<K,V>[] initTable() {
// tab:表示map中的table引用
// sc:表示临时局部的sizeCtl的值
// sizeCtl:默认为 0,用来控制 table 的状态、以及初始化和扩容操作
// sizeCtl < 0 表示table的状态:
// (1) = -1,表示有其他线程正在进行初始化的操作,(其他线程就不能再进行初始化操作,相当于一把锁)
// (2) = -(1 + nThreads),表示有 n 个线程正在一起扩容(低16位表示),高16位表示扩容的标识戳
// sizeCtl >= 0,标识 table 的初始化和扩容相关操作
// (3) = 0,默认值,后续在真正初始化 table 的时候使用,设置为默认容量 DEFAULT_CAPACITY --> 16。
// (4) > 0,将sizeCtl 设置为table初始化容量或者扩容完成后的下一次扩容的门槛
Node<K,V>[] tab; int sc;
// 自旋:附加条件是 table是null或者table长度为0,当前散列表尚未初始化
while ((tab = table) == null || tab.length == 0) {
/**
* sizeCtl < 0
* 1.-1的时候表示当前的table正在初始化(有线程正在创建table数组)当前线程需要自旋等待
* 2.表示当前map正在进行扩容,高16位表示:扩容的标识戳 低16位表示:(1 + nThread)表示当前参与并发扩容的线程数量
*
* sizeCtl == 0 表示创建table数组的时候,使用 DEFAULT_CAPACITY 为大小
*
* sizeCtl > 0
* 1. 如果table未初始化,表示初始化大小
* 2. 如果table已经初始化,表示下次扩容时的触发条件(阙值)
*/
// CASE1:sizeCtl < 0
// sizeCtl < 0 可能是以下两种情况:
// (1)-1,表示有其他线程正在进行 table 初始化操作
// (2)-(1 + nThreads):表示有 n 个线程正在一起进行扩容
if ((sc = sizeCtl) < 0)
// 大概率为-1,表示其他线程正在进行初始化操作,当前线程没有竞争到初始化table的锁,进而当前线程被迫等待
Thread.yield(); // lost initialization race; just spin
// -----------------------------------------------------------------------------
// CASE2:sizeCtl >= 0,且 U.compareAndSwapInt(this, SIZECTL, sc, -1) 结果为true
// U.compareAndSwapInt(this, SIZECTL, sc, -1):当前线程以CAS 的方式修改sizeCtl 为-1
// sizeCtl 如果成功被修改为 -1,就返回true,否则返回 false
// 当返回为 true的时候,则该线程就可以进入下面的else if 的代码块中,这时候sizeCtl = -1就相当于是一把锁,表示把下面的else if 代码块已经被占用,其他线程不能再进入了
// 进入到该条件中有以下三种条件:
// 1.sizeCtl == 0 表示创建table数组的时候,使用DEFAULT_CAPACITY 为大小
// 2. 如果table未初始化,表示初始化大小
// 3. 如果table已经初始化,表示下次扩容时的触发条件(阙值)
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { // 当前线程成功的设置sizectl的值为 -1
try {
// 这里为什么又要再次判断呢?
// 为了防止其他线程已经初始化table完毕了,然后当前线程再次对其初始化,导致丢失数据
// 如果条件成立,说明其他线程都没有进入过这个if块,当前线程就是具备初始化table的权力了
if ((tab = table) == null || tab.length == 0) {
// sc > 0:创建table的时候使用sc为指定大小,否则使用16默认值
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
// 最终赋值给map.table
table = tab = nt;
// n >>> 2 ==> 等于 1/4 n n - (1 / 4) * n = 3/4 n => 0.75 * n
// sc 就是 0.75 的 n,表示下一次扩容时的触发条件
sc = n - (n >>> 2);
}
} finally {
// 有两种情况:
// 1.如果当前线程是第一次创建 map.table的线程的话,sc表示的是下一次扩容的阙值
// 2.表示当前线程并不是第一次创建map.table的线程,当前线程进入到else-if块的时候将
// sizeCtl 设置为了-1,这时需要将其修改为进入前的值
sizeCtl = sc;
}
break;
}
}
return tab;
}
小节:
- 使用 CAS 锁控制只有一个线程初始化桶数组
- sizeCtl 在初始化后存储的下一次扩容的阙值
- 扩容阙值写死的是桶数组大小的 0.75 倍,桶数组大小即 map.table 的容量,也就是最多存储最多个元素
3、addCount(long x, int check) 方法源码解析(难点)
在阅读 addCount(long x, int check)
方法源码之前,最好再去熟悉一下 LongAdder
的源码:JDK 8 新特性 LongAdder 源码解析
addCount(long x, int check)
方法的作用:每次添加元素后,元素数量加 1,并判断是否达到了扩容门槛,达到了则进行扩容或者协助扩容。
// 1.统计当前散列表一共有多少数据
// 2.判断是否达到扩容阙值标准触发扩容
private final void addCount(long x, int check) {
// as:longAdder 的cells 变量
// b:LongAder 的 base 变量
// s:表示当前map.table 中元素的数量
CounterCell[] as; long b, s;
// 条件一:(as = counterCells) != null:true,表示cells 已经初始化了,当前线程应该去使用hash寻址去找到合适的cell去累加
// false:表示当前线程应该将数据累加到 base变量
// 条件二:false:表示写 base成功,数据累加到base中了,当前竞争不激烈,不需要创建cells数组
// true:表示写base 失败,与其他线程在base上发生了竞争,当前线程应该有尝试创建cells数组
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
// 有两种情况进入到if块中:
// 1.(as = counterCells) != null:true,表示cells 已经初始化了,当前线程应该去使用hash寻址去找到合适的cell去累加
// 2.表示写base 失败,与其他线程在base上发生了竞争,当前线程应该有尝试创建cells数组
// a:表示当前线程hash 寻址命中的cell 元素
// v:表示当前写cell的时候的期望值
// m:表示当前cells数组的长度
CounterCell a; long v; int m;
// uncontended:true:表示未发生竞争
// false:表示发生竞争
boolean uncontended = true;
// 条件一:as == null || (m = as.length - 1) < 0
// true:表示当前线程是通过写base变量竞争失败进入的上一个if块,那么就需要调用fullAddCount(x, uncontended)方法去初始化或者扩容或者重试
// 条件二:(a = as[ThreadLocalRandom.getProbe() & m]) == null 前提条件是cells数组已经初始化过了
// true:表示当前线程命中的cell表格是个空,需要当前线程进入fullAddCount(x, uncontended)方法去初始化cell,放入当前位置
// false:表示table表格不为空
// 条件三:!(uncontended =U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
// false:取反后的false,表示当前线程使用CAS方式更新当前命中的cell成功
// true:取反后的true,表示当前线程使用CAS方式更新当前命中的cell失败,需要进入fullAddCount方法需要进行重试或者扩容cells
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
// 该方法逻辑跟LonAdder中的 longAccumulate(x, null, uncontended) 方法一致
fullAddCount(x, uncontended);
// 考虑到fullAddCount里面的事情比较多,就让当前线程不参与到扩容相关的逻辑了,直接返回到调用点
return;
}
// check的值有四种情况:
// >=1:分为两种情况:
// 1.当前线程命中的cell元素的key不与原先链表中的元素冲突的话,则check为链表的长度
// 2.当前线程命中的cell元素的key与原先链表中的元素冲突的话,则check为冲突的cell元素的位置-1
// 0:当前线程命中的cell桶位为null
// 2:当前线程命中的cell元素不为空且为TreeBin节点
// -1:当前线程正在执行remove操作
if (check <= 1)
return;
// 获取当前散列表中的元素个数,这是一个期望值
s = sumCount();
}
// 条件成立:表示一定是一个put操作调用的addCount
if (check >= 0) {
// tab:表示map.table
// nt:map.nextTable:扩容过程中,会将扩容中的新table赋值给nextTable,保持引用,扩容结束之后,这里会被设置为null
// n:表示map.table数组的长度
// sc:表示sizeCtl的临时值
Node<K,V>[] tab, nt; int n, sc;
/**
* sizeCtl < 0
* 1.-1的时候表示当前的table正在初始化(有线程正在创建table数组)当前线程需要自旋等待
* 2.表示当前map正在进行扩容,高16位表示:扩容的标识戳 低16位表示:(1 + nThread)表示当前参与并发扩容的线程数量
*
* sizeCtl == 0 表示创建table数组的时候,使用 DEFAULT_CAPACITY 为大小
*
* sizeCtl > 0
* 1. 如果table未初始化,表示初始化大小
* 2. 如果table已经初始化,表示下次扩容时的触发条件(阙值)
*/
// 自旋操作
// 条件一:s >= (long)(sc = sizeCtl)
// true:1.当前sizeCtl为一个负数,表示正在扩容中
// 2.当前sizeCtl是一个正数,表示扩容阙值
// false:1.表示当亲table尚未达到扩容条件
// 条件二:(tab = table) != null:恒成立,一直为true
// 条件三:(n = tab.length) < MAXIMUM_CAPACITY)
// true:当前table的长度小于最大值限制,则可以进行扩容
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
// 计算扩容批次的唯一标识戳
// 16 -> 32 1000 0000 0001 1011
int rs = resizeStamp(n);
// 条件成立:表示当前map.table正在进行扩容操作 当前线程应该协助table完成扩容
if (sc < 0) {
// 条件一:(sc >>> RESIZE_STAMP_SHIFT) != rs
// true:说明当前线程获取到的扩容唯一标识戳非本批次扩容
// false:说明当前线程是本批次的扩容
// 条件二:JDK1.8中有bug,jira已经提出来了,其实想表达的是:sc == (rs << 16) + 1
// true:表示扩容完毕了,当前线程不需要再参与进来了
// false:扩容还在进行中,当前线程可以参与进来
// 条件三:JDK1.8中有bug,jira已经提出来了,其实想表达的是sc == (rs << 16) + MAX_RESIZERS
// true:表示当前参与并发扩容的线程达到了最大值 65535 - 1
// false:表示当前参与线程的总数还未达到最大值。当前线程可以参与进来
// 条件四:(nt = nextTable) == null
// true:表示本次扩容已经结束,当前线程不需要参与进来
// false:扩容正在进行中,当前线程可以参与进来
// 条件四:transferIndex <= 0
// true:说明map对象全局范围内的任务已经分配完了,当前线程进去也没活干
// false:否则的话表示还有任务可以分配
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
// 前置条件:当前table正在执行扩容中,当前线程有机会参与进扩容过程中
// 条件成立:说明当前线程成功参与到扩容任务中,并且将sc低16位的值+1,表示参与扩容的线程数
// 条件失败:1.说明有很多线程都在此处尝试修改sizeCtl,有其中一个线程修改成功了,导致你的sc期望值与内存中的值不一致,修改失败
// 2.transfer方法内部的线程也成功修改了sizeCtl导致CAS失败
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
// 协助扩容线程,持有nextTable参数
transfer(tab, nt);
}
// 表示当前map.table的元素数量达到了sizeCtl(扩容扩容阙值)
// rs << RESIZE_STAMP_SHIFT):1000 0000 0001 1011 0000 0000 0000 0000
// (rs << RESIZE_STAMP_SHIFT) + 2):1000 0000 0001 1011 0000 0000 0000 0010 是一个负数
// 注意,前16位表示扩容的标识戳,后16位表示有多少个线程参与扩容 -(1+nThreads)
// 条件成立:说明当前线程是触发扩容的第一个线程,在transfer方法中需要做一些扩容准备工作
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
// 触发扩容条件的线程,不持有nextTable
transfer(tab, null);
s = sumCount();
}
}
}
小节:
- 元素个数的存储方式类似于 LongAdder 类,存储在不同的段上,减少不同线程同时更新 size 的时候的冲突。
- 计算元素个数的时候把这些段的值以及 baseCount 相加算出总的元素个数。
- 正常情况下 sizeCtl 存储着扩容阙值,扩容阙值为 map.table 中元素数量的 0.75。
- 扩容的时候sizeCtl 高16位存储着扩容标识戳 (resizeStamp);低16位存储参与扩容的线程数 + 1 (1 + nThreads).
- 其他线程添加元素后如果发现扩容还未结束,也会参与帮助扩容
上面介绍了 put()
方法以及其相关的方法,接下来,下面就开始介绍下 transfer
这个方法比较难,最好能够手动结合源码进行分析,并仔细理解上面的内容。
transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) 方法
transfer(Node<K,V>[] tab, Node<K,V>[] nextTab)
方法的作用是:迁移元素,扩容的时候 table 容量变为原来的两倍,并把所有元素迁移到其他桶 nextTable 中。该方法迁移数据的流程就是在发生扩容的时候,从散列表原 table 中,按照桶位下标,依次将每个桶中的元素(结点、链表、树)迁移到新表 nextTable 中。
另外还有与其相关的 helpTransfer(Node<K,V>[] tab, Node<K,V> f)
:协助扩容(迁移元素),当线程添加元素的时候发现对应桶位的头结点为 FWD 节点,说明table 正在扩容而且当前桶位元素已经迁移完成了,则协助迁移其他桶的元素。
下图为并发扩容流程图,在分析源码前先熟悉一下流程:
链表迁移示意图:
4、transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) 方法源码解析(难点)
/**
* 迁移元素,扩容的时候 table 容量变为原来的两倍,并把扩容元素迁移到其他桶 nextTable 中:
**/
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
// n:表示扩容之前table数组的长度
// stride:表示分配给线程任务的步长
int n = tab.length, stride;
// 方便讲解源码,stride 固定为 16
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
// 条件成立:表示当前线程为触发本次扩容的线程,需要做一些扩容准备工作
// 条件不成立:表示当前线程是协助扩容的线程
if (nextTab == null) { // initiating
try {
// 创建了一个比扩容之前大一倍的table
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
sizeCtl = Integer.MAX_VALUE;
return;
}
// 赋值给对象属性 nextTable,方便协助扩容线程拿到新表
nextTable = nextTab;
// 记录迁移数据整体位置的一个标记,index计数是从1开始计算的
transferIndex = n;
}
// nextn:表示新数组的长度
int nextn = nextTab.length;
// FWD节点:当某个桶位数据处理完毕后,将此桶位设置为FWD节点,其他写线程或者读线程看到后,会有不同的逻辑
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
// 推进标记
boolean advance = true;
// 表示完成标记
boolean finishing = false; // to ensure sweep before committing nextTab
// 自旋操作
// i:表示分配给当前线程任务,执行到的桶位
// bound:表示分配给当前线程任务的下界限制
for (int i = 0, bound = 0;;) {
// f:桶位的头结点
// fh:头结点的hash值
Node<K,V> f; int fh;
/**
* 1.给当前线程分配任务区间
* 2.维护当前线程任务进度(i 表示当前处理的桶位)
* 3.维护map对象全局范围内的进度
*/
while (advance) {
// nextIndex:分配任务开始的下标
// nextBound:分配任务结束的下标
int nextIndex, nextBound;
// CASE1:
// 条件一:--i >= bound
// true:表示当前线程的任务尚未完成,还有相应的区间的桶位要处理,--i就让当前线程处理下一个桶位
// false:表示当前线程任务已经完成或者未分配
if (--i >= bound || finishing)
advance = false;
// CASE2:(nextIndex = transferIndex) <= 0
// 条件成立:表示对象全局范围内的桶位都分配完毕了,没有区间可以分配了,设置当前线程的i变量为-1,跳出循环后,执行退出迁移任务相关的程序
// 条件不成立:说明对象全局范围内桶位尚未分配完毕,还有区间可以分配
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
// CASE3:
// 前置条件:1.当前线程需要分配任务区间 2.全局范围内还有桶位尚未迁移
// 条件成立:说明给当前线程分配任务成功
// 条件失败:说明分配给当前线程失败,应该是和其他线程发生了竞争
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
// 处理线程任务完成后,线程退出transfer方法的逻辑
// 条件一i < 0:成立:表示当前线程未分配到任务
if (i < 0 || i >= n || i + n >= nextn) {
// 保存sizeCtl的变量
int sc;
if (finishing) {
// 扩容过程中会将扩容中的新table赋值给nextTable,保持引用,扩容结束之后,这里会被设置为null
nextTable = null;
// 将数组的引用设置为nextTab
table = nextTab;
// 将sizeCtl设置扩容后的数组长度的0.75
sizeCtl = (n << 1) - (n >>> 1);
return;
}
// CASE1:
// 条件成立:说明设置sizeCtl低16位-1成功,当前线程可以正常退出
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
// 条件成立:说明当前线程不是最后一个退出transfer任务的线程
// 这里说明一下:因为addCount在进行扩容的时候,第一次达到扩容阙值进行进入的线程的低16位是 1+nThreads的值
// 之后其他的所有线程都是帮忙参与扩容的线程,都是if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))该方法进入的,将sc-1后进入的
// 所以sc的值是一直变化的,当sc-2等于resizeStamp(n) << RESIZE_STAMP_SHIFT的时候,说明是最后一个线程要退出transfer方法
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
// 正常退出
return;
// 如果是最后一个退出任务的线程
finishing = advance = true;
// 之后自旋然后走其他else -if检查是否有遗漏的桶位尚未迁移
i = n; // recheck before commit
}
}
// 前置条件:[CASE2-CASE4] 当前线程任务尚未处理完,正在进行中
// CASE2:
// 条件成立:说明当前桶位未存放数据,只需要将此处设置为FWD节点即可
/*表示线程处理一个桶位数据的迁移工作,处理完毕后设置advance为true,表示继续推进,然后就会继续自旋*/
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
// CASE3:
// 条件成立:说明当前桶位已经迁移过了,不需要在进行处理,直接再次更新当前线程任务索引,再次处理下一个桶位或者进行其他操作
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
// CASE4:
// 前置条件:当前桶位有数据而且对应桶位的头结点不是FWD节点,说明这些数据需要迁移
else {
// synchronized 加锁当前桶位的头结点
synchronized (f) {
// 这些为什么要比较一下,是为了防止CASE2获取的头结点之后,其他线程已经进入到CASE4把当前桶位头结点的改变了,导致加锁的头结点不对,导致加锁失败
if (tabAt(tab, i) == f) {
// ln:表示低位链表的引用
// hn:表示高位链表的引用
Node<K,V> ln, hn;
// 条件成立:表示当前桶位是链表桶位
if (fh >= 0) {
// lastRun机制,减少节点的创建
// 可以获取出当前链表的末尾连续高位不变的node
int runBit = fh & n;
Node<K,V> lastRun = f;
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
// 条件成立:说明lastRun 引用的链表为低位链表 那么就让ln指向低位链表
if (runBit == 0) {
ln = lastRun;
hn = null;
}
// 否则,说明lastRun 引用的链表为 高位链表,就让hn指向高位链表
else {
hn = lastRun;
ln = null;
}
// 遍历剩余的链表节点将其分别插入到高位和低位链表的头部
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);
// 把高位链表设置到对应的新表桶位中,注意,位置为i+n
setTabAt(nextTab, i + n, hn);
// 将老表对应的桶位节点设置为fwd节点
setTabAt(tab, i, fwd);
advance = true;
// 该桶位节点迁移完成,进入while循环--i去处理下一个节点
}
// 条件成立:表示当前桶位是红黑树
else if (f instanceof TreeBin) {
// 将桶位头结点转化为TreeBin节点
TreeBin<K,V> t = (TreeBin<K,V>)f;
// 低位双向链表 lo 指向低位链表的头 loTail 指向低位链表的尾部
TreeNode<K,V> lo = null, loTail = null;
// 高位双向链表 hi 指向低位链表的头 hiTail 指向低位链表的尾部
TreeNode<K,V> hi = null, hiTail = null;
// lc:表示低位链表数量
// hc:表示高位链表元素数量
int lc = 0, hc = 0;
// 迭代当前TreeBin中的双向链表,从头结点到尾节点
for (Node<K,V> e = t.first; e != null; e = e.next) {
// h:表示循环当前处理当前元素的hash值
int h = e.hash;
// 使用当前节点构建出来的新的 TreeNode节点
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;
// 低位链表的元素数量加1
++lc;
}
// 条件成立:表示当前循环节点 属于高位链表节点
else {
// 条件成立:说明当前高位链表,还没有数据
if ((p.prev = hiTail) == null)
hi = p;
// 说明当前高位链表已经有数据了,此时当前元素需要追加到高位链表的尾部
else
hiTail.next = p;
// 将当前元素设置为尾部节点
hiTail = p;
// 低位链表的元素数量加1
++hc;
}
}
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);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
}
}
}
}
}
补充,lastRun 机制示意图:
小节:
- 新 Node 数组的大小是旧数组的两倍。
- 迁移数据先从靠后的桶开始。
- 迁移完成的桶在里面放置一下 ForwardingNode 类型的元素,标记该桶迁移完成。
- 迁移的时候根据 hash & n 是否等于 0 把桶中的元素分化成两个单向或者双向链表。
- 低位链表(单向或者双向)存储在原来的位置。
- 高位链表(单向或者双向)存储在原来的位置加 数组 table的长度的位置。
- 如果迁移的桶位是红黑树的话,还会创建TreeBin节点根据双向链表来创建红黑树。
- 迁移元素的时候会锁住当前桶,也是分段锁的思想;
5、helpTransfer(Node<K,V>[] tab, Node<K,V> f) 方法源码解析
helpTransfer(Node<K,V>[] tab, Node<K,V> f)
:协助扩容(迁移元素),当线程添加元素的时候发现数组 table 正在扩容,且当前元素所在的桶元素已经迁移完成了,则协助迁移其他桶的元素。当前桶迁移完成了才去协助迁移其他桶的元素。
// 进入该方法的前置条件:putVal()方法命中的桶位节点的hash值为MOVED,即当前桶位节点正处于扩容状态
// tab:扩容之前的table数组
// f:命中的桶位的头结点
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
// nextTab 引用的是 FWD节点的nextTab,FWD.nextTable = map.nextTable 理论上是这样的
// sc:map.sizeCtl 的临时值
Node<K,V>[] nextTab; int sc;
// 条件一:tab != null 永远不为空,恒成立
// 条件二:f instanceof ForwardingNode 恒成立,看进入该方法的前置条件
// 条件三:(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null 恒成立
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
// 拿当前表的长度获取扩容标识戳 假设 16 -> 32 1000 0000 0001 1011
int rs = resizeStamp(tab.length);
// 条件一:nextTab == nextTable
// 成立:表示当前扩容正在进行中
// 不成立:1.nextTab 被设置为null,扩容完毕后,nextTab会被设置为null
// 2.再次触发扩容,咱们拿到的 nextTab已经过期了
// 条件二:table == tab
// 成立:说明扩容还未完成,正在进行中
// 扩容完成:说明扩容已经结束了,最后退出的线程在检查完所有桶位的迁移情况后会设置nextTab为table
// 条件三:(sc = sizeCtl) < 0
// 条件成立:说明扩容还在进行中
// 条件不成立:说明sizeCtl 当前是一个大于0的数,此时代表下次触发扩容的阙值
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
// 条件一:(sc >>> RESIZE_STAMP_SHIFT) != rs
// true:说明当前线程获取到的扩容唯一标识戳非本批次扩容
// false:说明当前线程是本批次的扩容
// 条件二:JDK1.8中有bug,jira已经提出来了,其实想表达的是:sc == (rs << 16) + 1
// true:表示扩容完毕了,当前线程不需要再参与进来了
// false:扩容还在进行中,当前线程可以参与进来
// 条件三:JDK1.8中有bug,jira已经提出来了,其实想表达的是sc == (rs << 16) + MAX_RESIZERS
// true:表示当前参与并发扩容的线程达到了最大值 65535 - 1
// false:表示当前参与线程的总数还未达到最大值。当前线程可以参与进来
// 条件四:(nt = nextTable) == null
// true:表示本次扩容已经结束,当前线程不需要参与进来
// false:扩容正在进行中,当前线程可以参与进来
// 条件四:transferIndex <= 0
// true:说明map对象全局范围内的任务已经分配完了,当前线程进去也没活干
// false:否则的话表示还有任务可以分配
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || transferIndex <= 0)
break;
// 前置条件:当前table正在执行扩容中,当前线程有机会参与进扩容过程中
// 条件成立:说明当前线程成功参与到扩容任务中,并且将sc低16位的值+1,表示参与扩容的线程数
// 条件失败:1.说明有很多线程都在此处尝试修改sizeCtl,有其中一个线程修改成功了,导致你的sc期望值与内存中的值不一致,修改失败
// 2.transfer方法内部的线程也成功修改了sizeCtl导致CAS失败
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab);
break;
}
}
return nextTab;
}
return table;
}
6、get(Object key) 方法源码解析
get(Object key)
方法:获取元素,根据目标 key 所在桶的第一个元素的不同采用不同的方式获取元素,关键点在于 find()
方法的重写。
public V get(Object key) {
// tab:引用map.table
// e:当前元素
// p:目标节点
// n:table数组的长度
// eh:当前元素的hash
// ek:当前元素的key
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
// 通过扰动函数得到的更散列的hash值
int h = spread(key.hashCode());
// 条件一:(tab = table) != null
// true:表示已经put过数据,并且map内部的table也已经初始化完毕
// false:表示创建完map后,并没有put过数据,map内部的table是延迟初始化的,只有第一次写数据的时候会触发
// 条件二:(n = tab.length) > 0
// 条件三:(e = tabAt(tab, (n - 1) & h)) != null
// true:表示当前key寻址的桶位有值
// false:表示key寻址的桶位中是null,是null的话直接返回null
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
// 前置条件:当前桶位有数据
// 对比头结点与查询值通过扰动函数的hash是否一致
// 条件成立:说明头结点与查询key的hash值完全一致(hash值一致不代表key值一致)
if ((eh = e.hash) == h) {
// 完全比对:查询key 头结点的 key
// 条件成立:说明头结点就是索要查询的数据
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
// 当hash值小于0
// -1:FWD节点,说明当前table正在扩容,且当前查询桶位的数据已经被迁移走了
// -2:TreeBin节点,需要使用 TreeBin节点提供的find()方法查询
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
// 说明当前桶位已经形成链表
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
6.1、ForwardingNode 内部类(FWD 节点)
在 get
方法的 CASE2 中,eh < 0
会分为两种情况:
- 情况一:
eh == -1
是FWD 节点 => 说明当前 table 正在扩容,且当前查询的这个桶位的数据已经被迁移走了,需要借助 FWD 节点的内部方法find
去查询。 - 情况二:
eh == -2
是 TreeBin 节点 => 需要使用 TreeBin 节点提供的find
方法查询。
下面旧分析一下情况一,即当前桶位中是 FWD 节点,我们来分析一下 FWD 这个内部类,以及其内部的 find
方法:
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;
}
Node<K,V> find(int h, Object k) {
// loop to avoid arbitrarily deep recursion on forwarding nodes
// 自旋 tab:一定不为空 整个 ConcurrentHashMap 源码中只有一个地方实例化 ForwardingNode,就是在transfer迁移数据方法里面
// ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
//(当某个桶位数据处理完毕后,将此桶位设置为fwd节点,其它写线程或读线程看到后,会有不同逻辑)
outer: for (Node<K,V>[] tab = nextTable;;) {
// e:表示在扩容而创建的新表使用寻址算法得到的桶位头结点
// n:表示为扩容而创建的新表的长度
Node<K,V> e; int n;
// 条件一:k == null 永远不成立
// 条件二:tab == null 永远不成立
// 条件三:(n = tab.length) == 0 永远不成立
// 条件四:(e = tabAt(tab, (n - 1) & h)) == null 在扩容的表中重新定位hash对应的头结点
// true:1.在oldTable对应的桶位在迁移之前就是null
// 2.在扩容完成后,有其他线程写线程,将此桶位设置为了null
if (k == null || tab == null || (n = tab.length) == 0 ||
(e = tabAt(tab, (n - 1) & h)) == null)
return null;
// 前置条件:扩容后的表对应的hash所在的桶位一定不是null,e为此桶位的头结点
// e可能为哪些类型?
// 1.node类型
// 2.TreeBin类型
// 3.FWD类型
// 自旋操作
for (;;) {
// eh:表示新扩容后表指定桶位的当前节点的hash
// ek:表示新扩容后表指定桶位的当前节点的key
int eh; K ek;
// 条件成立:说明新扩容后的表,当前命中桶位中的数据,即为查询想要的数据
if ((eh = e.hash) == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
// eh<0:有两种情况:
// 1.TreeBin 类型 2.FWD 类型(新扩容的表,在并发很大的情况下,可能在此方法中,再次拿到FWD节点)
if (eh < 0) {
//
if (e instanceof ForwardingNode) {
tab = ((ForwardingNode<K,V>)e).nextTable;
continue outer;
}
// 说明当前新表中的该桶位中的头节点是TreeBin节点,使用TreeBin.find查找红黑树中的相应节点
else
return e.find(h, k);
}
// 前置条件:当前桶位的头结点,并没有命中查询,说明此桶位是链表
// 1.将当前元素指向链表的下一个元素
// 2.判断当前元素的下一个位置是否为空,条件成立:说明迭代到链表的末尾未找到对应的数据
if ((e = e.next) == null)
return null;
}// 退出内部自旋操作
}
}
}
小节:
- hash 到元素所在的桶位
- 如果桶中的第一个元素就是该找的元素,直接返回。
- 如果是树或者正在扩容,则调用各自的 Node 子类的
find()
方法寻找元素 - 如果是链表,遍历整个链表寻找元素
- 获取元素没有加锁
7、remove(Object key) 方法源码解析
remove(Object key)
方法:删除元素跟添加元素一样,都是先找到元素所在的桶,然后采用分段锁的思想锁住整个桶,再进行操作。
()// 删除方法
public V remove(Object key) {
// 调用节点替换方法
return replaceNode(key, null, null);
}
/**
* 结点替换:
* 参数1:Object key -> 就表示当前结点的key
* 参数2:V value -> 要替换的目标值
* 参数3:Object cv(compare Value) ->
* 如果cv不为null,则需要既比对key,还要比对cv,这样个参数都一致,才能替换成目标值
*/
final V replaceNode(Object key, V value, Object cv) {
// 计算key经过扰动运算后的hash
int hash = spread(key.hashCode());
// 自旋操作
for (Node<K,V>[] tab = table;;) {
// f:表示桶位头结点
// n:表示当前table数组的长度
// i:表示hash命中桶位下标
// fh:表示桶位头结点的hash
Node<K,V> f; int n, i, fh;
// CASE1:
// 条件一:tab == null true:表示当前map.table尚未初始化 false:已经初始化
// 条件二:(n = tab.length) == 0 true:表示当前map.table尚未初始化 false:已经初始化
// 条件三:f = tabAt(tab, i = (n - 1) & hash)) == null 获取当前桶位的头结点
// true:表示命中桶位中的头结点为null
if (tab == null || (n = tab.length) == 0 ||
(f = tabAt(tab, i = (n - 1) & hash)) == null)
break;
// CASE2:
// 前置条件:当前hash值命中的桶位的头结点不为null
// 条件成立:说明当前table正在扩容中,当前是个写操作,所以当前线程需要协助table完成扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
// CASE3:
// 前置条件:当前hash值命中的桶位的头结点不为null并且当前table也没有在扩容
// 当前桶位可能是链表,也可能是红黑树
else {
// oldVal:保留替换之前的数据引用
V oldVal = null;
// 校验标记
boolean validated = false;
// 加锁当前桶位,头结点,加锁成功之后会进入代码块
synchronized (f) {
// 判断synchronized 加锁是否为当前桶位的头结点,防止其它线程在当前线程加锁成功之前修改过桶位的头结点
// 条件成功:其他桶位头结点仍然为f,其他线程没有修改过
if (tabAt(tab, i) == f) {
// 条件成立:说明桶位为链表或者单个元素
if (fh >= 0) {
validated = true;
// e:表示当前循环处理元素
// pred:表示当前循环节点的上一个节点
for (Node<K,V> e = f, pred = null;;) {
// ek:当前节点的key
K ek;
// 条件一:e.hash == hash true:说明当前节点的hash与查找节点的hash一致
// 条件二: (ek = e.key) == key||(ek != null && key.equals(ek)
// if 条件成立:说明key与查询的key完全一致
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
// 当前节点的value值
V ev = e.val;
// 条件一:cv == null 表示要替换的值为null,那么就是一个删除操作
// 条件二:cv == ev ||(ev != null && cv.equals(ev) 说明是一个替换操作
if (cv == null || cv == ev ||
(ev != null && cv.equals(ev))) {
// 删除或者替换操作
// 将当前节点的值赋值给oldVal,后续返回会用到
oldVal = ev;
// 条件成立:value是参数,说明当前是一个替换操作
if (value != null)
// 直接替换
e.val = value;
// 条件成立:说明当前节点是非头结点
else if (pred != null)
// 当前节点的上一个节点,指向当前节点的下一个节点
pred.next = e.next;
else
// 说明当前节点即为头结点,只需要将桶位设置为头结点的下一个节点
setTabAt(tab, i, e.next);
}
break;
} // key 一致,根据value进行比较
// key 不一致
pred = e;
if ((e = e.next) == null)
break;
}
}
// 条件成立:TreeBin节点
else if (f instanceof TreeBin) {
validated = true;
// 转化为实际类型的TreeBin节点
TreeBin<K,V> t = (TreeBin<K,V>)f;
// r:表示红黑树根节点
// p:表示红黑树中查找到对应key一致的node节点
TreeNode<K,V> r, p;
// 条件一:(r = t.root) != null 理论上是成立的
// 条件二:p = r.findTreeNode(hash, key, null)) != null findTreeNode查找以当前节点为入口向下查找元素,包括本身节点
// 成立:说明查找到相应的key对应的node节点了
if ((r = t.root) != null &&
(p = r.findTreeNode(hash, key, null)) != null) {
// 保存查找到的节点的value值到pv中
V pv = p.val;
// 条件一:cv == null 成立:不比对value就做替换或者删除操作
// 条件二:cv == pv || (pv != null && cv.equals(pv) 成立:说明cv(对比值)与当前p节点的值一致
if (cv == null || cv == pv ||
(pv != null && cv.equals(pv))) {
// 替换或者删除操作
oldVal = pv;
// 条件成立:替换
if (value != null)
p.val = value;
// 条件成立:删除
else if (t.removeTreeNode(p))
// 这里没做判断,直接将红黑树转化为普通链表
setTabAt(tab, i, untreeify(t.first));
}
}
}
}
}
// 当其他线程修改过桶位头结点的时候,当前线程synchronized头结点锁错对象的时候,validated会进入下次自旋
if (validated) {
// oldVal != null 成立:说明删除或者替换值成功
if (oldVal != null) {
// 替换的值为null,说明当前是一次删除操作,需要更新当前元素个数
if (value == null)
// 当该方法是从remove方法中调用的时候,第二个参数为-1,不会进行扩容操作
addCount(-1L, -1);
return oldVal;
}
break;
} // if
} // else
} // for
return null;
}
- 通过扰动函数 spread() 计算 hash值
- 如果所在的桶不存在,表示没有找到目标元素,返回
- 如果正在扩容,则协助扩容完成后再进行删除操作
- 如果是以链表形式进行存储的,则遍历整个链表查找元素,找到之后再删除
- 如果是以红黑树形式进行存储的,则遍历查找红黑树中的元素,找到之后再删除
- 如果是以树形式存储的,删除元素之后树比较小,则退化成链表
- 如果确实删除了元素,则整个 map 元素个数减一,并返回旧值
- 如果没有删除元素,则返回 null
8、TreeBin 内部类(红黑树的代理类)源码分析
- TreeBin 是红黑树的代理类,对红黑树不了解的,可以参考:红黑树学习笔记(自己实现一个红黑树)
static final class TreeBin<K,V> extends Node<K,V> {
// 红黑树,根节点
TreeNode<K,V> root;
// 链表的头结点
volatile TreeNode<K,V> first;
// 等待者线程(当前lockState是读锁状态)
volatile Thread waiter;
/**
* 1.写锁状态 写是独占状态,以散列表来看,真正进入到TreeBIn中的写线程 同一时刻只有一个线程 1
* 2.读锁状态 读锁是共享的,同一时刻可以有多个线程同时进入到TreeBin对象中去获取数据,每一个线程都会给lockState + 4
* 3.等待者状态(写线程在等待),当TreeBin中有读线程目前正在读取数据的时候,写线程无法修改数据结构,那么就将 localState最低两位设置为10
*/
volatile int lockState;
// 写锁状态
static final int WRITER = 1; // set while holding write lock
// 等待者状态
static final int WAITER = 2; // set when waiting for write lock
// 读锁状态
static final int READER = 4; // increment value for setting read lock
// 当转化为红黑树的时候,hash值相同和key值相同的情况下,利用对象的地址hash值System.identityHashCode(a)计算出dir的值
// 以此来保证插入节点在当前节点的插入位置
static int tieBreakOrder(Object a, Object b) {
int d;
if (a == null || b == null ||
(d = a.getClass().getName().
compareTo(b.getClass().getName())) == 0)
d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
-1 : 1);
return d;
}
// b:当前桶位的头结点
TreeBin(TreeNode<K,V> b) {
// 设置节点的hash值为-2,表示此节点是TreeBin节点
super(TREEBIN, null, null, null);
// 使用first 引用 TreeNode链表
this.first = b;
// r:红黑树的根节点引用
TreeNode<K,V> r = null;
// x:表示遍历的当前节点
// next:表示遍历的当前节点的下一个节点
for (TreeNode<K,V> x = b, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next;
// 强制设置当前插入节点的左右子树为null
x.left = x.right = null;
// 条件成立:说明当前红黑树是一个空树,那么设置插入元素为头结点
if (r == null) {
// 根节点的父节点一定为null
x.parent = null;
// 根节点一定为黑色
x.red = false;
// 让当前节点作为红黑树的根节点
r = x;
}
else {
// 非第一次循环都会来到else分支,此时根节点已经有数据了
// k:表示当前插入节点的key
// h:表示当前插入节点的hash值
K k = x.key;
int h = x.hash;
// kc:表示当前插入节点的key 的Class类型
Class<?> kc = null;
// p:表示当前插入节点的父节点的临时节点
for (TreeNode<K,V> p = r;;) {
// dir:(-1,1)
// -1:表示插入节点的hash值小于当前p节点的hash值
// 1:表示插入节点的hash值大于当前p节点的hash值
// ph:表示当前插入节点的父节点的临时节点的hash值
int dir, ph;
// 临时节点的key
K pk = p.key;
// 当前插入节点的hash值小于当前节点,插入节点可能需要插入到当前节点的左子节点或者继续在左子树上查找
if ((ph = p.hash) > h)
dir = -1;
// 当前插入节点的hash值大于当前节点,插入节点可能需要插入到当前节点的右子节点或者继续在右子树上查找
else if (ph < h)
dir = 1;
// 当前节点的hash值与插入节点的hash值一致的情况下,这种情况下会做出最终排序
// 最终拿到的dir一定不是0
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
// 根据对象内存地址的hash值来确定dir的值(当key值一样的时候)
dir = tieBreakOrder(k, pk);
// xp:表示的是插入节点的父节点
TreeNode<K,V> xp = p;
// 条件成立:说明当前p节点即为插入节点要插入的位置,因为这里p节点已经该改变了,所以里面使用xp来作为父节点
// 条件不成立:说明p节点底下还有层次,需要将p根据dir的值指向p的左子节点或者p的右子节点,需要继续循环
if ((p = (dir <= 0) ? p.left : p.right) == null) {
// 设置插入节点的父节点为当前节点
x.parent = xp;
// 根据dir判断插入节点在父节点的左子节点还是右子节点
if (dir <= 0)
xp.left = x;
else
xp.right = x;
// 插入节点以后红黑树性质可能会被破坏 需要调用平衡方法保证红黑树的平衡
r = balanceInsertion(r, x);
break;
}
} // for 寻找插入位置
} // else
} // for 插入多个节点
// 将 r 赋值给TreeBin对象的roit引用
this.root = r;
assert checkInvariants(root);
}
/**
* Acquires write lock for tree restructuring.
*/
private final void lockRoot() {
// 条件成立:说明localState并不是0,说明此时有其他读线程在TreeBin红黑树中读取数据
// 如果当前没有任何读线程的情况下,对其进行加写锁
if (!U.compareAndSwapInt(this, LOCKSTATE, 0, WRITER))
// 对TreeBin对象加写锁失败,则执行该方法
contendedLock(); // offload to separate method
}
/**
* Releases write lock for tree restructuring.
*/
private final void unlockRoot() {
// 释放写锁直接将localState置为0即可
lockState = 0;
}
// 当添加数据并且会导致红黑树结构发生变化的时候会给TreeBin对象加写锁,加锁失败的时候会调用该方法
private final void contendedLock() {
// waiting:表示目前不等待
boolean waiting = false;
// s:表示localState的临时值
for (int s;;) {
// (s = lockState) & ~WAITER) == 0
// 条件成立:说明目前TreeBin中没有读线程在访问红黑树
// 条件失败:有读线程在访问红黑树
if (((s = lockState) & ~WAITER) == 0) {
// 条件成立:说明写线程抢占锁成功
if (U.compareAndSwapInt(this, LOCKSTATE, s, WRITER)) {
if (waiting)
// 设置waiter等待者线程为null
waiter = null;
return;
}
}
// 前置条件:有读线程在访问红黑树中的数据
// localState & 00000000... 10 == 0 条件成立:说明锁localState中waiter的标志位为0
// (说明此时没写线程在等待),此时当前线程可以设置为1,然后将当前线程挂起
else if ((s & WAITER) == 0) {
// 改变localState标志位为localState + WAITER(2)
if (U.compareAndSwapInt(this, LOCKSTATE, s, s | WAITER)) {
waiting = true;
// 等待者线程设置为当前线程
waiter = Thread.currentThread();
}
}
// 条件成立:说明当前线程在CASE2中已经将TreeBin.waiter设置为了当前线程,并且将localState中表示等待者标记位设置为了1
// 这个时候,就让当前线程挂起
else if (waiting)
// 挂起当前线程
LockSupport.park(this);
}
}
// 向红黑树中添加元素
final TreeNode<K,V> putTreeVal(int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;
for (TreeNode<K,V> p = root;;) {
int dir, ph; K pk;
if (p == null) {
first = root = new TreeNode<K,V>(h, k, v, null, null);
break;
}
else if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((pk = p.key) == k || (pk != null && k.equals(pk)))
return p;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
if (!searched) {
TreeNode<K,V> q, ch;
searched = true;
if (((ch = p.left) != null &&
(q = ch.findTreeNode(h, k, kc)) != null) ||
((ch = p.right) != null &&
(q = ch.findTreeNode(h, k, kc)) != null))
return q;
}
dir = tieBreakOrder(k, pk);
}
// xp:当前节点的值,说明要插入的节点就位于xp节点左子节点或者右子节点(左子树或者右子树)中
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
// 说明当前循环的节点p即为要插入节点的位置
// x:表示新插入的节点
// f:表示修改链表之前的链表的头结点(老的头结点)
TreeNode<K,V> x, f = first;
// 注意,这里在插入链表的时候使用的是头插法
first = x = new TreeNode<K,V>(h, k, v, f, xp);
// 条件成立:说明链表有数据(头插法)
if (f != null)
// 设置老的头结点的前置节点为当前插入节点
f.prev = x;
// 条件成立:根据dir来判断当前插入节点应该位于父节点xp的左子节点还是右子节点
if (dir <= 0)
xp.left = x;
else
xp.right = x;
// 如果父节点为黑色,则直接将插入节点设置为红色即可
if (!xp.red)
x.red = true;
// 父节点为红色的情况
else {
// 表示当前新插入节点后,新插入节点与父节点形成 红红相连 的情况
lockRoot();
try {
// 平衡红黑树使其再次平衡,调整树结构的过程中,会让树结构发生改变,所以需要对其进行加锁
root = balanceInsertion(root, x);
} finally {
unlockRoot();
}
}
break;
}
}
assert checkInvariants(root);
return null;
}
// 将链表转化为红黑树
// tab:map.table的引用
// index:当前命中桶位的下标
private final void treeifyBin(Node<K,V>[] tab, int index) {
// b:命中桶位的头结点
// n:map.table的数组的长度
// sc:sizeCtl 的临时值
Node<K,V> b; int n, sc;
if (tab != null) {
// 条件成立:说明当前table散列表的长度未达到64(树化条件),此时不进行树化操作,进行扩容操作
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
tryPresize(n << 1);
// 条件成立:说明当前桶位有数据,且是普通node数据
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
// 给当前桶位的头节点加锁
synchronized (b) {
// 条件成立:说明当前桶位的头结点没有变化,加锁成功
if (tabAt(tab, index) == b) {
// 将当前桶位的头结点的单向链表转化为TreeNode的双向链表
// hd:双向链表头结点
// tl:双向链表的尾节点
TreeNode<K,V> hd = null, tl = null;
for (Node<K,V> e = b; e != null; e = e.next) {
// 通过当前普通节点新创建TreeNode节点
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;
}
// 将当前桶位的头结点设置为TreeBin节点,调用了new TreeBin方法
setTabAt(tab, index, new TreeBin<K,V>(hd));
}
}
}
}
}
// h:当前查找元素的hash值
// k:当前查找元素的key值
final Node<K,V> find(int h, Object k) {
if (k != null) {
// e:表示循环迭代的当前元素(迭代的是链表 first代表的是链表的头结点)
for (Node<K,V> e = first; e != null; ) {
// s:保存的是localState的临时状态
// ek:链表当前节点的key值
int s; K ek;
// WAITER|WRITER => 0010 | 0010 => 0011
// localState & 0011 != 0 条件成立:说明当前TreeBin 有等待者线程 或者 目前有写线程正在加锁
if (((s = lockState) & (WAITER|WRITER)) != 0) {
if (e.hash == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
e = e.next;
}
// 前置条件:当前TreeBin中 没有等待者线程或者写线程加锁,说明当前TreeBin对象位于读锁状态
// 条件成功:说明添加读锁成功
else if (U.compareAndSwapInt(this, LOCKSTATE, s,
s + READER)) {
TreeNode<K,V> r, p;
try {
// 寻找要查询的节点
p = ((r = root) == null ? null :
r.findTreeNode(h, k, null));
} finally {
// w:表示等待者线程
Thread w;
// 1.U.getAndAddInt(this, LOCKSTATE, -READER):注意:这个方法的返回值是先get值然后在交换值,所以返回值是变化之前的值
// U.getAndAddInt(this, LOCKSTATE, -READER) == (READER|WAITER)
// 1.当前线程查询红黑树结束,释放当前线程的读锁即 localState -= 4
// (READER|WAITER) => 0110 => 表示当前只有一个线程在读,且"只有一个线程在等待"
// 表示当前读线程是TreeBin对象中的最后一个读线程
// 2.(w = waiter) != null:当前成员变量waiter等待者线程不为空,说明有一个写线程在等待读操作结束
if (U.getAndAddInt(this, LOCKSTATE, -READER) ==
(READER|WAITER) && (w = waiter) != null)
// 使用unpark让 写线程恢复运行状态
LockSupport.unpark(w);
}
return p;
}
}
}
return null;
}
}
至此,ConcurrentHashMap 源码已经大致分析完成了,如有错误,请指正,感谢大家哈!