java 版本 jdk1.8
初始化
initialCapacity
根据自己需要在map中存放多少元素确定,默认DEFAULT_CAPACITY=16
loadFactor
扩容的负载因子,达到容量的多少比例之后开始考虑扩容,在java 8 版本基本只用于计算初始容量了concurrencyLevel
估计有多少线程并发更新,会参考这个值确定map的容量,如果concurrencyLevel
大于了initialCapacity
会以concurrency
的值替换sizeCtl
这个值才是map内部维护的值,用于控制map是否扩容,是根据以上三个值来确定的计算方式如下:cap = (long)(1.0 + (long)initialCapacity / loadFactor)
,会将cap
取大于cap的最小2的N次幂,即cap=10
最后sizeCtl =16
(242^424),不过这个值会在内部维护的Node数组初始化完成之后或者扩容完成之后被重置sizeCtl = n-(n>>2)
。table
主要数据保存在这个Node数组里面,创建一个sizeCtl
大小的Node数组nextTable
扩容之后的Node数组,当且仅当在扩容阶段的时候不为空
内部数据结构
Node
数据节点,table
字段就是它的一个数组。内部维护next字段,当有多个元素的hash相等时,就形成链表TreeNode
继承Node,树形节点,在一定情况下,Node链表会转换为红黑树。内部有一个red
字段表明这个节点事红节点还是黑节点TreeBin
继承Node,红黑树的根节点。不保存实际数据,hash=-2
ForwadinNode
迁移标记节点。标记此节点下的所有元素已经迁移到新的table中,内部保存了对扩容之后的Node数组的引用
put
ConcurrentHashMap主要是用于并发读写的,所以,在插入方法中考虑了多线程并发操作
- 计算key的hash,和HashMap一样都是将
hashCode()
的高16位与低16位做异或运算,以减小hash碰撞。有一点不一样的是,这里会将异或运算的结果再和0x7fffffff
做与运算,把hash控制在0~231之间 - 插入
- 新元素index (
hash & (n-1)
,n为当前table的长度)对应位置为空,则使用CAS更新,若此时对该位置有多个线程并发插入,肯定只有一个CAS操作会成功,其余的都会失败,失败的元素会不断的重试直到成功为止,不过当再一次重试时,对应位置已然不是空,所以会走下面的流程 - 当新元素index的位置已经存在元素了,那么先对已存在的这个元素身上添加一个监视器(锁),防止并发对这个节点进行更新,但是这里不用太担心锁对性能影响,因为按照初始容量来算,也只有1/16的机会你会遇到这个锁,扩容之后几率更小。将新的元素追加到这个元素后面,形成链表分支
- 如果新元素index位置存在的是
TreeBin
那么表明这个节点下已经形成了红黑树,按照红黑树的方式将新的节点插入就可以了,同样在插入之前要锁住index位置那个节点,防止并发
- 新元素index (
- 记录并检查数量。map中的元素个数是保存在
baseCount
字段中,有新的元素加入时,使用CAS 来更改baseCount
的值,在多线程环境下,可能有多个线程在同时更改,造成失败。此时并没有选择重试,而是使用在内部的一个CounterCell
数组来保存在CAS失败的计数,所以,当我们调用size
方法时,其实得到的是baseCount + CounterCell[i].value
得到的 - 迁移。
ConcurrentHashMap
的扩容采用多线程的方式进行数据迁移,在扩容阶段,所有对map元素个数产生影响的线程,都可以参与到扩容流程中。在扩容阶段,把整个Node数组分成N个stride
,每个线程负责一个stride
,迁移完成一个stride
再去看是否还有需要迁移的stride
,直到所有stride
迁移完成