前言

这是一篇对 transfer 方法的拾遗,关于之前那篇文章的一些一笔带过,或者当时不知道的地方进行回顾。

疑点 1. 为什么将链表拆成两份的时候,0 在低位,1 在高位?

回顾一下 transfer 的相关代码:

int runBit = fh & n;
Node<K,V> lastRun = f;
for (Node<K,V> p = f.next; p != null; p = p.next) {
// 取于桶中每个节点的 hash 值
int b = p.hash & n;
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
if (runBit == 0) {// 如果最后更新的 runBit 是 0 ,设置低位节点
ln = lastRun;
hn = null;
}
else {
hn = lastRun; // 如果最后更新的 runBit 是 1, 设置高位节点
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;
// 如果与运算结果是 0,那么就还在低位
if ((ph & n) == 0) // 如果是0 ,那么创建低位节点
ln = new Node<K,V>(ph, pk, pv, ln);
else // 1 则创建高位
hn = new Node<K,V>(ph, pk, pv, hn);
}

关键看上面注释的代码,如果 runBit 是 0,那么就设置在低位节点,反之,如果是 1,设置在高位。

为什么这么设计呢?当时楼主一笔带过,称之为这个貌似没有什么特殊含义,实在是愚蠢之极。

今天解释一下。

这要从 ConcurrentHashMap 的取于下标算法开始说起。

我们知道,在 putVal 方法中,会通过取于对象的 hash 值获取下标。具体代码如下:

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

也就是 (n - 1) & hash),这个 n 就是 length。这个其实相当于 hash % n(n 必须是2的指数)。但是比 % 更高效。

复习一下与运算:第一个操作数的的第n位于第二个操作数的第n位如果都是1,那么结果的第n为也为1,否则为0.

然后开始推导:

(n - 1) & hash),取于算法。
假设,我们的 table 长度是 16,也就是 10000,减一就是 01111. 取于下面这个数。这个数特别之处在于,
他的右起第 5 位是 0。如果是 10000 & 这个数,结果是 0.
000000001111 000000010000
010101001001 // 结果 9 010101001001 // &运算结果: 0 当我们扩容后,16 变成 32,也就是 10000. 再看看 (n - 1) & hash) 的结果: 000000011111
010101001001 // 结果还是 9 从这里可以看出,如果 & 运算是 0 ,那么即使扩容,下标也是不变的。 再看看另一种情况,换一个 hash 数字,右起第五位是 1 :
000000001111 000000010000
010101010001 // 结果 1 010101010001 // &运算结果: 1 这里的 & 与运算后,结果是 1,和上面的不同。同时, (n - 1) & hash) 的结果也是 1. 当扩容后,结果是什么样子呢?
000000011111
010101010001 // 结果变化:10001 == 17 可以看到,(n - 1) & hash) 的结果是 17,17 - 1,刚好是 16,而这个 16 的原因是我们的二进制进了一位。

现在明白了吧?0 在低位,1 在高位不是随便设计的。这里让我想到了一致性 hash 算法:当桶的数量变化了,那么 hash 的位置也会变化

这里的设计是为了防止下次取值的时候,hash 不到正确的位置。

实际上,JDK 1.8 的 HashMap 也是这么实现的重新散列。文章深入理解 HashMap put 方法(JDK 8逐行剖析)。其中 resize 方法和这里高度类似。

疑点 2:为什么会有 i >= n || i + n >= nextn 的判断?

回顾一下代码:

if (i < 0 || i >= n || i + n >= nextn) {
int sc;
if (finishing) {
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1);
return;
}

这个判断在当时看来是没有可能存在的。到现在也没明白为什么。。。。

如果有大佬知道,请指点一二。

ConcurrentHashMap 扩容分析拾遗的更多相关文章

  1. ConcurrentHashMap原理分析(1.7与1.8)-put和 get 需要执行两次Hash

    ConcurrentHashMap 与HashMap和Hashtable 最大的不同在于:put和 get 两次Hash到达指定的HashEntry,第一次hash到达Segment,第二次到达Seg ...

  2. external-resizer 源码分析/pvc 扩容分析

    kubernetes ceph-csi分析目录导航 基于tag v0.5.0 https://github.com/kubernetes-csi/external-resizer/releases/t ...

  3. ConcurrentHashMap原理分析(二)-扩容

    概述 在上一篇文章中介绍了ConcurrentHashMap的存储结构,以及put和get方法,那本篇文章就介绍一下其扩容原理.其实说到扩容,无非就是新建一个数组,然后把旧的数组中的数据拷贝到新的数组 ...

  4. ConcurrentHashMap原理分析(1.7与1.8)

    前言 以前写过介绍HashMap的文章,文中提到过HashMap在put的时候,插入的元素超过了容量(由负载因子决定)的范围就会触发扩容操作,就是rehash,这个会重新将原数组的内容重新hash到新 ...

  5. 并发编程——ConcurrentHashMap#addCount() 分析

    前言 ConcurrentHashMap 精华代码很多,前面分析了 helpTransfer 和 transfer 和 putVal 方法,今天来分析一下 addCount 方法,该方法会在 putV ...

  6. 并发编程——ConcurrentHashMap#helpTransfer() 分析

    前言 ConcurrentHashMap 鬼斧神工,并发添加元素时,如果 map 正在扩容,其他线程甚至于还会帮助扩容,也就是多线程扩容.就这一点,就可以写一篇文章好好讲讲.今天一起来看看. 源码分析 ...

  7. Java ConcurrentHashMap 源代码分析

    Java ConcurrentHashMap jdk1.8 之前用到过这个,但是一直不清楚原理,今天抽空看了一下代码 但是由于我一直在使用java8,试了半天,暂时还没复现过put死循环的bug 查了 ...

  8. 【转】ConcurrentHashMap原理分析(1.7与1.8)

    https://www.cnblogs.com/study-everyday/p/6430462.html 前言 以前写过介绍HashMap的文章,文中提到过HashMap在put的时候,插入的元素超 ...

  9. java并发系列(七)-----ConcurrentHashMap原理分析(JDK1.8)

    JDK1.8的实现已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashM ...

随机推荐

  1. ASP.NET Web API 异常处理 HttpResponseException 以及Angularjs获取异常信息并提示

    一.HttpResponseException 如果一个Web API控制器抛出一个未捕捉异常,默认地,大多数异常都会被转化成一个带有状态码“500 – 内部服务器错误”的HTTP响应.HttpRes ...

  2. unigui session超时时间设置

    unigui session超时时间设置 默认的SESSION超时时间是10分钟. 网络 SOCKET 程序,像 数据库,中间件,UNIGUI等...为了防止过多的僵死连接卡死服务端,服务端都会主动踢 ...

  3. string转Date转回String(JAVA)

    String dateString = "2012-12-06 ";   SimpleDateFormat sdf = new SimpleDateFormat("yyy ...

  4. iOS4.0及以上系统,关闭程序,applicationWillTerminate为调用问题解决

    iOS4.0以后系统支持多任务类型,程序按下home键后,程序进入后台运行,如果内存不足被系统关闭或者手动关闭,都不会调用applicationWillTerminate回调函数. 解决方法:可以在程 ...

  5. !!! no internal routing support, rebuild with pcre support !!!

    sudo apt-get install libpcre3 libpcre3-dev sudo pip install uwsgi -I --no-cache-dir

  6. nginx在Linux下的安装

    安装之前的环境装备: 1.ngiinx 是C 语言开发的,我们上传的文件还是源码,需要gcc环境编译源码 : yum install gcc-c++ 2.nginx的http模块使用pcre来解析正则 ...

  7. Testing - 软件测试知识梳理 - 软件可靠性测试

    软件可靠性的基本概念 错误,缺陷,故障和失效 错误:指的是软件在生命周期中各个阶段的状态和行为与人们的期待不一致的偏差,不单单是软件系统本身,中间产品的偏差也算是软件错误 缺陷:指的是软件中一切不好的 ...

  8. 使用bash echo 输出回车转义

    输出回车 [root@~]# echo -e 'hello\n'hello 回车去掉 [root@~]# echo -n hello hello[root@~]#

  9. POJ 2840

    #include<iostream> #include<stdio.h> #include<string> using namespace std; int mai ...

  10. asp.net core 系列之用户认证(1)-给项目添加 Identity

    对于没有包含认证(authentication),的项目,你可以使用基架(scaffolder)把 Identity的程序集包加入到项目中,并且选择性的添加Identity的代码进行生成. 虽然基架已 ...