ConcurrentHashMap源码深度解析(二)(1),2024年最新计算机程序设计员英语

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。

这个思路挺清晰,但是,判断扩容结束和扩容线程数达到最大值,总觉得有问题:

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;

// 帮助扩容的线程+1

if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))

transfer(tab, nt);

}

sc < 0,那就是一个负数,rs是一个很大的正数:

  • (sc >>> RESIZE_STAMP_SHIFT) != rs,这个好理解,第一个扩容线程修改sc为rs左移16位再+2(sc=(rs << RESIZE_STAMP_SHIFT) + 2)),那此时sc是一个很小的负数,sc右移16位(抹掉了低位)可以回推出rs。所以这里就是为了判断当前数组的长度是否处于扩容状态。

  • sc == rs + 1,百度说是扩容结束的标志。sc是个负数,rs是个正数+1还是个正数,一个负数怎么也不可能等于一个正数。

  • sc == rs + MAX_RESIZERS,百度说是扩容线程数达到最大值的标志。同理显然也是不可能成立的。

难道是源码错了?官方JDK啊,而且市面上很多商业项目都在用java8,不可能出bug啊?百度很多国内博客也没人觉得这里是bug呀,我疑惑了,想了几个晚上,最后决定求助于一位大佬朋友。

大佬朋友很牛逼,搜到了国外一个专门收集jdk bug的论坛(国内打开国外网站超级慢+英文不好,直接劝退一拨人),国际同道中人也有类似的疑惑:

openjdk.bug

既然认为这是java8的bug,那为什么实际使用中不影响呢?判断扩容是否结束和判断扩容线程数达到最大都不起作用了,这样扩容的线程数也不受限制了,(sc >>> RESIZE_STAMP_SHIFT) != rs判断当前数组是否在扩容状态,也是可以判断扩容是否结束的,扩容真的结束了,sc会修改为新数组的扩容阈值,自然就不处于扩容状态了,只是会使得一些线程参与到了扩容中却发现结束了(逻辑虽然不严谨,但是对项目运行性能的影响也不大)。

既然是bug,后面的版本应该会修复这个bug吧,找到java11还没修复,找到java12看样子修复了:

java12

(3)transfer元素迁移

不管第一个扩容线程还是帮助线程都会调用transfer,顾名思义,转移,从旧数组转移到新数组的过程。这个过程中涉及到扩容线程任务的分配和元素的复制迁移。(吐槽一下官方源码,一个函数代码长就算了,if-else还很多,在实际开发中,讲究一个去else化,能及时返回就及时返回,搞这么多else分支,还互相嵌套,可读性很差诶。)

注释很清楚,虽然长,情况又多,反复琢磨几遍,明白作者的意图,就觉得还行,是人的思维。

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {

int n = tab.length, stride;

// 单核不拆分,下面讨论多核的情况

// 计算步长,拆分任务n >>> 3 = n / 2^3

// 先将n分为8份,然后等分给每个cpu,若最后计算的步长小于最小步长16,则设置为16

if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)

stride = MIN_TRANSFER_STRIDE; // subdivide range

if (nextTab == null) { // initiating

try {

// 扩容 2倍

@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 记录迁移进度

transferIndex = n;

}

int nextn = nextTab.length;

ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);

// 从后面的迁移逻辑看到 迁移复制元素是逆序迁移

// advance= true 则代表可继续向前一个位置迁移复制元素

boolean advance = true;

// 是否所有线程都全部迁移完毕,true则可以将nextTab赋值给table了

boolean finishing = false; // to ensure sweep before committing nextTab

// i 代表当前线程正在迁移的数组位置,bound代表它本次可以迁移的范围下限

for (int i = 0, bound = 0;😉 {

Node<K,V> f; int fh;

while (advance) {

int nextIndex, nextBound;

// (1)两种情况不需要继续向前一个位置迁移复制元素(逆序):

// ①i每次自减1,i>=bound说明本批次迁移未完成,不需要继续向前推进。

// ②finishing标志为true,说明所有线程分配的迁移任务都已经完成了,则不需要向前推进。

// 若 --i < bound,说明当前批次的迁移任务完成,可继续分配新范围的任务

// 也就是一个线程可以多次分到任务,能者多劳。

if (–i >= bound || finishing)

// 向前一个位置迁移复制元素

advance = false;

//(2) 每次执行,都会把 transferIndex 最新的值同步给 nextIndex

else if ((nextIndex = transferIndex) <= 0) {

//若 transferIndex小于等于0,则说明原数组中所有位置的迁移任务都分配完毕(不代表所有位置都迁移完毕)

//于是,需要跳出while循环,并把 i设为 -1,

// 以跳到(4)判断正在处理的线程是否完成自己负责范围内迁移工作。

i = -1;

advance = false;

}

else if (U.compareAndSwapInt

(this, TRANSFERINDEX, nextIndex,

nextBound = (nextIndex > stride ?

nextIndex - stride : 0))) {

//(3)cas 设置TRANSFERINDEX,分配任务范围[nextBound,nextIndex),任务的长度是stride

// 举例,假设 n=64,即初始的transferIndex=64,stride=16

// nextIndex=transferIndex=64,nextBound=nextIndex-stride=48

// bound=48

// i=63

// 从后往前复制

bound = nextBound;

i = nextIndex - 1;

advance = false; // 本次任务分配完成,结束循环

}

}

// (4)i已经越界了,整个数组的迁移任务已经全部分配完毕

if (i < 0 || i >= n || i + n >= nextn) {

int sc;

if (finishing) {

// 扩容完毕

// nextTable置为空

nextTable = null;

// 新数组赋值给旧数组

table = nextTab;

// sizeCtl 设置为新的数组长度的 3/4.即 3/4 *2n

sizeCtl = (n << 1) - (n >>> 1);

return;

}

// 到这,说明所有的迁移任务都分配完了

// 当前线程也已经完成了自己的迁移任务(无论参与了几次迁移),

// 则sc-1,表明参与扩容的线程数减1

if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {

// 迁移开始时,会设置 sc=(rs << RESIZE_STAMP_SHIFT) + 2

// 每当有一个线程参与迁移,sc 就会加 1。

// 因此,这里就是去校验当前 sc 是否和初始值相等。

if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)

// 不相等,当前线程扩容任务结束。

return;

// 相等,说明还有一个线程还在扩容迁移(不一定是触发扩容的第一个线程)

// 则当前线程会从后向前检查一遍,哪些位置的节点没有复制完,就帮忙一起复制。

// 一圈扫描下来,肯定是全部迁移完毕了,则finishing可提前设置为true。

finishing = advance = true;

i = n; // recheck before commit

}

}

else if ((f = tabAt(tab, i)) == null)

// (5)若i的位置元素为空,就把占位节点设置为fwd标志。

// 设置成功,advance置为true,向前推进复制

advance = casTabAt(tab, i, null, fwd);

else if ((fh = f.hash) == MOVED)

// (6)若当前位置的头结点是 ForwardingNode ,则说明这个位置的所有节点已经迁移完成,

// 可以继续向前迁移复制其他位置的节点

advance = true; // already processed

else {

// (7)对tab[i]进行迁移,可能是链表 or 红黑树

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并不是一条链表的最后一个,一条链表的节点可以分为两类,

// 在循环中寻找lastRun的满足条件是链表中最后一个与前一个节点runBit不相等的节点作为lastRun,

// 而此时lastRun后面可能还有节点,但runBit都是和lastRun相等的节点。

// 这里找lastRun和java7是一样的

for (Node<K,V> p = f.next; p != null; p = p.next) {

// 计算p的位置

int b = p.hash & n;

if (b != runBit) {

// 和runBit不是同一位置

runBit = b;

lastRun = p;

}

}

// hash & n=0为低位节点,hash & n!=0为高位节点。

// 判断找到的lastRun是低位节点还是高位节点

if (runBit == 0) {

ln = lastRun;

hn = null;

}

else {

hn = lastRun;

ln = null;

}

// lastRun之前的结点因为fh&n不确定,所以全部需要再hash分配。

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);

setTabAt(tab, i, fwd);

advance = true;

}

else if (f instanceof TreeBin) {

// 是红黑树,

// 原理上和链表迁移的过程差不多,也是将节点分成高位节点和低位节点

TreeBin<K,V> t = (TreeBin<K,V>)f;

// lo低位树头节点,loTail低位树尾节点

// hi高位树头节点,hiTail高位树尾节点

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;

}

}

// 低位节点的个数 <= UNTREEIFY_THRESHOLD=6, 则树退为链表

// 否则判断是否有高位节点,无,则原先那棵树t就是一棵低位树,直接赋值给ln

// 有高位节点,则低位节点重新树化。

// 高位节点的判断同理

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;

}

}

}

}

}

}

那多线程间是如何分配任务的呢?如何将元素从旧数组迁移到新数组的呢?让我再来给你捋捋:

(3.1)多线程间是如何分配任务

分配任务

首先看开头步长stride的概念,任务分配单元,就是一次分给线程stride个位置的元素迁移任务。只考虑多核的情况stride计算方式是将原数组的长度分成8份,然后再等分给每个cpu,但是最小步长是MIN_TRANSFER_STRIDE=16

transferIndex记录迁移的进度,初始为原数组的长度n,逆序进行。ForwardingNode转发节点,某个位置的元素迁移完了但是整个迁移任务还没结束,这个位置就会被ForwardingNode占位,写线程遇到ForwardingNode会帮助扩容,读线程遇到ForwardingNode转发请求。

每次线程分配的任务区间为[transferIndex-stride, transferIndex)transferIndex每次递减stride。源码中是首先会把最新的transferIndex赋值给nextIndex,然后nextIndex-stride赋值给nextBound,同时cas更新transferIndexnextBound,即分配了一个stride单位的任务,一个单位任务的区间为[nextBound,nextIndex)。然而,不是一个线程只分配一个stride单位的任务,该线程的迁移任务做完了,整个大任务还没有分配完(transferIndex>0),则还会继续分配,能者多劳。

在一个[nextBound,nextIndex)单位的任务做迁移时,从i=nextIndex-1开始降序遍历,直到i--小于bound=nextIndex说明这个单位的任务做完了,可以分配新的任务了。当一个位置的节点都迁移完了,旧数组该位置会被fwd占位,同时advance置为true,表示可以向下一个位置迁移元素了,此时又来到上面的while (advance){}i--,开始新位置的元素迁移。

transferIndex<=0时,表示所有任务分配完毕,且分给当前线程的迁移任务都做完了,则i置为-1。此时当前线程也没必要再帮助扩容了,则扩容线程数-1(sc - 1)。但是这里有个点,作者想的很缜密,当还有一个线程的迁移任务没有做完时((sc - 2) = resizeStamp(n) << RESIZE_STAMP_SHIFT),此时倒数第二个线程自己的任务做完了,并不会立即停止,而是从后向前再遍历检查一遍,是否所有位置的节点都迁移完了,还没有开始迁移的位置则迁移,所以倒数第二个线程可能会帮助最后一个线程做任务,一圈扫描下来,可确保所有位置的元素都迁移完了,此时就可以将新数组nextTab替换旧数组table,同时设置新的扩容阈值给sizeCtl

(3.2)将元素从旧数组迁移到新数组

迁移

迁移元素的过程也很有意思,相对于java7又做了优化,java7只有链表,在复制迁移元素时,首先会找到lastRun,将一条链表分为两种节点:lastRun前和lastRun后。lastRun的概念是最后一个与前驱节点哈希映射不一样的节点,这样就可以分出lastRun后面的节点和lastRun是同类节点,迁移到新数组还是同一条链表,所以可以跟随lastRun一直做迁移,而lastRun之前的节点需要一个个再哈希映射复制到新数组中。

java8依然用了lastRun的概念,同时也用了高低位节点的概念,将一条链表或红黑树分为两种节点,即两条链表,高位节点链表和低位节点链表。那是如何区分的呢?

hash & n=0为低位节点,hash & n!=0为高位节点。低位节点迁移到新数组时位置不会变,原来在旧数组的位置为i,到新数组还是i;而高位节点,原来在数组的位置为i,到新数组的位置则为i+旧数组的长度n。为什么有这个规律呢?

因为n是旧数组的长度,且数值是2的整数次幂,即 n=2^(m-1)(m为不小于2的正整数),对应二进制只有第m位是1,其余都是0,如16(10000),32(100000)。所以ph & n只有两种结果,0或者n,等于0时说明ph的第m位是0,等于n时说明ph的第m位是1。

n的掩码 mask=n-1,16的掩码1111,32的掩码11111,2n的掩码比n的掩码多一位1,十进制上多n。mask&ph起到模运算的效果来映射数组下标:

  • ph & n=0时, ph的第m位为0,是无效位,所以ph&(n-1)一定等于ph&(2n-1)

  • ph & n=n时, ph的第m位是1,是有效位,ph&(2n-1)的第m位为1,其余位和ph&(n-1)一样,所以ph&(n-1)+n=ph&(2n-1)

正是因为有这个规律,所以将一条链表分为高低位两条链表,将元素从旧数组迁移到新数组时只需要迁移两条链表就行了。

至于该位置要是红黑树,同理一棵树分成两颗树,低位树和高位树,若树的节点个数小于等于UNTREEIFY_THRESHOLD=6,则树退化为链表,否则重新树化为一颗红黑树。同理也只需要迁移两条链表(lnhn)就可以了。

2、helpTransfer帮助扩容

当更新元素时,遇到该位置为转发节点ForwardingNode时,数组正在扩容,但是当前位置的元素已经迁移完了,则判断是否需要帮助扩容。(因为java8的代码有问题,如下复制自java12)

final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {

Node<K,V>[] nextTab; int sc;

// 判断当前节点f是否为ForwardingNode,且其转发的nextTable是否为空

if (tab != null && (f instanceof ForwardingNode) &&

(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {

int rs = resizeStamp(tab.length) << RESIZE_STAMP_SHIFT;

while (nextTab == nextTable && table == tab &&

(sc = sizeCtl) < 0) {

// 新数组nextTable,旧数组table地址均未改变,且sc<0,

// 则判断扩容线程数是否达到最大值sc == rs + MAX_RESIZERS

// 扩容是否结束sc == rs + 1还没来得及commit(table=nextTable)

// 任务是否分配完毕 transferIndex <=0

if (sc == rs + MAX_RESIZERS || sc == rs + 1 ||

transferIndex <= 0)

break;

// 需要帮助扩容,扩容线程数+1

if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1)) {

transfer(tab, nextTab);

break;

}

}

return nextTab;

}

return table;

}

3、tryPresize链表转红黑树触发扩容

链表的节点个数达到转为红黑树的阈值,但是数组的长度小于64时,触发扩容而不是树化。

tryPresize有可能是putAll调用的,参数size就是添加的map的size,当数组table还没初始化时触发初始化,根据size和扩容因子计算的预估容量c(tableSizeFor详解以及初始容量优化见上一篇文章)和原table的初始容量sc比较,谁大谁则成为新初始容量。

若数组table不为空有元素,putAll时,添加的元素预估容量c可能会大于扩容阈值sc,则先触发扩容再批量添加元素。

tryPresizetreeifyBin调用,tryPresize(n << 1),为确保因达到转为红黑树阈值但数组长度小于64情况下一定要触发扩容,所以传给tryPresize的参数是数组长度的2倍,使得计算的c一定大于扩容阈值,但是只传个n也是一定大于扩容阈值的,不太能get到作者为什么要传个n<<1

private final void tryPresize(int size) {

// c 计算合适的扩容 容量,size >= MAXIMUM_CAPACITY/2 则合适的容量是MAXIMUM_CAPACITY

// 否则找一个>=size且离size最近的2的整数次方的值

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) {

// tryPresize 在 putAll里调用时,如果数组还未初始化,则进行数组初始化

// putAll 时 size传的是 需要添加的map的size,c是根据size计算的一个预估容量值,为的是避免不必要的扩容

// 预估值c和原数组初始容量谁大,谁就是新的初始容量

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;

}

}

}

// table不为空且有数据

// putAll 计算的c可能大于sc扩容阈值,则会先触发扩容,则批量添加元素。

// 而treeifyBin 传进来的参数为n<<1,是一定大于扩容阈值sc的,所以是一定会触发扩容的

else if (c <= sc || n >= MAXIMUM_CAPACITY)

break;

else if (tab == table) {

// 这里java12和java8不太一样了

// java8 还会判断 是第一个触发扩容还是帮助扩容

// 而java12认为是第一个线程触发扩容,若cas失败,则说明有其他线程触发,也不帮助。

int rs = resizeStamp(n);

if (U.compareAndSetInt(this, SIZECTL, sc,

(rs << RESIZE_STAMP_SHIFT) + 2))

transfer(tab, null);

}

}

}

五、get操作遇到正在扩容


当get操作时,哈希映射的位置节点因为扩容被迁移到新数组了,该怎么办呢?java8在扩容的过程中,已经迁移完的位置会放一个fwd(ForwardingNode)转发节点,get操作遇到fwd,则检索请求从旧数组转发到新数组,这就保证了扩容的实时一致性,而fwd的作用还有一个就是当更新操作遇到某位置的占位节点fwd时,会帮助扩容,没遇到就正常操作。

java8扩容引入fwd也是和java7扩容的一个大区别,java7是一个segment内的HashEntry数组完全迁移完,再将新数组替换旧数组,只能保证最终一致性,get操作是不会感知数组正在扩容的,其他操作也是感知不到的,所以也就不会有帮助扩容的概念。

public V get(Object key) {

Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;

// 1. hash高低位扰动

int h = spread(key.hashCode());

// 2.hash映射找到对应数组中的槽

if ((tab = table) != null && (n = tab.length) > 0 &&

(e = tabAt(tab, (n - 1) & h)) != null) {

if ((eh = e.hash) == h) {

// 3.如果 槽中的占位节点就是要找的key,则返回

if ((ek = e.key) == key || (ek != null && key.equals(ek)))

return e.val;

}

// 4.eh < 0 可能遇到 红黑树 or ForwardingNode

else if (eh < 0)

return (p = e.find(h, key)) != null ? p.val : null;

// 5. 否则就是普通的链表 遍历寻找

while ((e = e.next) != null) {

if (e.hash == h &&

((ek = e.key) == key || (ek != null && key.equals(ek))))

return e.val;

}

}

return null;

}

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;

}

// 转发到nextTable中继续检索

Node<K,V> find(int h, Object k) {

// loop to avoid arbitrarily deep recursion on forwarding nodes

outer: for (Node<K,V>[] tab = nextTable;😉 {

Node<K,V> e; int n;

if (k == null || tab == null || (n = tab.length) == 0 ||

(e = tabAt(tab, (n - 1) & h)) == null)

// 新数组 映射的槽是空的则返回null

return null;

for (;😉 {

int eh; K ek;

if ((eh = e.hash) == h &&

((ek = e.key) == k || (ek != null && k.equals(ek))))

return e;

if (eh < 0) {

if (e instanceof ForwardingNode) {

tab = ((ForwardingNode<K,V>)e).nextTable;

// 又遇到另一个转发节点,跳过一次外围循环,从新的tab检索,

// 不会在扩容阶段又在新数组上扩容把?有待后续验证

continue outer;

}

else

// 这里就是红黑树了,去树上找

return e.find(h, k);

}

if ((e = e.next) == null)

// 到最后了还没找到则返回null

return null;

}

}

}

}

六、replaceNode更新节点


removereplace都是调用replaceNode方法。remove调用时,传入的valuecv都是null,若遍历节点找到key相等的节点则会将该节点删除。

/**

  • Implementation for the four public remove/replace methods:

  • Replaces node value with v, conditional upon match of cv if

  • non-null. If resulting value is null, delete.

  • @param key 需要替换的key

  • @param value 替换的新值

  • @param cv 旧值

  • @return

*/

final V replaceNode(Object key, V value, Object cv) {

int hash = spread(key.hashCode());

for (Node<K,V>[] tab = table;😉 {

Node<K,V> f; int n, i, fh;

if (tab == null || (n = tab.length) == 0 ||

(f = tabAt(tab, i = (n - 1) & hash)) == null)

// 1.哈希映射位置的节点为null,则不需要替换

break;

else if ((fh = f.hash) == MOVED)

// 2.fh=MOVED,正在扩容,则帮助扩容

tab = helpTransfer(tab, f);

else {

V oldVal = null;

boolean validated = false;

synchronized (f) {

if (tabAt(tab, i) == f) {

if (fh >= 0) {

// 普通链表替换

validated = true;

for (Node<K,V> e = f, pred = null;😉 {

K ek;

if (e.hash == hash &&

((ek = e.key) == key ||

(ek != null && key.equals(ek)))) {

// key和hash相等

// cv 不为null,需要判断cv和ev是否相等,相等才能替换 or 删除

V ev = e.val;

if (cv == null || cv == ev ||

(ev != null && cv.equals(ev))) {

oldVal = ev;

if (value != null)

// 传入的value!=null,则替换旧值为新值

e.val = value;

else if (pred != null)

// 传入的value=null,且pred前驱不为null,则删除该节点

pred.next = e.next;

else

// 前驱为null,说明删除的是头节点

setTabAt(tab, i, e.next);

}

// 已经找到节点并对其做了处理,结束循环

break;

}

// 继续遍历

pred = e;

if ((e = e.next) == null)

// 下一个节点为null,说明到尾部了,结束循环,需要替换的节点不存在

break;

}

}

else if (f instanceof TreeBin) {

// 红黑树 替换

validated = true;

TreeBin<K,V> t = (TreeBin<K,V>)f;

TreeNode<K,V> r, p;

if ((r = t.root) != null &&

(p = r.findTreeNode(hash, key, null)) != null) {

// r.findTreeNode 从红黑树中找到节点

V pv = p.val;

if (cv == null || cv == pv ||

(pv != null && cv.equals(pv))) {

// 传入cv旧值,则需要判断

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数同学面临毕业设计项目选题时,很多人都会感到无从下手,尤其是对于计算机专业的学生来说,选择一个合适的题目尤为重要。因为毕业设计不仅是我们在大学四年学习的一个总结,更是展示自己能力的重要机会。

因此收集整理了一份《2024年计算机毕业设计项目大全》,初衷也很简单,就是希望能够帮助提高效率,同时减轻大家的负担。
img
img
img

既有Java、Web、PHP、也有C、小程序、Python等项目供你选择,真正体系化!

由于项目比较多,这里只是将部分目录截图出来,每个节点里面都包含素材文档、项目源码、讲解视频

如果你觉得这些内容对你有帮助,可以添加VX:vip1024c (备注项目大全获取)
img

l,需要判断cv和ev是否相等,相等才能替换 or 删除

V ev = e.val;

if (cv == null || cv == ev ||

(ev != null && cv.equals(ev))) {

oldVal = ev;

if (value != null)

// 传入的value!=null,则替换旧值为新值

e.val = value;

else if (pred != null)

// 传入的value=null,且pred前驱不为null,则删除该节点

pred.next = e.next;

else

// 前驱为null,说明删除的是头节点

setTabAt(tab, i, e.next);

}

// 已经找到节点并对其做了处理,结束循环

break;

}

// 继续遍历

pred = e;

if ((e = e.next) == null)

// 下一个节点为null,说明到尾部了,结束循环,需要替换的节点不存在

break;

}

}

else if (f instanceof TreeBin) {

// 红黑树 替换

validated = true;

TreeBin<K,V> t = (TreeBin<K,V>)f;

TreeNode<K,V> r, p;

if ((r = t.root) != null &&

(p = r.findTreeNode(hash, key, null)) != null) {

// r.findTreeNode 从红黑树中找到节点

V pv = p.val;

if (cv == null || cv == pv ||

(pv != null && cv.equals(pv))) {

// 传入cv旧值,则需要判断

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数同学面临毕业设计项目选题时,很多人都会感到无从下手,尤其是对于计算机专业的学生来说,选择一个合适的题目尤为重要。因为毕业设计不仅是我们在大学四年学习的一个总结,更是展示自己能力的重要机会。

因此收集整理了一份《2024年计算机毕业设计项目大全》,初衷也很简单,就是希望能够帮助提高效率,同时减轻大家的负担。
[外链图片转存中…(img-QGrURvOm-1712514024071)]
[外链图片转存中…(img-6nrv2jcd-1712514024071)]
[外链图片转存中…(img-U02wLdPl-1712514024072)]

既有Java、Web、PHP、也有C、小程序、Python等项目供你选择,真正体系化!

由于项目比较多,这里只是将部分目录截图出来,每个节点里面都包含素材文档、项目源码、讲解视频

如果你觉得这些内容对你有帮助,可以添加VX:vip1024c (备注项目大全获取)
[外链图片转存中…(img-LosvSF0T-1712514024072)]

  • 8
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值