ConcurrentHashMap源码阅读--附面试题和答案
ConcurrentHashMap主体流程与HashMap有很多的相似处,可以先阅读HashMap源码再查看会比较容易理解
全局变量和类简介
//默认的初始化容量
private static final int DEFAULT_CAPACITY = 16;
//默认负载因子
private static final float LOAD_FACTOR = 0.75f;
//链表转红黑树的最小长度,前提是数组容量大于64 ,如果不大于64,则只是触发扩容
static final int TREEIFY_THRESHOLD = 8;
//链表转红黑树的数组必须的最小容量
static final int MIN_TREEIFY_CAPACITY = 64;
//红黑树转链表的最大节点数量
static final int UNTREEIFY_THRESHOLD = 6;
//扩容时候,每个线程最少迁移的桶数量
private static final int MIN_TRANSFER_STRIDE = 16;
//ForwardingNode对象节点的哈希值
static final int MOVED = -1; // hash for forwarding nodes
//树跟节点封装成TreeBin对象的固定哈希值
static final int TREEBIN = -2; // hash for roots of trees
static final int RESERVED = -3; // hash for transient reservations
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
//可用CPU数量
static final int NCPU = Runtime.getRuntime().availableProcessors();
//链表节点类
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
//红黑树根节点被封装成 TreeBin ,哈希值默认-2
static final class TreeBin<K,V> extends Node<K,V> {
TreeNode<K,V> root;
volatile TreeNode<K,V> first;
//红黑树节点
static final class TreeNode<K,V> extends Node<K,V>
//占位节点,哈希值默认-1 ,扩容时候重定向读到新数组,不用阻塞读
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;
}
//当前ConcurrentHashMap存储使用的数组
transient volatile Node<K,V>[] table;
//扩容时候临时使用的数组
private transient volatile Node<K,V>[] nextTable;
//记录存储数量size的值,不精确
private transient volatile long baseCount;
//记录扩容时候迁移任务分配的到桶的位置,从最高位开始
private transient volatile int transferIndex;
//记录更新baseCount值失败后,存储需要增加的计数
private transient volatile CounterCell[] counterCells;
//关键属性
//负数表示控制初始化和扩容的标记,
// -1 的时候正在初始化数组
// -(1+活跃的扩容线程数)表示在扩容,并记录线程数,线程扩容结束会减少记录
//当table=null 表示需要创建数组的容量大小,如果此时为0 则使用默认值容量16
//初始化后,则表示控制下一次扩容的大小阈值
private transient volatile int sizeCtl;
Hash算法
//获取hash值与上 0x7fffffff(31位都是1),保证hash值不小于0
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
Put方法实现
//在put过程中,如果哈希冲突,需要存放到树或者链表上,则对树的根节点或者链表头节点使用 synchronized加锁
//在链表转红黑树过程中,会对链表头节点使用 synchronized加锁
final V putVal(K key, V value, boolean onlyIfAbsent) {
//获取hash值,所有哈希算法算出来的哈希值都是非负数
int hash = spread(key.hashCode());
int binCount = 0;
//死循环
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//懒加载初始化数组
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//数组位置上没有元素,CSA直接存放
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
//hash值等于-1 ,表示改位置有线程正在扩容,则当前线程协助扩容,扩容结束后再走存放逻辑
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else { //该位置没有扩容
V oldVal = null;
synchronized (f) { //对第一个节点加锁
if (tabAt(tab, i) == f) {
if (fh >= 0) { //hash算法决定了hash值肯定大于等于 0 ,链表的hash值不小于0
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
//判断相同的key,则覆盖value
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
//没有相同的key,构建node放到链表尾部
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
//链表转换为红黑树的时候,节点被封装成TreeBin对象,它的hash值固定为 TREEBIN = -2
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;
}
}
}
}
//链表长度大于8,,数组长度大于64触发转红黑树
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
数组初始化方法
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(); // lost initialization race; just spin
//CAS判断 sizeCtl内存地址处的值与 sc的值一样 ,则重置为-1 ,进行初始化,再进来的线程判断则走自旋
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
//获取下一次扩容的阈值
sc = n - (n >>> 2); //n >>> 2就是除以4 ,计算式相当于n*0.75,位运算更高效
}
} finally {
//设置一次扩容的阈值
sizeCtl = sc;
}
break;
}
}
return tab;
}
Size计数的方法
BASECOUNT 存储数组存放的元素个数,但是不是准确值,因为CAS更新这个值的时候会失败,失败后会创建 CounterCell记录需要增加的值放到 CounterCell[] counterCells数组中。 size计算就是 BASECOUNT 加上 数组中的值之和
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
//CAS添加计数的BASECOUNT值
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
//CAS添加计数的BASECOUNT值失败,则CAS添加CounterCell中记录的值
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
//死循环直到则CAS添加CounterCell中记录的值成功
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
s = sumCount();
}
//校验是否需要扩容
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
if (sc < 0) {
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);
}
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}
协助扩容方法
当put的时候,发现正在扩容,该线程会加入扩容大军进行协助扩容
//协助扩容方法
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
Node<K,V>[] nextTab; int sc;
//再次判断确认数组不为空,且槽位上节点被 ForwardingNode 占据表名正在扩容
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
int rs = resizeStamp(tab.length);
//判断扩容没有结束,死循环申请扩容任务
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || transferIndex <= 0)
break;
//扩容线程数加 1 ,进行扩容
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab);
break;
}
}
return nextTab;
}
return table;
}
核心: 扩容方法
//扩容方法
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) { // initiating
try {
@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 = nextTab;
transferIndex = n; //迁移从原数组最高位置开始
}
int nextn = nextTab.length;
//创建重定向节点:1.占位标桶位置记已经迁移完成
//2.转发作用,当get的时候遇到这个节点,则转发到新数组去查询,而不用阻塞
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
boolean advance = true; //继续推进桶迁移的标记
boolean finishing = false; // 迁移完成的标记
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) {
//transferIndex <= 0 说明数组的hash桶已被线程分配完毕,没有了待分配的hash桶,将 i 设置为 -1 ,
// 后面的代码根据这个数值退出当前线的扩容操作
i = -1;
advance = false;
}
//只有首次进入for循环才会进入这个判断里面去,设置 bound 和 i 的值,也就是领取到的迁移任务的数组区间
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;
//扩容结束,将 nextTable 设置为 null,表示扩容已结束,将 table 指向新数组,sizeCtl 设置为扩容阈值
if (finishing) {
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1);
return;
}
//每条线程扩容结束都会将扩容线程数量减一
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
//判断是否为最后一条扩容线程
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
//除了修改结束标识之外,还得设置 i = n; 以便重新检查一遍数组,防止有遗漏未成功迁移的桶
finishing = advance = true;
i = n; // recheck before commit
}
}
//判断该桶位置为空,则使用占位节点进行占位
else if ((f = tabAt(tab, i)) == null)
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;
//迁移链表
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;
}
}
//判断将lastRun节点设置为高位链或者低位链头节点
if (runBit == 0) {
ln = lastRun;
hn = null;
}
else {
hn = lastRun;
ln = null;
}
//循环链表只读lastRun节点,使用头插法复制拼接高位链表和低位链表
//链表中lastRun节点以后的节点与lastRun节点是同一个链上的,不需要继续遍历了,这就是lastrun机制
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);
}
//将高低位链表分别存到新数组对应的桶中,将旧数组的桶元素用ForwardNdoe占位
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
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;
}
}
//判断是否需要红黑树转成链表,不需要则封装成TreeBin
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;
}
}
}
}
}
}
面试题及答案
1. 说说Put 流程
数组初始化流程如下:
2. 谈谈扩容流程
3. 扩容中,读能够访问到数据,是怎么实现的?
- 当读取数据的桶没有进行迁移,按照原来的方式正常访问。
- 当读取到的桶正在迁移,因为迁移桶中的数据是直接赋值出来形成 ln hn 链的,不会影响原来的数据,可以直接访问而不会阻塞 get 操作。
- 当读取到的桶已经迁移完成,此时是 ForwardingNode 节点,里面持有了新数组引用,此次会重定向 get 操作到新数组中进行查找操作
4.库容过程中,写操作如何处理?
- 当写的桶位置还没有进行迁移,正常写入
- 当写的桶位置拿到元素哈希值为-1,表明正在进行扩容,当前线程加入扩容中协助扩容,扩容结束后再进行写入
- 当写的桶位置正在进行迁移,此时桶第一个元素节点被上锁,会阻塞写操作
5.指定桶位置形成了红黑树,并且红黑树正在进行自平衡,此时读操作怎么处理?
读线程会先判断 lockState 锁状态位是否被其它线程持有写锁或者等待写锁,
如果其它线程持有写锁,则以链表的形式遍历红黑树来进行读取操作;
如果其它线程持有了读锁,则 lockState 状态位加 4, 读锁可重入,使用红黑树的方式进行遍历查询数据。
6.在 1.8 版本中,统计存储元素个数是怎么实现的,为什么不适用AtomicLong 来进行统计?
在 put 操作结束后,会调用 addCount,更新计数,使用 CAS 更新 baseCount 统计个数,当 CAS 操作失败后;使用 CAS 更新 CounterCell[]数组记录要增加的数量,如果继续失败,则进行死循环操作进行添加,直到成功。所以 baseCount 加上数组中存储值的总和就是当前容器在统计时候的个数。
不使用 AtomicLong 进行计数是因为,在高并发的场景下,原子操作可能会导致失败,这个时候就没法继续处理了。
7.简单说一下 LastRun 机制
8.如果 lastRun 节点正好在一条全部都为高位或者全部都为低位的链表上,会不会形成死循环?
扩容的时候是使用赋值原来的节点数据进行扩容的,而不是操作原有的节点,不会存
在形成死循环的条件。因为是复制的迁移方式,所以在迁移过程中,会导致 Map 占用内存空间增翻一倍。