一、为什么引入 ConcurrentHashMap 1.8 ?
- JDK 1.7 采用分段锁思想,整个 Hash 表被分成多个段,每个段中会对应一个 Segment 段锁,段与段之间可以并发访问,但是多线程想要操作同一个段是仍需要获取锁的。
- JDK 1.8 在控制并发方面则取消了基于 Segment 的分段锁思想,改用 CAS + synchronized 控制并发操作;在底层数据结构使用 Node 数组+链表+红黑树,但为了兼容 jdk1.7,若仍保留了 segment 这个数据结构。
二、源码阅读
(1) 底层数据结构
成员变量定义了 ConcurrentHashMap 一些边界值
// node数组最大容量:2^30=1073741824
private static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认初始值,必须是2的幕数
private static final int DEFAULT_CAPACITY = 16;
//数组可能最大值,需要与toArray()相关方法关联
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
//并发级别,遗留下来的,为兼容以前的版本
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
// 负载因子
private static final float LOAD_FACTOR = 0.75f;
// 链表转红黑树阀值,> 8 链表转换为红黑树
static final int TREEIFY_THRESHOLD = 8;
//树转链表阀值,小于等于6(tranfer时,lc、hc=0两个计数器分别++记录原bin、新binTreeNode数量,<=UNTREEIFY_THRESHOLD 则untreeify(lo))
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;
private static final int MIN_TRANSFER_STRIDE = 16;
private static int RESIZE_STAMP_BITS = 16;
// 2^15-1,help resize的最大线程数
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
// 32-16=16,sizeCtl中记录size大小的偏移量
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
// forwarding nodes的hash值
static final int MOVED = -1;
// 树根节点的hash值
static final int TREEBIN = -2;
// ReservationNode的hash值
static final int RESERVED = -3;
// 可用处理器数量
static final int NCPU = Runtime.getRuntime().availableProcessors();
//存放node的数组
transient volatile Node<K,V>[] table;
/*控制标识符,用来控制table的初始化和扩容的操作,不同的值有不同的含义
x = 0:默认值
x = -1:代表哈希表正在进行初始化
x < 0:相当于 HashMap 中的 threshold,表示阈值
x < -1:代表有多个线程正在进行扩容*/
private transient volatile int sizeCtl;
再看一下底层数据结构 Node,它实现了 HashMap 中的 Entry,所以 Node 只不过是一个键值对。
static class Node<K,V> implements Map.Entry<K,V> {
//链表的数据结构
final int hash;
final K key;
//val和next都会在扩容时发生变化,所以加上volatile来保持可见性和禁止重排序
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; }
//不允许更新value
public final V setValue(V value) {
throw new UnsupportedOperationException();
}
public final boolean equals(Object o) {
Object k, v, u; Map.Entry<?,?> e;
return ((o instanceof Map.Entry) &&
(k = (e = (Map.Entry<?,?>)o).getKey()) != null &&
(v = e.getValue()) != null &&
(k == key || k.equals(key)) &&
(v == (u = val) || v.equals(u)));
}
//用于map中的 get() 方法,子类重写
Node<K,V> find(int h, Object k) {
Node<K,V> e = this;
if (k != null) {
do {
K ek;
if (e.hash == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
} while ((e = e.next) != null);
}
return null;
}
}
红黑树节点 TreeNode 继承了 Node
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;
}
}
TreeBin 用作树的头结点,只存储 root 和 first 节点,不存储节点的key、value值。
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; // 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
//...
}
ForwardingNode 在数据转移的时候放在头部的节点,是一个空节点。
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;
}
}
(2) 构造方法
public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0) // 初始容量小于0,抛出异常
throw new IllegalArgumentException();
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY : tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1)); // 找到最接近该容量的2的幂次方数
this.sizeCtl = cap;
}
public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
this.sizeCtl = DEFAULT_CAPACITY;
putAll(m); // 将集合m的元素全部放入
}
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
this(initialCapacity, loadFactor, 1);
}
public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (initialCapacity < concurrencyLevel) // Use at least as many bins
initialCapacity = concurrencyLevel; // as estimated threads
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
int cap = (size >= (long)MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY : tableSizeFor((int)size);
this.sizeCtl = cap;
}
观察最后一个构造方法发现新增了一个 sizeCtl 变量,它是怎么得来的呢?
总结一下:
- 该初始化过程通过指定的初始容量 initialCapacity,加载因子 loadFactor 和预估并发度concurrencyLevel 三个参数计算出一个最小的且大于等于 initialCapacity 大小的 2 的 n 次幂数,即 table 数组的初始大小 sizeCtl
- 若 initialCapacity 为 15,则 sizeCtl 为 16
- 但如果 initialCapacity 大小超过了允许的最大值,则 sizeCtl 为最大值
(3) put 方法
public V put(K key, V value) {
return putVal(key, value, false);
}
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; // 用于记录相应链表的长度
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 如果数组"空",进行 Node 数组初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// 通过 hash 值算出对应的数组下标,从内存中得到第一个节点 f,并检查是否为空 ,如果是再用cas赋值
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 用一次 CAS 操作尝试将新值 f 放入位置 i
// 如果 CAS 失败,说明此时有并发操作使得该位置不为空,那么直接进入下一次循环
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
break;
}
// 走到这里,f 就不为空了;如果整个Map正在扩容
else if ((fh = f.hash) == MOVED)
// 帮助数据迁移,这个等到看完数据迁移部分的介绍后,再理解这个就很简单了
tab = helpTransfer(tab, f);
else { // 到这里就是说,f 是该位置的非空头结点
V oldVal = null;
// 获取数组该位置的头结点的同步锁
synchronized (f) {
if (tabAt(tab, i) == f) {//因为在获取锁的过程中,可能被其他线程改变,所以再次检查是否等于原值
if (fh >= 0) { // 头结点的 hash 值大于 0,说明是链表
binCount = 1; // 记录链表的长度
// 遍历链表
for (Node<K,V> e = f;; ++binCount) {
K ek;
// 如果发生hash冲突且遇到"相等"的 key,则进行值覆盖
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) {
pred.next = new Node<K,V>(hash, key, value, null);
break;
}
}
} // 红黑树,这里将 TreenNode 改为 TreeBin 其实是为了配合 Synchronize,原因如下:
//如果此处的结点为红黑树,如果按照 HashMap 的方式去判断并插入,会导致此处的头结点会发生变化,而变化之后锁住的对象就不是根节点了。
//而如果此处是一个 TreeBin,线程修改的只是 TreeBin 里面的红黑树,无论里面的树怎么改变,此处的锁亦然不变。
//这种 synchronized (f) {...},写法在 put 方法中也可见(f是TreeBin类型)
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}//元素个数
if (binCount != 0) {
// 判断链表长度是否 > 8
if (binCount >= TREEIFY_THRESHOLD)
// 如果当前数组的总元素 < 64,那么会选择进行数组扩容,而不是转换为红黑树
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
put 方法的篇幅较长,总结一下:
- 根据 key 的 hashcode 值算出 hash 值,遍历内部的 table,并判断 tab 是否为空:
- 如果 tab 为空,则对 tab 进行初始化
- tab 不空则根据 hash 值得到 tab 的下标 i,拿到获得该位置头结点 f,判断 f 是否为空:
- 如果 cas 检查到 f 为空,则将新对象放到位置 i,并结束 tab 的遍历。
- 如果
f.hash == MOVED
则表示 此时数组正在扩容,则会去尝试协助其他线程扩容。 - 如果 f 不空,则尝试获取结点 f 的同步锁:
- 如果拿到锁,就判断 f 的类型:
- 如果是链表,检查是否有 hash 冲突和 key 相同的情况,如果
onlyIfAbsent = false
则将旧值进行覆盖;如果没有冲突则使用尾插法将结点插入。 - 如果是红黑树,则执行红黑树的插入方法。
- 如果是链表,检查是否有 hash 冲突和 key 相同的情况,如果
- 如果拿到锁,就判断 f 的类型:
- 最后添加结点成功就调用 addCount 方法统计 size,并且在 addCount 方法中检查是否需要扩容。
- Q:那 put 方法是在哪些方面提供了线程安全保障的呢?如果怎么保证线程的呢?
- A:时机有:初始化数组时、添加结点是、以及扩容时。
3.1 initTable 方法
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
// thread2在执行下面的compareAndSwapInt方法cas不成功,那么thread2会到这个分支
// 从而交出 CPU 等待下次系统调度
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
// 一定是只有一个线程CAS成功,如果当前线程可以将 sizeCtl 设置为 -1,代表抢到了机会
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {//有可能在这里已经别其他线程初始化成功,所以再次判断一下
if ((tab = table) == null || tab.length == 0) {
// DEFAULT_CAPACITY 默认初始容量是 16
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
// 初始化数组,长度为 16 或初始化时提供的长度
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
// 将这个数组赋值给 table,table 是 volatile 的
table = tab = nt;
// 如果 n 为 16 的话,那么这里 sc = 12;n - n/4 = 0.75 × n
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;// 设置 sizeCtl 为 sc,我们就当是 12 吧
}
break;
}
}
return tab;
}
初始化方法不难,也总结一下:
- 如果 tab 不空就直接返回。
- 如果 tab 为空,每个并发线程就使用 cas 去竞争初始化机会:
- 如果某一个线程竞争不到机会,就会放弃竞争,自旋至 tab 不空为止。
- 否则,就对 tab 进行初始化。
注: sizeCtl 默认为 0,sizeCtl 中记录 size 大小的偏移量,用来控制 table 的初始化和扩容操作.它的数值有以下含义:
-1
:代表 table 正在初始化,其他线程应该交出 CPU 时间片,退出-N
: 表示正有 N-1 个线程执行扩容操作>0
:表示 tab 已经初始化,代表 tab 容量,默认为 tab 大小的 0.75 倍,如果还未初始化,代表需要初始化的大小。
按照代码的执行顺序,如果没有初始化,那么可能就会进行扩容 transfer,下面就来聊一下扩容...
我们知道在 jdk1.7 中,最大并发量就是就是 seg 的个数,在存在并发操作时,虽然这样设计使得 seg 对象在扩容时不会影响到其他 seg 对象,但如果该 seg 正在扩容,其他线程还得等到扩容完毕才能对 seg 对象进行读写,因此扩容效率就成为了并发的一个瓶颈;
jdk1.8 就对这个问题进行了一个优化:首先 JDK1.8 去掉了分段锁,将锁的级别控制在了更细粒度的 Node 元素级别,同时作者 Doug lea 大神就认为既然其他线程闲着也是闲着,不如一起参与扩容吧,于是在 jdk1.8 中就引入了一个 ForwardingNode 类以及一个 sizeCtl 来控制 table 的初始化和扩容操
3.2 helpTransfer 方法
我们在 put 方法中发现当 (fh = f.hash) == MOVED
成立时,会进入 helpTransfer 方法中。里面涉及到了一个ForwardingNode类,先讲讲它是干什么的:
/**
* A node inserted at head of bins during transfer operations.
*/
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;
}
...
}
字面意思是:当整个 table 的某个位置正在进行扩容时,会把一个 ForwardingNode 类型的结点插在 table 的某个 tab[i] 的头部,注意:并不是某个位置有 ForwardingNode 就表示扩容完全结束了,其他位置可能没有。
作用不难想到:
- 一个用于连接两个 table 的节点类。它包含一个 nextTable 指针,用于指向下一张表。而且这个节点的 key value next 指针全部为n ull,它的 hash 值为 -1
- 在扩容时,线程 t1 会先判断该槽点是否为空:
- 如果为空,t1 就会将此处的第一个设置为 forwordingNode,告诉其他线程此位置的数据迁移已有线程包办。
- 不为空,则采用头插法先把此位置的数据给迁移到新的数组中,最后给旧 table 的原位置赋值为 fwd。
helptransfer参考:https://www.jianshu.com/p/39b747c99d32
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
Node<K,V>[] nextTab; int sc;
// 如果 tab 不空并且结点 f 不是ForwardingNode类型,说明这个位置不在扩容(其他位置可能在扩容)。
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
int rs = resizeStamp(tab.length);
//若sizeCtl是负数,tab、nextTable和当前table、nextTable相同,说明扩容尚未完成
//因为一旦扩容完成,就会将cmap的属性更新
while (nextTab == nextTable && table == tab && (sc = sizeCtl) < 0) {
// 1.如果sizeCtl>>>16 != rs,则表示标识符变化了
// 2.或者sizeCtl == rs + 1(扩容结束了,不再有线程进行扩容)(默认第一个线程设置sc == (rs左移16位+2),
// 当第一个线程结束扩容,会将 sc 减一。此时sc=rs+1)
// 3.或者sizeCtl == rs + 65535 (如果达到最大帮助线程的数量,即 65535)
// 4.或者transferIndex正在调整 (扩容结束)
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || transferIndex <= 0)
break;
/* sc的含义
-1:代表 table 正在初始化,其他线程应该交出 CPU 时间片,退出
-N:表示正有 N-1 个线程执行扩容操作
>0:表示 tab 已经初始化,代表 tab 容量,默认为 tab 大小的 0.75,如果还未初始化,代表需要初始化的大小。*/
// 用CAS尝试将SIZECTL加1,表示表示增加了一个线程协助扩容
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab);
break;
}
}
return nextTab;
}
return table;
}
总结一下 helpTransfer 方法:
- 先判断当前位置的结点类型是否是 ForwardingNode,和当前结点的 nextTable 是否为空:
- 如果 f 是 ForwardingNode 类型并且 f 的 nextTable 属性为空,则表示 map 不在扩容,返回一个 table 即可(可能是旧数组,也可能是新数组)
- 否则,进一步判断
this.nextTable
是否和f.nextTab
相等,以及sc=sizeCtl
是否小于 0:- 如果
this.nextTable==f.nextTab
,并且sc < 0
,则表示旧数组正在扩容,为了保险,再次检查 4 种代表扩容是否结束的情况是否符合:- 如果扩容结束,break 掉 while 返回即可
- 否则,再次判段
sc
是否与内存中的SIZECTL
相同:- 如果相同,则将用 cas 尝试将 SIZECTL 加 1,表示表示增加了一个线程协助扩容,然后进入 transfer 方法。
- 否则,回到 while 中再次检查
this.nextTable==f.nextTab
是否成立,自旋。
- 如果
存疑:
- Q1:在总结的时候,我有一个疑惑,为什么一定要用 cas 检查
SIZECTL
,才能进入 transfer 方法呢?- A1:
- Q2:为什么当
sc == rs + 1
就表示 cmap 扩容成功?- A2:这个判断可以在 addCount 方法中找到答案:默认第一个线程设置
sc == (rs >>> 16) + 2
,当第一个线程结束扩容了,就会将sc--
。此时sc = rs + 1
。
- A2:这个判断可以在 addCount 方法中找到答案:默认第一个线程设置
如果上面的逻辑都没问题,接下来就应该看看 cmap 是如何数据迁移的...
3.3 transfer 方法
数据迁移的逻辑比较复杂,因为它支持并发扩容,而且还没有加锁。详细看看 transfer 方法...
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
// stride(步长)在单核下直接等于n,多核模式下为 (n>>>3)/NCPU,最小值是16
// 一共会有n个位置是需要进行迁移
// n个位置分为多个区域,每个区域有 stride 个任务
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
// 如果 nextTab 为 null,先进行一次初始化
// 保证只有第一个发起迁移的线程调用此方法时nextTab为null
if (nextTab == null) {
try {// 容量翻倍
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 = nextTab;//ConcurrentHashMap 中的属性
transferIndex = n; //同上,用于控制迁移的位置,初始位置为n
}
int nextn = nextTab.length;
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
// advance 指的是做完一个位置的迁移工作,可以准备做下一个位置的
boolean advance = true; //控制是否要继续向前扫描
boolean finishing = false; //确保在提交NextTab之前进行扫描,to ensure sweep before committing nextTab
// i是当前扫描位置索引,bound是左边界,注意是从后往前
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
// advance为true表示需要继续向区域[bound, transferIndex]扫描
// 简单理解结局:i指向transferIndex,bound指向 transferIndex-stride
while (advance) {
int nextIndex, nextBound;
// 如果--i>=bound证明当前扫描区域还没有扫描完,所以没必要扩张区域。第一次碰到不会执行(i初始为0)
if (--i >= bound || finishing)
advance = false;
// 如果transferIndex<=0,说明旧数组的所有位置都有线程处理
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false; //退出循环
}//如果cas成功则表示当前线程获得了一个扫描区域
else if (U.compareAndSwapInt(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound; //nextBound 是这次迁移任务的边界
i = nextIndex - 1;
advance = false; //当前线程已确定扫描区域
}
}//当前线程t进到这里,表示t"没事可干"
if (i < 0 || i >= n || i + n >= nextn) {
int sc; // sizeCtl的别名
if (finishing) { // 如果table的迁移操作已经完成
nextTable = null;
table = nextTab; // 将新的nextTab赋值给table,完成迁移
// 重新计算 sizeCtl:n 是原数组长度,所以 sizeCtl 得出的值将是新数组长度的 0.75倍(4)
sizeCtl = (n << 1) - (n >>> 1);
return;
}
// 之前我们说过,sizeCtl 在迁移前会设置为 (rs << RESIZE_STAMP_SHIFT) + 2
// 然后,每有一个线程参与迁移就会将 sizeCtl 加 1,
// 这里使用CAS操作对sizeCtl进行减1,代表做完了属于自己的任务
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
// 任务结束,方法退出
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
// 到这里,说明 (sc - 2) == resizeStamp(n) << RESIZE_STAMP_SHIFT,
// 也就是说,所有的迁移任务都做完了,也就会进入到上面的 if(finishing){} 分支了
finishing = advance = true;
i = n; // recheck before commit
}
}
// 如果位置i为空,就用cas插入一个ForwardingNode结点
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
// 该位置处是一个ForwardingNode(MOVED的本质),代表该位置数据迁移完毕
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {// 获取锁并开始处理数组该位置处的迁移工作
synchronized (f) {
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
// 头结点的hash值>0,表示此处为链表
if (fh >= 0) {
// 将链表一分为二,
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;
}
}
if (runBit == 0) {
ln = lastRun;
hn = null;
}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);
}
// 低位链表放在新数组的位置i
setTabAt(nextTab, i, ln);
// 高位链表放在新数组的位置i+n
setTabAt(nextTab, i + n, hn);
// 将原数组该位置处设置为fwd,标记该位置已经处理完毕,
setTabAt(tab, i, fwd);
// advance 设置为 true,标记该位置已经迁移完毕
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 (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;
}
}
// 如果低位链表的节点数少于 8,则将此处红黑树拆成链表
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;
// 将 ln 放置在新数组的位置 i
setTabAt(nextTab, i, ln);
// 将 hn 放置在新数组的位置 i+n
setTabAt(nextTab, i + n, hn);
// 将原数组该位置处设置为 fwd,代表该位置已经处理完毕,
// 其他线程看到该位置的 hash 值为 MOVED,则不进行迁移
setTabAt(tab, i, fwd);
// advance 设置为 true,代表该位置已经迁移完毕
advance = true; //继续向前扫描
}
}
}
}
}
}
整理一下思路:视频 85:00
- ConcurrentHashMap 的扩容支持多线程,在多线程环境下,每一个线程会通过步长计算出自己的负责数据迁移的区域。
- 当线程在某个位置
i
迁移数据时,每一个线程都会锁住i
位置的头结点,这样保证了被锁的位置不能被其他线程操作。 - 当位置
i
的数据被一个线程迁移后,该位置的锁将会别释放掉,并将此处的头结点设置为ForwardingNode
,告诉其他线程不必在继续协助该位置,但没有ForwardingNode
的结点可以进行 put 操作,最后的数据还是会搬到新数组中, 与此同时,刚刚的线程继续从右往左扫描,尝试搬迁其他位置的数据:- 如果有尚未搬迁而且未被其他线程正在搬迁的位置,该线程将会继续变迁该位置的数据。
- 否则,该线程将会退出本次 transfer 的流程,继续等待其他线程完成数据迁移。
- 所有数据迁移完全后,扩容完成。
3.3 treeifyBin 方法
如果要进入 treeifyBin 方法后判断是否要进行树化
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
if (tab != null) {
// MIN_TREEIFY_CAPACITY = 64
// 如果数组长度小于 64 的时候,其实也就是 8 < x < 64 时会对数组扩容
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
// 后面我们再详细分析这个方法
tryPresize(n << 1);
// 如果 b 是非空头结点
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));
}
}
}
}
}
3.4 addCount 方法
addCount 方法不容易理解,简单说一下它的作用吧:
- 对 table 的总元素个数加一。无论是通过修改 baseCount,还是通过使用 CounterCell。当 CounterCell 被初始化了,就优先使用 CounterCell,不再使用 baseCount。
- 检查是否需要扩容,或者是否正在扩容。如果需要扩容,就调用扩容方法,如果正在扩容,就帮助其扩容。
对应视频 JDK8中ConcurrentHashMap源码解析(上) 的 65分钟
(4) get 方法
get 方法相对比较简单
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
//计算hash值
int h = spread(key.hashCode());
//根据hash值确定节点位置
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
//如果搜索到的节点key与传入的key相同且不为null,直接返回这个节点
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
//如果eh<0 说明这个节点在树上 直接寻找
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;
}
简单总结一下:
- 计算 key 的 hash 值,然后再算出桶下标
i
- 判断
i
位置是否有元素:- 如果没有元素,返回 null
- 否则,如果 hash 值与 key 都相同,那么返回当前位置元素的 val
- 如果 hash 值与 key 都不相同,那么在此检查位置
i
的结点类型:- 如果是红黑树就调用红黑树的查找方法
- 如果是链表就遍历链表找出对应元素。
(5) size 方法
详讲每个方法,包括 UNsafe:https://blog.csdn.net/u010723709/article/details/48007881
addCount:https://www.jianshu.com/p/749d1b8db066 https://www.cnblogs.com/dgutfly/p/11425599.html
开源中国:https://my.oschina.net/hosee/blog/675884#h2_10 https://www.javadoop.com/post/hashmap#toc_11
https://blog.csdn.net/Bill_Xiang_/article/details/81122044
有目录那个博客园:https://www.cnblogs.com/study-everyday/p/6430462.html#autoid-2-1-4
helptransfer、以及sizeCtl 参考:https://www.jianshu.com/p/39b747c99d32
代码很多注释:https://www.cnblogs.com/zerotomax/p/8687425.html#go2
讲了finnish的怎么得到:https://www.cnblogs.com/yangming1996/p/8031199.html