ConcurrentHashMap源码相关

ConcurrentHashMap存储数据的结构是什么样子的

和hashMap的结构基本是一致的,都是数组加链表加红黑树,存储数据的单元还是node结构,node里面有key 、value、指向下一个node的next节点,还有hash值,next值还有一个作用就是解决hash冲突之后生成链表使用的。


ConcurrentHashMap的负载因子是多少可以修改么

是0.75不可以修改也不可以指定,final修饰的。


node.hash字段一般是大于等于0的,为什么

这个hash值赋值有其他特殊含义,散列表在扩容的时候会触发数据迁移的过程,就是把原表的数据迁移到扩容之后的一个新的散列表的一个逻辑。 老的散列表迁移完的桶需要放一个标记节点,叫forwardingNode节点。 这个node的hash值默认是-1.

还有一种在红黑树的场景下这个红黑树由一个特殊的节点, TreeBin结构。它本身也是继承自node hash值是-2.


ConcurrentHashMap 的sizeCtl等于-1的时候等于什么意思

表示当前的散列表正在做初始化,hashMap的散列表是在使用的时候创建的,currentHashMap也是这样,但是他需要在并发的条件下,散列表结构只会被创建一次,当多个线程执行到initTable的时候会使用CAS的方式去更改sizeCtl的值 ,采用期望值是0,更新之后是-1的方式修改,并发的情况下只有一个线程会执行成功,然后由这个线程去执行initTable的逻辑,CAS失败的线程会进行一个自选检查这个散列表是否被创建出来, 在这部分doug lea进行过优化,每次自旋检查之后,会让线程短暂的释放它所占用的CPU,让当前线程重新取竞争CPU资源,把CPU资源让给更加饥饿的线程去使用。


ConcurrentHashMap 的sizeCtl字段大于零并且这个散列表已经初始化完成,表示什么

表示下次触发扩容的阈值,大于等于阈值的之后就会触发扩容操作 。


ConcurrentHashMap 的sizeCtl是负数但是不是-1的时候代表什么情况

表示当前散列表正在扩容,高16为表示扩容标识戳,第十六位表示参与扩容的线程数+1;


ConcurrentHashMap 的扩容标识戳的计算方式

需要保证每个线程计算出来的扩容标识戳是一致的,能标记出是从同一个小表到同一个大表的扩容。 首先会拿老表的size转换成二进制之后,从高位开始计数一共有多少个0 ,比如说size是16 转换成二进制之后就是 1 0000, int类型是32位可以得出高位是27个0,转换成二进制就是 11011 ,然后再把 0000 0000 0001 1011 和 1000 0000 0000 0000进行按位或运算这样计算出来的数值就是16到 32 的一个扩容标识戳了。这个扩容标识戳和需要扩容表的size是强相关的。不同size的表计算出来的扩容标识戳是不一样的


ConcurrentHashMap 如何保证写数据线程安全

slot的头结点有值的情况下,会使用synchronized 去锁住头结点,来保证slot下的执行是串行化的。

slot的头结点没值的情况下,依赖CAS去实现线程安全,线程会使用CAS的方式向slot的头结点写数据,成功的话就返回,失败的话就说明有其他的线程已经竞争到头结点的位置了, 然后会重新执行写逻辑,再次路由到slot的 头结点就已经有值了 ,然后在使用synchronized 去锁住头结点,执行写数据。


hash的寻址算法

先拿key的hashCode然后进行扰动运算,把hashcode的高16位和低16位进行异或运算,然后将符号位强行置为0,保证hash值是一个正数。 高低位异或可以让hashcode的高位也参与到寻址算法里面,具有增强散列的效果,然后table.length()-1&hash值,因为table.length()的值一定是2的n次方,所以table.length()-1的二进制一定是11111......这种值,然后再和hash值进行与运算之后结果一定是在0和table.length()之间的一个值。


ConcurrentHashMap如何统计当前散列表数据量

使用的是LongAdder 是jdk8的新特性也是doug lea写的 只不过没有引用使用而是把对应的代码拷贝过来了。


ConcurrentHashMap为什么不使用AtomicLong统计数量

atmoticLong并发安全是采用CAS实现的,CAS在并发量大的情况下,效率不是很理想,在并发场景下 100个线程同时让atomicLong自增,这时候只会有一个执行成功,CAS会先比较期望值一致才会进行替换,擦拭在内核中实际是调用 cmpxchg指令,这个指令在多核场景下会通过锁总线的形式保证只有一个CPU去执行指令 ,如果100线程同时执行自增,反映到CPU层面他其实是串行通过的,每次只会有一个线程执行成功,剩下的执行失败之后会再从内存中读取新的值,然后再去执行,最后一个线程会失败99次,对CPU资源造成很大浪费。


ConcurrentHashMap 中LongAdder如何实现在大并发下还会有不错的效率

将热点数据打散拆分到多个点执行,使冲突变小,从而提升性能,空间换时间。


ConcurrentHashMap 中LongAdder的结构。

LongAdder最核心大概两个字段,long类型的base、Cell数组,Cell里面是long类型的value,使用LongAdder的过程中,如果CAS修改base从来没有失败过,数据会全部累加到base 当某个线程与其他线程产生冲突,CAS修改base字段失败的时候,就会构建Cell数组 再往后所有的累加请求就不在操作base字段了 ,而是根据分配给线程的hash值进行一个位与运算,找到Cell数组中对应的位置,通过CAS的方式写到Cell里面。



ConcurrentHashMap触发扩容条件的线程都做了哪些事情

1、需要修改sizeCtl的值 ,将sizeCtl的高十六位的最高位设置为1 标识为一个负数,低十六位原始值是参与扩容的线程数+1,因为当前线程是触发扩容的线程所以会将低十六位设置为2

2、需要创建一个新的table大小是之前的2倍,并且需要告诉新表的引用地址到map.nextTable字段,因为后续协助扩容的线程需要知道将老表的数据迁移到哪里,然后需要设置老表的长度到map.transferIndex字段,这个字段记录老表的总的迁移进度,迁移进度是从高桶开始一直到下标是0的桶。


ConcurrentHashMap老表迁移完的桶是怎么标记的

迁移的时候会创建一个ForwardingNode对象,这个node比较特殊,用来表示这个Node已经迁移完毕


ForwardingNode还有什么功能

ForwardingNode里面还有一个指向新表的字段,并提供了一个查询方法,当我查询的时候碰到当前的node已经是ForwardingNode节点,则表示该节点已经被迁移到新的table上了 ,则会通过ForwardingNode提供的find方法重新定向到新表上查询 。


正在扩容的时候新来了一个写线程要写数据该如何处理

要分两种情况讨论,

当需要写入的桶还没有开始迁移,这种就会先拿到桶的锁执行正常的插入操作,迁移桶位的时候也会加锁所以这里是同步的不存在并发安全问题。

当写入操作访问的桶的头结点是ForwardingNode节点,说明正在扩容,写操作的线程会协助扩容操作,写入线程会根据全局transferIndex区规划当前线程的任务区间,当前线程就会将任务区间内的桶数据搬运到新的table,搬运完成之后再根据全局的transferIndex去领取下一次的搬运任务,如果没有任务可以领取到则表示扩容完成,然后开始执行写逻辑,数据就会被写入到新的table中


扩容期间,参与扩容工作的线程是如何维护sizeCtl的低16位的

每个参与扩容任务的工作线程,在开始协助扩容之前都会更新sizeCtl的低十六位, 让sizeCtl的低十六位+1表示又有新的线程进来协助扩容,然后参与扩容工作的所有线程最终都会因为分配不到新的数据搬运工作而退出扩容任务,在退出之前会将sizeCtl的低十六位的值-1


最后一个退出扩容任务的线程需要做哪些工作

当sizeCtl的低十六位-1 后的值是1 表示当前线程是最后一个退出的线程,然后他会再检查一遍老表,查看是否有遗漏的桶没有被迁移,判断的条件是桶的头节点是否是ForwardingNode节点,如果是表示已经迁移完成,如果不是就会继续做迁移工作,这一步完成之后会将新标的引用保存到map.table字段上,然后会根据新表的大小算出下一次扩容的阈值更新的sizeCtl上。


桶升级成红黑树,且当前线程有读线程访问,再来写请求会如何处理

不能写,因为写数据之后会导致红黑树失衡,会触发自动平衡操作 ,使树的整个结构发生变化,所以这是肯定不会发生的。

TreeBin对象内部有个int 类型的state字段去保证,每个线程在读数据之前都会通过CAS的方式将state的值+4,读操作完成之后会再-4,写线程在写数据到红黑树之前会先检查state字段的值,判断值是否为0 如果为0表示没有读线程,然后写线程会通过CAS的方式将state的值设置为1,设置为独占锁。

如果写线程发现当前state值不是0,说明当前有读线程进行访问,写线程会把自己线程的thread引用暴露到TreeBin的对象中并且把state值的bit位的第二位设置为1 ,表示当前有写线程正在等待,然后调用LockSupport.park()方法将自己挂起,当读线程都结束的时候会检查当前state是不是2,如果是2的话说明有写线程正在等待写入,读线程就会调用LockSupport.unPark()方法将写线程唤醒。


如果当前红黑树正在进行写操作,此时又来读请求该如何处理

TreeBin保留了红黑树和链表两个结构,当state的值为1的时候表示当前红黑树为写锁独占,此时就会去链表中读取数据并返回

发表评论
留言与评论(共有 0 条评论) “”
   
验证码:

相关文章

推荐文章