ConcurrentHashMap源码

一、ConcurrentHashMap 简介

1、ConcurrentHashMap  结构图如下:

2、众所周知,ConcurrentHashMap  是线程安全的,在JDK1.7中,ConcurrentHashMap  采用分段锁(segment)的机制实现线程安全,而在JDK1.8中,ConcurrentHashMap 抛弃了分段锁的设计,而是采用CAS+Synchronized来保证并发更新的安全,底层依然采用 数组 + 链表 + 红黑树的数据结构,数组可以扩容,链表可以转化为红黑树

二、对象属性

其中最重要的就是sizeCtl 参数,代表数组的大小(但是还有其他取值及其含义,后面再讲解),sizeCtl = 【 (1.5 * initialCapacity + 1),然后向上取最近的 2 的 n 次方】。如 initialCapacity 为 10,那么得到 sizeCtl 为 16,如果 initialCapacity 为 11,得到 sizeCtl 为 32

 

 

三、构造方法

ConcurrentHashMap 的构造方法和 HashMap 的大同小异,下面我们来简单看一下源码:

 

concurrencyLevel,能够同时更新ConccurentHashMap且不产生锁竞争的最大线程数,在Java8之前实际上就是ConcurrentHashMap中的分段锁个数,即Segment[]的数组长度。正确地估计很重要,当低估,数据结构将根据额外的竞争,从而导致线程试图写入当前锁定的段时阻塞;相反,如果高估了并发级别,你遇到过大的膨胀,由于段的不必要的数量; 这种膨胀可能会导致性能下降,由于多数缓存未命中。

      在Java8里,仅仅是为了兼容旧版本而保留。唯一的作用就是保证构造map时初始容量不小于concurrencyLevel

四、常用方法源码

ConcurrentHashMap 的使用中最常用的方法就是 put 、get 和 ConcurrentHashMap 扩容机制,下面分别介绍这三部分的源码。

1、put 方法

 

1)、如果数组还没有初始化,那么首先会进行初始化,这边会通过CAS操作将sizeCtl设置为-1,设置成功后就可以进行初始化操作,源码如下:

2)、根据key的hash值计算索引找到对应的桶,如果桶不存在,那么通过一个CAS操作来设置桶的第一个元素,失败的继续执行下面的逻辑即向桶中插入或更新,源码如下:

3)、如果桶存在,但是桶中第一个元素的hash值是-1,说明此时该桶正在进行迁移操作,这一块会在下面的扩容中详细讲解。  这是控制并发扩容的核心 ,由于给节点上了锁,只允许当前线程完成此节点的操作,处理完毕后,将对应值设为ForwardingNode(fwd),其他线程看到forward,直接向后遍历。如此便完成了多线程的复制工作,也解决了线程安全问题。

4)、获取到该桶的锁,在锁定的情况下执行链表的插入或者更新

5)、获取到该桶的锁,在锁定的情况下执行红黑树的插入或者更新

6)、在锁的保护下插入或者更新完毕后,如果是链表结构,需要判断链表中元素的数量是否超过8(默认),一旦超过就要考虑进行数组扩容或者是链表转红黑树

2、get方法

1)、找到了index索引位置的数组对象,直接返回该该对象

2)、如果元素的hash值小于0,则该节点可能为ForwardingNode或者红黑树节点TreeBin,如果是ForwardingNode(表示当前正在进行扩容),使用新的数组来进行查找,如果是红黑树节点TreeBin,使用红黑树的查找方式来进行查找

3)、如果元素的hash值大于0,则为链表结构,依次遍历链表元素找到对应的元素

3、扩容机制

一旦链表中的元素个数超过了8个,那么可以执行数组扩容或者链表转为红黑树,这里依据的策略跟HashMap依据的策略是一致的。当数组长度还未达到64个时,优先数组的扩容,否则选择链表转为红黑树。源码如下所示:

扩容操作在方法 tryPresize 中进行,源码如下:

第一个执行的线程会首先设置sizeCtl属性为一个负值,然后执行transfer(tab, null),再下一个循环将 sizeCtl 加 1,并执行 transfer(tab, nt),之后可能是继续 sizeCtl 加 1,并执行 transfer(tab, nt),实际上就是就是执行 1 次 transfer(tab, null) + 多次 transfer(tab, nt)。后来的线程会检查当前扩容是否已经完成,没完成则帮助进行扩容,完成了则直接退出。

       虽然说 tryPresize 方法中多次调用 transfer 不涉及多线程,但是这个 transfer 方法可以在其他地方被调用,比如 put 时调用了 helpTransfer 方法,helpTransfer 方法会调用 transfer 方法,这样就出现了多线程调用 transfer 的情况。

ConcurrentHashMap的扩容操作可以允许多个线程并发执行,那么就要处理好任务的分配工作。每个线程获取一部分桶的迁移任务,如果当前线程的任务完成,查看是否还有未迁移的桶,若有则继续领取任务执行,若没有则退出。在退出时需要检查是否还有其他线程在参与迁移工作,如果有则自己什么也不做直接退出,如果没有了则执行最终的收尾工作。

从上述过程分析我们有以下几个问题需要探讨:

1、当前线程怎样感知其他线程是否也在参与迁移工作?

       当前线程是根据 sizeCtl 的值来判断的,它初始值是一个负值=(rs << RESIZE_STAMP_SHIFT) + 2), 每当一个线程参与进来执行迁移工作,则该值进行CAS自增,该线程的任务执行完毕要退出时对该值进行CAS自减操作,所以当sizeCtl的值等于上述初值则说明了此时未有其他线程还在执行迁移工作,可以去执行收尾工作了。源码如下:

 

2、任务按照什么规则分片执行桶迁移工作的?

stride即是每个分片的大小,目前有最低要求16,即每个分片至少需要16个桶。stride的计算依赖于CPU的核数,如果只有1个核,那么此时就不用分片,即stride=n。其他情况就是 (n >>> 3) / NCPU

3、如何记录已经分配出去的任务?

通过属性transferIndex(初值为最后一个桶),表示从transferIndex开始到后面所有的桶的迁移任务已经被分配出去了。所以每次线程领取扩容任务时,则需要对该属性进行CAS的减操作,即transferIndex-stride。

第一个发起数据迁移的线程会将 transferIndex 指向原数组最后的位置,然后从后往前的 stride 个任务属于第一个线程,然后将 transferIndex 指向新的位置,再往前的 stride 个任务属于第二个线程,依此类推。当然,这里说的第二个线程不是真的一定指第二个线程,也可以是同一个线程。其实就是将一个大的迁移任务分为了一个个任务包。transfer 这个方法并没有实现所有的迁移任务,每次调用这个方法只实现了 transferIndex 往前 stride 个位置的迁移工作。源码如下:

 

4、每个线程如何处理分配到的部分桶的迁移工作?

第一个获取到分片的线程会创建一个新的数组,容量是之前的2倍。遍历自己所分到的桶:

1)、桶中元素不存在,则通过CAS操作设置桶中第一个元素为ForwardingNode,其Hash值为MOVED(-1)。此时若其他线程进行put操作,发现第一个元素的hash值为-1则代表正在进行扩容操作(并且表明该桶已经完成扩容操作了,可以直接在新的数组中重新进行hash和插入操作),该线程就可以直接操作新的数组了

2)、桶中元素存在且hash值为-1,则说明该桶已经被处理了

3)、桶中为链表或者红黑树结构,则需要获取桶锁,防止其他线程对该桶进行put操作,然后处理方式同HashMap的处理方式一样

5、在某个桶的迁移过程中,别的线程想要对该桶进行put操作怎么办?

一旦某个桶在迁移过程中了,必然要获取该桶的锁,所以其他线程的put操作要被阻塞,一旦迁移完毕,该桶中第一个元素就会被设置成ForwardingNode节点,所以其他线程put时需要重新判断下桶中第一个元素是否被更改了,如果被改了重新获取重新执行逻辑,源码如下:

6、某个桶已经迁移完成(其他桶还未完成),别的线程想要对该桶进行put操作怎么办?

该线程会首先检查是否还有未分配的迁移任务,如果有则先去执行迁移任务,如果没有即全部任务已经分发出去了,那么此时该线程可以直接对新的桶进行插入操作(映射到的新桶必然已经完成了迁移,所以可以放心执行操作),源码如下:

 

下面我们来看一下实现关键逻辑的 transfer 方法的源码:

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值