-
六、replaceNode更新节点
-
七、总结
对ConcurrentHashMap的基本概念有一个初步印象后,接下来才是真正的探索。
扩容是重头戏,看过的人都说难。确实,和java7版本比起来,难度真不是一个量级的。有些细节看着莫名其妙,一想就是好几天,看似想明白也只能算是猜想合理,直呼Doug Lea的心思是真的细啊!
深究细节是费时且痛苦的,欣喜的是,怎么也想不明白的逻辑,发现这次是源码错了!官方JDK!现在市面上普遍都在用java8,怎么可能存在bug呢?
首先思考几个问题:
-
ConcurrentHashMap
是如何实现扩容机制的? -
多线程辅助扩容?如何分配扩容迁移任务?
-
相对于java7有哪些异同和优化点?
-
扩容的过程中有get操作怎么办?
老规矩,从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();
// 1. 哈希值高低位扰动
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)
// 2. tab 为空 初始化
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 3. tab不为null,则通过(n - 1) & hash 计算 tab对应索引下标,找到node
// node为null说明没有发生hash冲突,cas 设置新节点node到tab的对应位置,成功则结束循环
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
// 4. 发现哈希值为MOVED时,
// 说明数组正在扩容,帮助扩容,这个节点只可能是ForwardingNode
tab = helpTransfer(tab, f);
else {
// 5.正常情况下发生哈希冲突
V oldVal = null;
synchronized (f) {
// 再次检查i位置的节点是否还是f
// 如果有变动则重新循环
if (tabAt(tab, i) == f) {
if (fh >= 0) {
// 6. fh>=0 是链表
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
// 链表中已经有hash相等且(key地址相等 or key值相等)
// 则判断是否需要替换
// put onlyIfAbsent=false,新值替换旧值
// putIfAbsent onlyIfAbsent=true,新值不替换旧值
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
// 解决hash冲突的方式
// 链表法,新节点放在了链表尾部(尾插法),这里和jdk1.7不一样
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) {
// 7.红黑树
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
// putTreeVal的返回值是已经存在的节点
// p != null 说明 key已经存在,看是否需要替换value
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
// 8. binCount,链表的长度>=8时 可能变为红黑树,也可能是扩容
// 数组长度小于64时,是扩容数组
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
// 若旧值不为null,则说明是替换,不需要后面的addCount
return oldVal;
break;
}
}
}
// 9. 元素数量+1
addCount(1L, binCount);
return null;
}
(1)首先计算key的哈希值,并做高低位扰动spread。
(2)数组table若为空,则初始化initTable()
。初始化的工作量很小,就是实例化一个数组,但是如何在多线程环境安全初始化呢?这里就涉及到sizeCtl
的值变化:
-
初始化前,
sizeCtl
存的是初始容量。 -
初始化时,
sizeCtl
相当于一把自旋锁,有且只有一个线程能将其cas
修改为-1,代表获取锁;其他线程则一直执行while循环,自旋让出cpu时间,直到数组不为null,即当初始化结束时,退出整个函数。 -
初始化完成,
sizeCtl
又被赋值为扩容阈值,当前容量的3/4,也代表释放锁。
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 spi
// SIZECTL 设置为 -1,相当于轻量级自旋锁
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
// 如果某个线程成功地把sizeCtl 设置为-1,它就拥有了初始化的权利
// 等到初始化完成,再把sizeCtl设置成当前容量的3/4,即为扩容阈值
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;
// 3/4
sc = n - (n >>> 2);
}
} finally {
// 初始化完成,sizeCtl设置成当前容量的3/4,即为扩容阈值
// 这里也相当于释放锁。
sizeCtl = sc;
}
break;
}
}
return tab;
}
(3)数组不为空了,则哈希映射数组下标(f = tabAt(tab, i = (n - 1) & hash)
),从主内存中获取最新的节点,若为空则说明没有发生哈希冲突,cas设置新节点到对应位置,设置失败可能因为有其他线程竞争设置了,则重新循环判断。(long)i << ASHIFT) + ABASE
代表i位置节点在主内存中的偏移量。
// 从主内存获取该位置最新节点
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
// cas 设置新节点到对应位置
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
Node<K,V> c, Node<K,V> v) {
return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
(4)(3)找到对应位置的节点不为空,说明发生了哈希冲突,此时判断节点的哈希值是否等于MOVED
,是则说明数组正在扩容,当前线程帮助扩容helpTransfer
(后面细说)。
(5)若节点哈希不等于MOVED
,说明是正常情况的哈希冲突,后面就看是加到链表里还是加到树上。此时synchronized
该节点,再次判断当前位置节点是否变化,如果变了说明有其他线程修改了,重新循环。
(6)节点的哈希值fh >= 0
,判断其是普通节点,即链表,遍历链表,有key相同的节点则判断是替换还是直接结束,若是新增节点,则以尾插法加到链表尾部,binCount
在遍历链表的过程中自增,记录链表的长度,后面看是否需要转为红黑树。(java7新增节点用的是头插法,java7HashMap
用的也是头插法,并发情况下容易造成循环链表死循环,后来java8就都用尾插法了)。
(7)fh < 0 && f instanceof TreeBin
判断是红黑树,putTreeVal
返回值为null是新增节点,不为null则返回值是树中已存在的节点,判断是否需要替换。
(8)树化判断,binCount
开始赋值为0,若新节点加到了链表中,binCount
会在遍历链表的过程中累加记录链表的长度,若新节点加到了红黑树中,binCount
赋值为2;binCount>= TREEIFY_THRESHOLD
,说明链表的节点达到树化的阈值8个,则执行treeifyBin
。
达到树化阈值TREEIFY_THRESHOLD
不一定就链表转为红黑树,若数组的长度小于MIN_TREEIFY_CAPACITY=64
,需要先扩容tryPresize(n << 1)
(tryPresize
涉及扩容后面细说)。若数组的长度>=MIN_TREEIFY_CAPACITY=64
,则锁住当前位置占位节点,开始树化。
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
if (tab != null) {
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
// tab 容量小于64,为什么要 << 1
tryPresize(n << 1);
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
// 链表转为红黑树。
// 锁住的是一个节点
synchronized (b) {
// 再次判断tab index位置的节点是否有改变
if (tabAt(tab, index) == b) {
// hd 头、tl尾
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;
}
// 红黑树化TreeBin
setTabAt(tab, index, new TreeBin<K,V>(hd));
}
}
}
}
}
(9)不是替换元素,需要最后执行addCount(1L, binCount)
,元素个数+1。
addCount
较为复杂单独拎出来讨论。一个简单的元素个数加减,如果让你来实现这个功能该如何做,一个volatile
修饰的变量,然后cas加减?在竞争激烈的情况,cas自旋可能会成为性能瓶颈,某些线程会因为cas计数失败而长时间自旋。
Doug Lea是怎么做的呢?基础变量(baseCount
)+数组(CounterCell[]
)辅助计数。
作者的思路就是尽量避免竞争,cas修改baseCount
成功就不会再去修改CounterCell[]
,修改失败也不会自旋,以哈希映射的方式找到CounterCell[]
对应位置的格子cas计数,依然失败就多次重复哈希映射找其他空闲格子,还失败就扩容CounterCell[]
,扩容之后都竞争不过其他线程,此时就进入自旋重复哈希映射,直到修改成功,虽然最终可能也会陷入不断自旋重试的情况,但是多个线程抢多个资源和多个线程抢一个资源相比,性能明显会好很多。详情看代码:
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
// counterCells!=null,则直接在counterCells里加元素个数,
// counterCells=null,则尝试修改baseCount,失败则修改counterCells。
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
// 初始化乐观的认为true即没有竞争
boolean uncontended = true;
// ThreadLocalRandom.getProbe() 相当于当前线程的hash值
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
// 找到对应的格子不为null,则cas 该格子内的value+x
// counterCells为空or对应格子为空or update格子失败uncontended=false,
// 则进入fullAddCount,这个方法是一定会加成功的,但是加成功就立刻退出整个方法了,不判断扩容了?
fullAddCount(x, uncontended);
return;
}
// 从put走到addCount,check是一定>=2的,
// 从computeIfAbsent到addCount,可能check =1,意为没有发生哈希冲突的添加元素,则不会检查扩容,
// 毕竟扩容是个耗时的操作
if (check <= 1)
return;
// 统计下元素总个数。
s = sumCount();
}
// 替换节点和清空数组时,check=-1,只做元素个数递减,不会触发扩容检查,也不会缩容。
if (check >= 0) {
// 后面触发扩容可以先不看,后面会一起细说。
// 触发扩容判断 代码省略
}
}
有个疑问,当不得已走到fullAddCount()
,这个方法是一定会修改元素个数成功的,但是成功就立刻退出整个addCount
方法了,不再向后判断扩容?个人猜想:
-
假设走到
fullAddCount
是因为CounterCell[] as
为空,那么外围的if
,就是cas修改baseCount
失败了,说明有其他线程修改成功了由它去检查是否扩容。 -
假设走到
fullAddCount
是因为cas修改counterCell
失败,说明有其他线程修改成功,则由它去检查扩容。 -
既然都执行到
fullAddCount
,这个方法流程上还是比较复杂的,可能较为耗时,作者意图应该是不想让客户端调用时间太长,既然有其他线程去检查扩容了,当前线程就结束吧,不要让调用者等太久。
1、fullAddCount全力修改元素个数
这个方法有些复杂,但是目的很单纯,就是一定要修改成功。自旋里可以分为三个大分支:
-
counterCells
数组不为空,则哈希映射对应格子,多次失败后冲突升级触发扩容,依然不成功则重复哈希映射自旋。 -
counterCells
数组为空,则初始化。 -
没有获取初始化的权利,则cas修改
baseCount
。
需要解释两个变量的作用:
-
collide
,意为是否发生碰撞,即为竞争,cas修改对应位置的格子不成功collide
就会被设置为true,意为升级,再哈希循环一次还不成就可能触发扩容(2倍)。CounterCell[]
数组长度和cpu的核数有关,若数组长度n>=NCPU
不再冲突升级了(collide=false
),也不会再触发扩容,而是不断再哈希自旋重试; -
cellsBusy
,相当于一把自旋锁,cellsBusy=1
获取锁,cellsBusy=0
释放锁。在分支中扩容CounterCell
、新增格子或CounterCell
数组的初始化都会用到cellsBusy
。
private final void fullAddCount(long x, boolean wasUncontended) {
int h; // h 类似于 hash
if ((h = ThreadLocalRandom.getProbe()) == 0) {
// h = 0 则初始化重新获取哈希值,并wasUncontended=true意为没有竞争
ThreadLocalRandom.localInit(); // force initialization
h = ThreadLocalRandom.getProbe();
wasUncontended = true;
}
// false 为没有发生碰撞,竞争
// true 的意为升级为严重竞争级别,可能触发扩容
boolean collide = false; // True if last slot nonempty
for (;😉 {
CounterCell[] as; CounterCell a; int n; long v;
if ((as = counterCells) != null && (n = as.length) > 0) {
// 1. as不等于null且有CounterCell
if ((a = as[(n - 1) & h]) == null) {
// (1)映射找到的CounterCell=null,则新建一个
if (cellsBusy == 0) { // Try to attach new Cell
CounterCell r = new CounterCell(x); // Optimistic create
if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
// cellsBusy 相等于一把乐观锁,到这里说明没有其他线程竞争
boolean created = false;
try { // Recheck under lock
CounterCell[] rs; int m, j;
if ((rs = counterCells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
// j位置依然是空的,则r赋值给j位置
rs[j] = r;
// 设置创建成功
created = true;
}
} finally {
// 释放锁
cellsBusy = 0;
}
if (created)
// 结束循环
break;
continue; // Slot is now non-empty
}
}
collide = false;
}
// (2)映射位置的CounterCell不为空,发生哈希冲突
else if (!wasUncontended) // CAS already known to fail
// (2.1)wasUncontended=false说明有竞争,则不继续向后走去抢了,
// 走(5)再哈希,再次循环就认为没有竞争wasUncontended=true
wasUncontended = true; // Continue after rehash
// (2.2) wasUncontended=true,认为没有竞争,则尝试cas 给该CounterCell里value+x
else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
// 修改成功就退出了
break;
// (2.3)修改未成功
else if (counterCells != as || n >= NCPU)
// (2.3.1)as地址变了(其他线程扩容了) or n即as数组长度已经大于等于cpu核数了
// (没有多余的核数给其他线程),就不冲突升级了,走(5)再哈希,再次循环尝试
collide = false; // At max size or stale
// (2.3.2)counterCells = as && n < NCPU
else if (!collide)
// collide=false 则升级冲突级别为true,走(5)再哈希,再次循环尝试
collide = true;
// (3)已经是严重冲突collide=true
else if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
try {
if (counterCells == as) {// Expand table unless stale
// counterCells 扩容为 2倍
// 这个扩容的触发机制就是映射到的counterCell不为null,且多次尝试cas操作+x失败,
// 且当前counterCells地址没有被修改,且数组长度小于NCPU(cpu核数)时触发2倍扩容
CounterCell[] rs = new CounterCell[n << 1];
for (int i = 0; i < n; ++i)
rs[i] = as[i];
counterCells = rs;
}
} finally {
cellsBusy = 0;
}
collide = false;
continue; // Retry with expanded table
}
// (5)重新生成一个伪随机数赋值给h,进行下一次循环判断
// 再哈希
h = ThreadLocalRandom.advanceProbe(h);
}
// 2.as 为null or as是空的
else if (cellsBusy == 0 && counterCells == as &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
boolean init = false;
try { // Initialize table
// 多次判断counterCells == as,未防止as变更
if (counterCells == as) {
// 初始化CounterCell数组,初始容量为2
CounterCell[] rs = new CounterCell[2];
rs[h & 1] = new CounterCell(x);
counterCells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
// 3. 2中修改CELLSBUSY失败没抢到初始化as的锁,则尝试 直接cas修改baseCount + x
else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
break; // Fall back on using base
}
}
2、sumCount
如何统计所有元素个数呢?基数baseCount+CounterCell[]之和
。
final long sumCount() {
CounterCell[] as = counterCells; CounterCell a;
long sum = baseCount;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
像提供给用户获取元素个数的方法size()
以及判空的isEmpty()
都是调用了sumCount()
。
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}
public boolean isEmpty() {
return sumCount() <= 0L;
}
扩容是ConcurrentHashMap
的核心,明白作者的意图就不觉得难了。扩容机制的触发有三个地方:
-
当更新元素(
put or replace or remove
… )时,哈希映射数组找到的节点的hash值等于MOVED=-1
,表示数组正在扩容,帮助扩容helpTransfer
。 -
当添加元素时,链表达到转红黑树的阈值,若此时数组长度小于
MIN_TREEIFY_CAPACITY=64
,则触发扩容tryPresize
。 -
当添加元素成功后,
addCount
更新元素个数时,元素个数达到扩容阈值则触发扩容。
1、addCount触发扩容
除去链表转红黑树可能触发的扩容,addCount
算是最正统的扩容源头,所以首先从addCount
开始探寻扩容的神秘足迹。
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
// 更新元素个数,详细解析看 三、
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
// ThreadLocalRandom.getProbe() 相当于当前线程的hash值
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
// 找到对应的格子不为null,则cas 该格子内的value+x
// counterCells为空or对应格子为空or update格子失败uncontended=false,
// 则进入fullAddCount,这个方法是一定会加成功的,但是因为这个过程可能会比较耗时,加成功就立刻退出整个方法了,
fullAddCount(x, uncontended);
return;
}
// 从put走到addCount,check是一定>=2的,
// 从computeIfAbsent到addCount,可能check =1,意为没有发生哈希冲突的添加元素,则不会检查扩容。
// 毕竟扩容是个耗时的操作
if (check <= 1)
return;
// 统计下元素总个数。
s = sumCount();
}
// 扩容重点看这里
// 替换节点和清空数组时,check=-1,只做元素个数递减,不会触发扩容检查,也不会缩容。
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
// s 元素个数 >= sc扩容的阈值,并且tab的地址没有改变,并且数组的长度没有达到最大值
// 则开始扩容
// 以n=64举例
// rs=32793 1000000000011001
int rs = resizeStamp(n);
if (sc < 0) {
// 1. 扩容检查,若需要帮助,则帮助扩容
// ①(sc >>> RESIZE_STAMP_SHIFT) != rs 为了判断是否处于扩容状态。
// ②sc=rs+1判断扩容已经结束 百度的
// ③sc==rs+MAX_RESIZERS扩容线程数超过最大值 百度的
// sc < 0 了,rs是一个正数,rs+1和rs + MAX_RESIZERS怎么可能等于一个负数?
// 所以这里是一个bug,和朋友讨论,这里的确是一个bug。
// transferIndex 记录是扩容迁移元素的索引,逆序扩容,transferIndex<=0 说明任务已经迁移任务已经分配完了。
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
// 处于扩容状态,且扩容已经结束 or 扩容的线程达到最大值,则没必要帮助扩容
break;
// 帮助扩容的线程+1
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
// 2. 第一个触发扩容的线程
// (rs << RESIZE_STAMP_SHIFT) + 2,为什么加2呢
// 1000000000011001 0000 0000 0000 0000 + 2
// sc = 1000000000011001 0000 0000 0000 0010
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
// 3. 第一个扩容线程没有触发成功,则重新统计元素总个数,再循环一次。
s = sumCount();
}
}
}
刚才计算的元素总个数s >= sc
扩容的阈值,并且tab
的地址没有改变,并且数组的长度没有达到最大值则触发扩容。
(1)resizeStamp计算过程
rs是什么意思,有什么作用?刚开始就被一块石头绊住了。那就来先看看resizeStamp(n)
的计算过程:
/**
-
返回值作为正在扩容的数据表的size即n的一个标志,rs可以反推出n
-
Returns the stamp bits for resizing a table of size n.
-
Must be negative when shifted left by RESIZE_STAMP_SHIFT.
*/
static final int resizeStamp(int n) {
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
看源码注释,返回值作为正在扩容数组的长度n的一个标志位?并且当向左移RESIZE_STAMP_SHIFT=16
位时得到一个负数?
Integer.numberOfLeadingZeros(n)
的作用是获取n的二进制从左往右连续的0的个数,比如:
2的二进制10从左往右有30个连续的0
4的二进制100从左往右有29个连续的0
8的二进制1000从左往右有28个连续的0
16的二进制10000从左往右有27个连续的0
(int有32位,左边不足的补0)
(1 << (RESIZE_STAMP_BITS - 1))
是干嘛呢?
RESIZE_STAMP_BITS = 16
,1右移15位,相当于1*2^15=32768,是个2的整数次方的数。
很有意思的是,一个数和一个比它大的2的整数次方的数做|运算,相当于两数做加法。很好理解,2的整数次的数二进制1的左边都是0,和一个较小的数做|运算就是把这个较小的数补到这个较大的2的整数次方数的低位上。
所以resizeStamp
计算过程示例:
n=2, resizeStamp=30+32768=32798,二进制:1000 0000 0001 1110
n=4, resizeStamp=29+32768=32797,二进制:1000 0000 0001 1101
n=8, resizeStamp=28+32768=32796,二进制:1000 0000 0001 1100
n=16, resizeStamp=27+32768=32795,二进制:1000 0000 0001 1011
验证源码注释的两个点:
-
返回值作为正在扩容数组的长度n的一个标志位?的确可以,比如32798是n=4的扩容标志位,
32-(32797-32768)
可反推出 n=4。 -
返回值右移
RESIZE_STAMP_SHIFT=16
位,确实得到一个负数(左边第一位是1了),而且是一个绝对值很大的负数。
(2)扩容线程计数
知道了resizeStamp
的计算过程,看看它用在了哪里:
-
sc >0
说明还没有开始扩容,则触发第一个扩容线程,cas修改sc为(rs << RESIZE_STAMP_SHIFT) + 2)
,rs左移16位,得到一个负数再+2,为什么+2?没有get到作者的意图。 -
sc<0
可能已经开始扩容了,则判断是否在扩容状态?是否已经扩容结束?是否扩容线程达到最大?当前数组处于扩容状态且扩容未结束,扩容线程数也没有达到最大值,则帮助扩容,线程数+1(sc+1
)。 -
阅读后面的代码,一个线程的扩容任务完成后会
sc-1
,即线程数-1。
这个思路挺清晰,但是,判断扩容结束和扩容线程数达到最大值,总觉得有问题:
小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Java工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Java开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Java)
101
n=8, resizeStamp=28+32768=32796,二进制:1000 0000 0001 1100
n=16, resizeStamp=27+32768=32795,二进制:1000 0000 0001 1011
验证源码注释的两个点:
-
返回值作为正在扩容数组的长度n的一个标志位?的确可以,比如32798是n=4的扩容标志位,
32-(32797-32768)
可反推出 n=4。 -
返回值右移
RESIZE_STAMP_SHIFT=16
位,确实得到一个负数(左边第一位是1了),而且是一个绝对值很大的负数。
(2)扩容线程计数
知道了resizeStamp
的计算过程,看看它用在了哪里:
-
sc >0
说明还没有开始扩容,则触发第一个扩容线程,cas修改sc为(rs << RESIZE_STAMP_SHIFT) + 2)
,rs左移16位,得到一个负数再+2,为什么+2?没有get到作者的意图。 -
sc<0
可能已经开始扩容了,则判断是否在扩容状态?是否已经扩容结束?是否扩容线程达到最大?当前数组处于扩容状态且扩容未结束,扩容线程数也没有达到最大值,则帮助扩容,线程数+1(sc+1
)。 -
阅读后面的代码,一个线程的扩容任务完成后会
sc-1
,即线程数-1。
这个思路挺清晰,但是,判断扩容结束和扩容线程数达到最大值,总觉得有问题:
小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Java工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Java开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-O1IUN72d-1710870301446)]
[外链图片转存中…(img-RecU87Ov-1710870301447)]
[外链图片转存中…(img-RlqTDMV5-1710870301447)]
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Java)
[外链图片转存中…(img-TK5Db71H-1710870301447)]