1.8的ConcurrentHashMap摒弃了1.7的Segment设计,而是在1.8HashMap的基础上实现了线程安全的版本,即也是采用的数组+链表+红黑树的形式。
数组可以扩容,链表可以转化为红黑树。
有一个重要的参数sizeCtl,代表数组的大小(但是还有其他权值及其含义,后面详细讲)
用户可以设置一个初始容量initialCapacity给ConcurrentHashMap,sizeCtl=大于(1.5倍initialCapacity+1)的最小的2的幂次。如initialCapacity=20,则sizeCtl=32,如initialCapacity=24,则sizeCtl=64。
初始化的时候,会按照sizeCtl的大小创建出对应大小的数组。
put过程
1、如果数组还未初始化,那么进行初始化,这里会进行初始化,这里会通过一个CAS操作将sizeCtl设置为-1,设置成功的,可以进行初始化操作;
2、根据key的hash值找到对应的桶,如果桶还不存在,那么通过一个CAS操作来设置桶的第一个元素,失败的继续执行下面的逻辑,即向桶中插入或更新;
3、如果找到的桶存在,但是桶的第一元素的hash值是-1,说明此时桶正在进行迁移操作,这一块会在下面的扩容中详细谈及;
4、如果找到的桶存在,那么要么是链表结构,要么是红黑树结构,此时需要获取该桶的锁,在锁定的情况下执行链表或红黑树的插入或更新。
如果桶中的第一个元素的hash值大于0,说明是链表结构,则对链表插入或者更新;
如果桶中的第一个元素是TreeBin,说明是红黑树结构,则按照红黑树的方式进行插入或者更新。
在锁的保护下,插入或者更新完毕后,如果是链表结构,需要判断链表中元素的数量是否超过8(默认),一旦超过,就需要考虑进行数组扩容,或者是链表转红黑树。
扩容过程
一旦链表中的元素个数超过8个,那么可以执行数组扩容或者是链表转为红黑树,这里依据的策略跟HashMap依据的策略是一致的。
当数组长度还未达到64个时,优先数组的扩容,否则选择链表转为红黑树。
扩容还是采用2倍扩容的方式。
第一个执行的线程会首先设置sizeCtl属性为一个负值,然后执行transfer(tab,null),其他晚进来的线程会检查当前扩容是否已经完成,没完成则帮助其进行扩容,完成了则直接退出。
该ConcurrentHashMap的扩容操作可以允许多个线程并发进行,那么就要处理好任务的分配工作。每个线程获取一部分桶的迁移任务,如果当前线程的任务完成,查看是否还有未迁移的桶,若有则继续领取任务执行,若没有则退出。在退出时需要检查是否还有其他线程在参与迁移工作,如果有则自己什么也不做直接退出,如果没有了则执行最后的收尾工作。
问题1:当前线程如何感知其他线程也在参与迁移工作?
靠sizeCtl的值,它初始值是一个负值=(rs<
问题2:任务按照何规则进行分片?
上述stride表示每个分片的大小,目前有最低要求16,即每个分片至少有16个桶。stride的计算依赖于CPU的核数,如果只有1个核,那么此时就不用分片了,即stride=n/其他情况就是(n>>>3)/NCPU。
问题3:如何记录已经分出去的任务?
ConcurrentHashMap含有一个属性trnasferIndex(初值为最后一个桶),表示从transferIndex开始到后面所有的桶的迁移任务已经被分配出去了。所以每次线程领取扩容任务,则需要对该属性进行CAS的减操作,即一般是transferIndex-stride。
问题4:每个线程如何处理分到的部分桶的迁移工作?
1、第一个获取到分片的线程会创建一个新的数组,容量是之前的2倍。
2、遍历自己所分到的桶:
(1)桶中元素不存在,则通过CAS操作设置桶中的第一个元素为ForwardingNode,其hash值为MOVED(-1),同时该元素含有新的数组引用;
(2)此时若其他线程进行put操作,发现第一个元素的hash值为-1则代表正在进行扩容操作(并且表明该桶已经完成扩容操作了,可以直接在新的数组中重新进行hash和插入操作),该线程就可以去参与进去,或者没有任务则不用参与,此时可以去直接操作新的数组了;
(3)桶中元素存在且hash值为-1,则说明该桶已经被处理了(本不会出现多个线程重叠的情况,这里主要是该线程在执行完所有的任务后再次进行检查,再次核对)
(4)桶中为链表或者红黑树结构,则需要获取桶锁,防止其他线程对该桶进行put操作,然后处理方式同HashMap的处理方式一样,对桶中元素分为2类,分别代表当前桶中和要迁移到新桶中的元素。设置完毕后代表桶迁移工作已经完成,旧数组中该桶可以设成ForwardingNode了。
get过程
1、根据key计算出hash值,找到对应的数组index;
2、如果该index位置无元素则直接返回null;
3、如果该元素有元素:
(1)如果第一个元素的hash值小于0,则该节点可能为ForwardingNode或者红黑树节点TreeBin;
(2)如果是ForwardingNode(表示当前正在扩容),使用新的数组来进行查找;
(3)如果是红黑树节点TreeBin,使用红黑树的查找方式来进行查找;
(4)如果第一个元素的hash值大于等于0,则为链表结构,依次遍历即可找到对应的元素。