由于HashMap是非线程安全的,扩容时容易造成环形链表,导致在获取链表中不存在的值时陷入死循环。于是在HashMap在并发环境中需要加锁使用,这种方式开销大。Java中提供了另一种数据结构ConcurrentHashMap,专门用于并发环境下的Map映射需求。
具体机制:CAS 结合 synchronized,并通过sun.misc.Unsafe提供的可见性操作接口来访问数据。
主要关注以下几个地方:
- put 放数据
- resize 扩容
- get 取数据
1、对于 put 操作
key值求hash后所在table中索引位置无节点(null),通过CAS方式将节点插入到该索引位置
key值求hash后所在table中索引位置有节点(不为null),通过synchronized锁住该索引位置处链表或者红黑树的第一个节点
2、对于 resize 扩容操作
多线程都能参与扩容操作,多线程同步怎么做的?
1)sizeCtl :该标签用于保证resize调整不会重叠,在初始化和resize扩容的时候,sizeCtl负值。其中 -1:表示table正在初始化 -(1+活跃的扩容线程数):表示正在扩容。其它也有默认值、初始化表时的表的size值、或者下一次扩容阈值。
每个线程在帮助扩容时,都会通过CAS来对sizeCtl进行+1操作,表示增加了一个线程帮助扩容
2)ForwardingNode:标记节点,用来进行并发控制,对旧table中标记为ForwardingNode的节点不再处理,避免重复。
3)transferIndex:扩容时旧table中下一个需要转移的一个长度单位的起始下标,不管哪个线程一次任务中就处理 MIN_TRANSFER_STRIDE 这么多个。
先来看看怎么确定某个元素put后到达table中的下标位置
i = (n - 1) & hash
n 表示 table 长度,hash 表示插入元素的哈希值,这么做是因为 n 是 2 的幂,n-1 其二进制表示中各位均为1。
(n - 1) & hash 与 hash % n 取余的效果相同。这样便将 hash 值分布到 table 中。
transfer中有一段代码不好理解,这里是将旧表中的元素移到新表中去
if (fh >= 0) {
// fh & (n - 1) 决定了f节点在table中的下标,因为n是2的幂,
// 所以 fh & n 只有两个结果,要么是 0,要么是 n
// 所以这里是为了将该链表拆分成两个部分,结果为0的为一部分,为n的为另一部分
// 扩容之后数组长度增加一倍,数组元素要重新分布
// 最好的情况当然是原长度部分和扩容长度部分的元素各一半,并且尽多保持原来的顺序
int runBit = fh & n;
Node