ConcurrentHashMap逻辑整理(一)

ConcurrentHashMap数组全局唯一,采用懒加载模式初始化。
一般项目中是用ConcurrentHashMap,都是作为JVM缓存使用的
key和value都不能为null

put操作:

  1. 将key进行hash运算,计算出hash值,然后基于这个hash值确定数据存放的索引位置
  2. 查看COncurrentHashMap的数组是否已经初始化,(数组全局唯一,懒加载,DCL保证线程安全)
    – 初始化就继续尝试存放数据
    – 没初始化,先尝试将数组new出来
  3. 基于hash值,确定数据存放位置
    – 如果数组索引位置没有数据,将当前数据扔进去。有限将key-value封装为一个Node对象,这里为了保证线程安全,直接采用CAS的方式将当前索引位置的null替换为指定的Node数据
    如果CAS成功,循环结束
    如果CAS失败,重走循环,从第2步开始
  4. 如果数组索引位置有数据,查看当前数组的状态是否是扩容的情况 MOVED(hash=-1)
    – 如果正在扩容,单签线程去帮助扩容,加快扩容速度,协助扩容结束后,重新走循环
  5. 如果数组索引位置有数据,且没有在扩容影响到当前线程
    – 加锁,基于数组索引位置的Node对象作为synchronized的锁
    – 循环将数据挂到链表的末端
    数据添加到链表后,查看链表长度是否达到了8,达到了8个,就需要尝试链表转红黑树(ConcurrentHashMap要求必须数组长度 >= 64 且 链表长度达到8才会转换,如果数组长度未达到64,会有限扩容数组)
    – 将数据添加到红黑树结构中 TREEBIN(hash=-2)
  6. 循环结束
  7. 需要记录元素个数,为了保证线程安全,这里采用LongAdder做计数器。(LongAdder比ActomicLong的效率高)
  8. 添加结束

扩容流程&协助扩容

ConcurrentHashMap扩容只有两件事:

  • 构建新数组,长度是老数组长度的2倍(new)
  • 将老数组中的数据迁移到新数组中

有一个属性可以区分扩容线程和协助扩容线程

private transient volatile int sizeCtl;

第一个来扩容的线程,会优先修改sizeCtl的值,将其修改为一个小于-1的值。
其他所有来协助扩容的线程,发现sizeCtl小于-1,直接对sizeCtl做 +1 操作,代表我来扩容了。修改sizeCtl的操作是基于CAS的。

  • 扩容的线程是来初始化新数组的,并且做迁移数据的操作。
  • 协助扩容的线程是来帮忙进行数据迁移的,不会做数组初始化。

整体扩容流程

  1. 计算每次迁移多长索引位置的数据到新数组(步长,将老数组数据分成n段,每次迁移一段),会根据CPU内核数和数组长度来计算,最小是16。
  2. 会由扩容线程。来初始化新数组,长度是原来的2倍。
  3. 扩容线程和协助扩容线程开始领取迁移任务。领取到任务的准备干活,没领取到任务的可以退出扩容。
  4. 没领取到任务的线程可以退出扩容,但是退出前,查看下扩容结束了没
    结束了,说明当前线程是最后一个退出扩容的线程,整体再检查一遍。
    没结束,sizeCtl-1,退出
  5. 迁移数据操作,在迁移任务中某一个索引位置是,会由不同情况
    当前位置没有数据,直接扔一个MOVED(hash = -1)
    当前索引位置是moved,啥也不做。
    当前位置有数据,锁住当前位置
    链表迁移到新数组,
    红黑树迁移到新数组(红黑树保留一个双向链表,利用双向链表迁移数据)

查询

ConcurrentHashMap本来就是作为JVM缓存使用的,对查询速度要求极高,所以查询永远不会泽色,无论什么情况,都能去查询数据。

查询数据的操作有以下几种情况:

  • 数据在数组上,查询到即返回
  • 数据在链表上,next遍历查找,找到返回
  • 数据在红黑树上
    • 如果此时有写线程在操作红黑树或者等待操作红黑树,那么读线程会去查询保留的双向链表
    • 如果此时没有写线程在操作或等待操作红黑树,直接去红黑树中找数据。如果此时有写线程想操作红黑树,那么需要等到读线程完毕后,才可以操作。
  • 如果数组上的Node是MOVED状态,代表数据已经迁移到新数组上了,直接去新数组上去查询数据
  • 如果数组上的Node是REVERSED状态,代表当前位置被占了,但是value还在计算中,返回null

红黑树是一个平衡的二叉树,怎么保持平衡的?
为了保证平衡,写操作可能会旋转某个节点,导致节点的指针发生变化。如果此时读线程在红黑树中遍历找数据,结果写线程改变了红黑树的指针,导致无法找到对应的数据。

有一个属性标识该树节点正在被读或者被写入

volatile int lockState;
// values for lockState
static final int WRITER = 1; // set while holding write lock
static final int WAITER = 2; // set when waiting for write lock
static final int READER = 4; // increment value for setting read lock,每次加4,>= 4 就是正在被读取
  • 24
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值