概述
文章目录
- 哈希值是负数的几种特殊情况
- sizeCtl
- put
- putVal
- spread(int h) 求哈希值
- initTable() 初始化数组
- treeifyBin() 扩容或链表转红黑树
- tryPresize(int size)
- resizeStamp(int n) 计算扩容标识戳
- transfer(tab, null) 第一个进来执行扩容的线程
- lastRun 机制图示
- helpTransfer 协助扩容
- 源码中的 BUG 及修复版本
- BUG 一:sizeCtl 临时变量 sc 判断条件
- sc == rs + 1
- sc == rs + MAX_RESIZERS
- bug fix in jdk12
- BUG 二:tryPresize(int size) 中 sc < 0 的判断
- bug fix in jdk9
- BUG 三:computeIfAbsent 导致死循环
- computeIfAbsent In JDK 8
- bug fix in jdk9
#八股文 #JUC
哈希值是负数的几种特殊情况
// 数组中的数据的哈希值是负数的特殊含义
static final int MOVED = -1; // 数组正在扩容,并且当前位置的数据已经迁移到了新数组
static final int TREEBIN = -2; // 当前索引位置上是红黑树
static final int RESERVED = -3; // 当前索引位置已经被占了,但是值还没设置
sizeCtl
// sizeCtl > 0:若当前数组没有初始化,代表初始化的长度;若数组已初始化,代表下次扩容的阈值
// sizeCtl = 0:数组数还没初始化
// sizeCtl = -1:数组正在初始化
// sizeCtl < -1:数组正在扩容
// 数组初始化和扩容的标识信息。
private transient volatile int sizeCtl;
put
// 若 key 已存在,则使用 value 覆盖 oldValue,并返回 oldValue
public V put(K key, V value) {
return putVal(key, value, false);
}
// 若 key 已存在,什么都不做,并返回 oldValue
// absent:缺席、不到场
public V putIfAbsent(K key, V value) {
return putVal(key, value, true);
}
putVal
put 时没有哈希冲突用 CAS,有哈希冲突用 synchronized。
final V putVal(K key, V value, boolean onlyIfAbsent) {
// key 和 value 不能为 null(HashMap key 和 value 可以为 null)
if (key == null || value == null) throw new NullPointerException();
// 根据 key 计算出 hashcode
int hash = spread(key.hashCode());
int binCount = 0;
// 死循环
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 初始化数组(HashMap 也是第一次 put 时初始化数组)
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// 下标为 i 的桶中的数据为 null 时,则以 CAS 的方式将 Node 放入桶中(不加锁)
// CAS 操作失败就再走一次循环
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))
break;
}
// static final int MOVED = -1;
// hashcode = -1,当前位置的数据已经迁移到了新数组
else if ((fh = f.hash) == MOVED)
// 看看数组中是否还有没迁移的数据,帮助扩容
tab = helpTransfer(tab, f);
else {
// 如果以上情况都不满足,说明出现哈希冲突则利用 synchronized 写入数据
V oldVal = null;
// 锁住当前桶
synchronized (f) {
// 确认数据没有变化
if (tabAt(tab, i) == f) {
// 链表添加结点
if (fh >= 0) {
// 记录链表长度
binCount = 1;
// 死循环
for (Node<K,V> e = f;; ++binCount) {
K ek;
// 从桶中结点开始遍历链表校验 key 是否相同
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
// onlyIfAbsent 为 false 才覆盖老数据
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;
}
}
}
// 红黑树添加结点
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) {
// static final int TREEIFY_THRESHOLD = 8;
// 如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树
if (binCount >= TREEIFY_THRESHOLD)
// 判断是扩容还是转红黑树
treeifyBin(tab, i);
if (oldVal != null)
// 若 oldValue 有值,返回 oldValue。
// 若 oldValue 没有值,返回 null
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
spread(int h) 求哈希值
// HASH_BITS 的二进制表示: 0111 1111 1111 1111 1111 1111 1111 1111
// 除了首位为 0,其他位都为 1
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
static final int spread(int h) {
// h ^ (h >>> 16) 让 hashCode 的高 16 位参与到索引位置的计算中,从而减少哈希冲突
// 将异或操作后的哈希值对 HASH_BITS 进行 & 运算是为了保证返回的哈希值一定是正数
// 因为 CHM 中数组中的数据的哈希值如果是负数,有特殊含义
return (h ^ (h >>> 16)) & HASH_BITS;
}
initTable() 初始化数组
// 数组默认初始化长度
private static final int DEFAULT_CAPACITY = 16;
// LOAD_FACTOR 是个 static final 修饰的常量
private static final float LOAD_FACTOR = 0.75f;
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 的时间片
// 数组还未初始化,以 CAS 的方式将 sc 修改为 -1,代表数组正在初始化
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
// DCL(Double Check Lock)
if ((tab = table) == null || tab.length == 0) {
// 默认初始化长度 16
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
// Node 数组初始化完成
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
// 依次给局部变量和成员变量赋值
table = tab = nt;
// 计算下次扩容的阈值。LOAD_FACTOR = 0.75 是个常量。
// n * (1 - 1/4) = 0.75 * n;
sc = n - (n >>> 2);
}
} finally {
// 局部变量 sc 赋值给成员变量 sizeCtl
sizeCtl = sc;
}
break;
}
} return tab;
}
treeifyBin() 扩容或链表转红黑树
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
if (tab != null) {
// static final int MIN_TREEIFY_CAPACITY = 64;
// 若数组长度小于 64,直接扩容
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
// 扩容前的一些准备的校验
tryPresize(n << 1);
// 链表转红黑树
// 将单向链表转化为 TreeNode 对象(双向链表),再通过 TreeBin 方法转为红黑树
// TreeBin 中保留着双向链表和红黑树
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));
}
}
}
}
}
}
tryPresize(int size)
扩容标识戳
private static final int MAXIMUM_CAPACITY = 1 << 30;
private static int RESIZE_STAMP_BITS = 16;
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
// 00000000 00000000 11111111 11111111
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
// treeifyBin 中调用该方法时,size 是数组长度的 2 倍
// putAll 中调用该方法时,size 是插入 map 的 size
private final void tryPresize(int size) {
// 初始化数组的长度
int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
tableSizeFor(size + (size >>> 1) + 1);
int sc;
while ((sc = sizeCtl) >= 0) {
// n : 数组长度
Node<K,V>[] tab = table; int n;
// 初始化数组
if (tab == null || (n = tab.length) == 0) {
n = (sc > c) ? sc : c;
// 以 CAS 的方式将 sc 修改为 -1
if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
// DCL
if (table == tab) {
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
}
}
// c 没有超过阈值或者 数组长度大于等于 MAXIMUM_CAPACITY
else if (c <= sc || n >= MAXIMUM_CAPACITY)
break;
// 确认 table 没有被其他线程修改过
else if (tab == table) {
// 计算扩容标识戳
int rs = resizeStamp(n);
// bug之1:sc 是局部变量(sc = sizeCtl),sc 大于等于 0 才能进入 while 循环
// 由于之前已经判断过初始化了,所以这里必是扩容
if (sc < 0) {
Node<K,V>[] nt;
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || // 判断协助扩容的标识戳是否一致(sc 的高 16 位扩容标识戳 rs)
sc == rs + 1 || // bug之2 这里应该是 sc == rs << RESIZE_STAMP_SHIFT + 1 判断扩容操作是否已经达到了最后的检查阶段(sc 的高 16 和 rs 相同,如果 sc 和 rs << 16 + 1 相等,则说明 sc 的低 16 位等于 1。)
// MAX_RESIZERS 是 1 左移 16 位减 1 即 低 16 位都是 1
sc == rs + MAX_RESIZERS || // bug之三,这里应该是 sc == rs << RESIZE_STAMP_SHIFT + MAX_RESIZERS 判断扩容线程是否已经达到最大值
(nt = nextTable) == null || // 新数组为 null,说明扩容完毕,因为扩容完毕以后会将 nextTable 置为 null
transferIndex <= 0) // transferIndex 为线程领取任务的最大结点,如果为0,代表所有老数据的迁移任务都被领完了
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
// 第一个进来执行扩容的线程
// 基于 CAS 的方式将 sizeCtl 从修改为 rs 左移 16 位 + 2
// sizeCtl : 10000000 00011010 00000000 00000010 代表三重含义
// 1. 10000000 00011010 00000000 00000010 < -1,代表正在扩容
// 2. 低 16 位代表正在扩容的线程数+1,此时低 16 位为 2,表示有 1 个线程正在扩容
// 3. 高 16 位代表标识戳 rs
// 这么做的原因是最后一个迁移完数据的线程需要检查一遍有没有数据漏掉
// 因为扩容分为两步,创建新数组并迁移数据
// 当最后一个线程迁移完毕数据后,对低位 -1,最终结果低位还是 1,此时这个线程需要对整个老数组再次检查,数据是否迁移干净 recheck before commit
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
// 开始扩容,传入老数组
transfer(tab, null);
}
}
}
resizeStamp(int n) 计算扩容标识戳
基于数组长度得到一个标识,在其他线程帮助扩容时,需要校验该标识,标识一致才可以帮助扩容。
private static int RESIZE_STAMP_BITS = 16;
// Integer.numberOfLeadingZeros(n) 返回 n 用二进制表示时,前置 0 的个数
// 1 << (RESIZE_STAMP_BITS - 1) 表示 1 左移 15 位,00000000 00000000 10000000 00000000。
// Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1)) 中的 '|' 操作是为了保证当前扩容标识戳左移 16 位后一定是负数(最高位为 1)。
// 为什么要保证当前扩容标识戳左移 16 位后一定是负数?
// 跟 跟sizeCtl 有关,sizeCtl 是负数代表正在扩容。
// 假设数组长度 n=32,二进制为:0b 00000000 00000000 00000000 00100000
// Integer.numberOfLeadingZeros(32) = 26
// 26 的二进制为:0b 00000000 00000000 00000000 00011010
// 00000000 00000000 00000000 00011010 | 00000000 00000000 10000000 00000000
// 00000000 00000000 10000000 00011010
static final int resizeStamp(int n) {
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
transfer(tab, null) 第一个进来执行扩容的线程
- 计算步长 stride
- 初始化新数组
- 线程领取迁移数据任务
- 判断迁移是否结束
- 查看当前位置数据是否为 null
- 查看当前位置数据是否为 fwd
- 链表迁移数据 lastRun 机制
- 红黑树迁移 迁移完数据长度若小于等于 6,则转回链表
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
// stride 迁移数据的步长
int n = tab.length, stride;
// static final int NCPU = Runtime.getRuntime().availableProcessors();
// private static final int MIN_TRANSFER_STRIDE = 16;
// stride = (NCPU > 1) ? (n >>> 3) / NCPU : n
// if(stride < MIN_TRANSFER_STRIDE) stride = MIN_TRANSFER_STRIDE;
// 若 cpu 核数 > 1,就计算每个 cpu 需要迁移的数组的下标的长度,计算公式为 n / 8 / NCPU
// 若计算得出的 stride < 16,就用 16,表示每个线程每次最少迁移16长度的数据
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE;
// 初始化新数组
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
// OOM or 数组长度溢出
sizeCtl = Integer.MAX_VALUE;
return;
}
// 将 nextTab 赋值给成员变量 nextTable
nextTable = nextTab;
// transferIndex 设置为老数组的长度
transferIndex = n;
}
//
int nextn = nextTab.length;
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
boolean advance = true;
// 扩容是否结束
boolean finishing = false; // to ensure sweep before committing nextTab
// 死循环扩容
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing)
advance = false;
// 说明没有任务可以领取了
// 此处将 transferIndex 赋值给 nextIndex
else if ((nextIndex = transferIndex) <= 0) {
// 把 i 赋值为 -1,以便进入 if (i < 0 || i >= n || i + n >= nextn)
i = -1;
advance = false;
}
// 开始领取任务
// 基于 CAS 的方式将 nextIndex(transferIndex) 的值更新为 nextBound
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
// 老数组长度 -1,nextIndex 就是 transferIndex 更新前的值
i = nextIndex - 1;
// 领取任务成功退出循环,迁移 i~bound 之间的任务
// 举个例子 老数组长度 32,步长 stride 为 16,第一个线程领取到的任务就是32~16,换成数组下标就是31~16,其他线程可能领取到的就是 15~0 的数据的迁移任务。
advance = false;
}
}
// 迁移完成或没有任务可以领取
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
if (finishing) {
// 结束扩容,将 nextTable 设置为 null
nextTable = null;
// 将迁移完数据的新数组指向老数组
table = nextTab;
// 将 sc 赋值为下次扩容的阈值 2 * n - n/2 = 1.5 * n
sizeCtl = (n << 1) - (n >>> 1);
return;
}
// 如果一个线程完成了迁移,尝试再去领取任务,若 transferIndex <= 0,所有任务已经被其他线程领走了,则把 i 设置为 -1。再执行退出扩容的操作(sc - 1),退出扩容成功后,判断当前线程是否最后一个退出扩容的,若是,则做检查操作。
// 基于 CAS 的方式,将 sc 低位 -1,代表当前线程退出扩容
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
// 判断是否是最后一个结束迁移数据的线程
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
finishing = advance = true;
// 检查老数组是否迁移完成
i = n; // recheck before commit
}
}else if ((f = tabAt(tab, i)) == null)
// 如果迁移位置的数据为 null,则放置一个 fwd,表示数据已经迁移(最后一个线程检查时走的逻辑)
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;
// lastRun 机制
// 提前循环一次链表,将结点赋值到对应的高低位 Node
// 便于迁移最后几个都是迁移到高位或者低位的结点
// 直接将这几个的头结点迁移过去就好了
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 位置,不用继续往下循环了
// 这样就不用每个结点都重新创建,节约资源,提升效率
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);
// 放高位
setTabAt(nextTab, i + n, hn);
// 将当前桶位置设置为 fwd,代表数据已迁移
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;
}
}
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 机制图示
迁移前:
![[Pasted image 20220901204510.png]]
迁移后:
![[Pasted image 20220901204544.png]]
helpTransfer 协助扩容
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
Node<K,V>[] nextTab; int sc;
// 老数组不为 null,当前结点是 fwd, 新数组也不为 null
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
// 创建扩容标识戳
int rs = resizeStamp(tab.length);
// DCL
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || transferIndex <= 0)
// 上述条件有一个满足就不需要协助扩容了 break
// sc == rs + 1 || sc == rs + MAX_RESIZERS 这里 bug 同上
break;
// CAS sc+1 协助扩容
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab);
break;
}
}
return nextTab;
}
return table;
}
源码中的 BUG 及修复版本
不要以为源码中都是对的,然后强行解释。 - 我说的
BUG 一:sizeCtl 临时变量 sc 判断条件
此 BUG 影响不大,在 JDK 12 中修复。
int rs = resizeStamp(n);
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || transferIndex <= 0)
上述这个判断条件中源码中出现多次,有问题的下面两个条件:
sc == rs + 1 || sc == rs + MAX_RESIZERS
sc 的低 16 位代表正在扩容的线程数+1,此时低 16 位为 2,表示有 1 个线程正在扩容。高 16 位代表标识戳 rs。具体推断逻辑看上文源码分析。
sc == rs + 1
sc == rs + 1
是为了判断扩容操作是否已经达到了最后的检查阶段。
正确的写法应该是 sc == rs << RESIZE_STAMP_SHIFT + 1
。因为 sc 的高 16 代表 rs,如果 sc 和 rs << RESIZE_STAMP_SHIFT + 1 相等,则说明 sc 的低 16 位(正在扩容的线程数)等于 1,即到了最后的检查阶段。
sc == rs + MAX_RESIZERS
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
MAX_RESIZERS 是 1 左移 16 位减 1 即 低 16 位都是 1。
sc == rs + MAX_RESIZERS
是为了判断扩容线程是否已经达到最大值。
正确的写法应该是应该是 sc == rs << RESIZE_STAMP_SHIFT + MAX_RESIZERS
。
bug fix in jdk12
int rs = resizeStamp(n) << RESIZE_STAMP_SHIFT;
if (sc == rs + MAX_RESIZERS || sc == rs + 1 ||
(nt = nextTable) == null || transferIndex <= 0)
break;
JDK12 中将 resizeStamp(n) 的返回值左移了 16 位以后再赋值给 rs。同时 (sc >>> RESIZE_STAMP_SHIFT) != rs 也改成了 sc == rs + MAX_RESIZERS。
BUG 二:tryPresize(int size) 中 sc < 0 的判断
// 将 sizeCtl 赋值给局部变量 sc
while ((sc = sizeCtl) >= 0) {
Node<K,V>[] tab = table; int n;
if (tab == null || (n = tab.length) == 0) {
n = (sc > c) ? sc : c;
if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if (table == tab) {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
}
}
else if (c <= sc || n >= MAXIMUM_CAPACITY)
break;
else if (tab == table) {
int rs = resizeStamp(n);
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);
}
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
}
}
上述 sc < 0 的判断条件永远不可能进入,因为 sc 是 while 循环中的局部变量。sc >= 0,才会进入 while 循环。如果执行到 sc < 0,则说明之前没有对 sc 做赋值操作。所以此时 sc 肯定是大于等于 0 的。
bug fix in jdk9
private final void tryPresize(int size) {
int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
tableSizeFor(size + (size >>> 1) + 1);
int sc;
while ((sc = sizeCtl) >= 0) {
Node<K,V>[] tab = table; int n;
if (tab == null || (n = tab.length) == 0) {
n = (sc > c) ? sc : c;
if (U.compareAndSetInt(this, SIZECTL, sc, -1)) {
try {
if (table == tab) {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
}
}
else if (c <= sc || n >= MAXIMUM_CAPACITY)
break;
else if (tab == table) {
int rs = resizeStamp(n);
// 主要看这里,已经不校验 sc < 0 了
if (U.compareAndSetInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
}
}
}
BUG 三:computeIfAbsent 导致死循环
https://bugs.openjdk.org/browse/JDK-8062841 内含 Doug Lea 的回复
此 BUG 在 JDK 9 中被 Doug Lea 老爷子修复。
// https://bugs.openjdk.org/browse/JDK-8062841 中的 demo
public class Test {
public static void main(String[] args) {
Map<String, Integer> map = new ConcurrentHashMap<>(16);
map.computeIfAbsent(
"AaAa",
key -> map.computeIfAbsent(
"BBBB",
key2 -> 42)
);
}
}
该方法的意思是:当前 Map 中 key 对应的 value 不存在时,会调用 mappingFunction 函数,并且将该函数的执行结果(不为 null)作为该 key 的 value 返回。
computeIfAbsent In JDK 8
// 上文中分析过的代码此处就不再赘述了
public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
if (key == null || mappingFunction == null)
throw new NullPointerException();
int h = spread(key.hashCode());
V val = null;
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) & h)) == null) {
Node<K,V> r = new ReservationNode<K,V>();
synchronized (r) {
// 使用 ReservationNode 占位
if (casTabAt(tab, i, null, r)) {
binCount = 1;
Node<K,V> node = null;
try {
// 使用传入的函数 mappingFunction 计算 value
if ((val = mappingFunction.apply(key)) != null)
node = new Node<K,V>(h, key, val, null);
} finally {
// 将占位的 r 替换成上面的 node
setTabAt(tab, i, node);
}
}
}
if (binCount != 0)
break;
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
boolean added = false;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek; V ev;
if (e.hash == h &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
val = e.val;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
if ((val = mappingFunction.apply(key)) != null) {
added = true;
pred.next = new Node<K,V>(h, key, val, null);
}
break;
}
}
}
else if (f instanceof TreeBin) {
binCount = 2;
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> r, p;
if ((r = t.root) != null &&
(p = r.findTreeNode(h, key, null)) != null)
val = p.val;
else if ((val = mappingFunction.apply(key)) != null) {
added = true;
t.putTreeVal(h, key, val);
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (!added)
return val;
break;
}
}
}
if (val != null)
addCount(1L, binCount);
return val;
}
bug fix in jdk9
public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
if (key == null || mappingFunction == null)
throw new NullPointerException();
int h = spread(key.hashCode());
V val = null;
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh; K fk; V fv;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & h)) == null) {
Node<K,V> r = new ReservationNode<K,V>();
synchronized (r) {
if (casTabAt(tab, i, null, r)) {
binCount = 1;
Node<K,V> node = null;
try {
if ((val = mappingFunction.apply(key)) != null)
node = new Node<K,V>(h, key, val);
} finally {
setTabAt(tab, i, node);
}
}
}
if (binCount != 0)
break;
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else if (fh == h && // check first node
((fk = f.key) == key || fk != null && key.equals(fk)) &&
(fv = f.val) != null)
return fv;
else {
boolean added = false;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == h &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
val = e.val;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
if ((val = mappingFunction.apply(key)) != null) {
if (pred.next != null)
throw new IllegalStateException("Recursive update");
added = true;
pred.next = new Node<K,V>(h, key, val);
}
break;
}
}
}
else if (f instanceof TreeBin) {
binCount = 2;
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> r, p;
if ((r = t.root) != null &&
(p = r.findTreeNode(h, key, null)) != null)
val = p.val;
else if ((val = mappingFunction.apply(key)) != null) {
added = true;
t.putTreeVal(h, key, val);
}
}
// bug fix
else if (f instanceof ReservationNode)
throw new IllegalStateException("Recursive update");
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (!added)
return val;
break;
}
}
}
if (val != null)
addCount(1L, binCount);
return val;
}
最后
以上就是坚强仙人掌为你收集整理的ConcurrentHashMap 中竟有好几个BUG?JDK8 源码深度解析的全部内容,希望文章能够帮你解决ConcurrentHashMap 中竟有好几个BUG?JDK8 源码深度解析所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复