ConcurrenHashMap
。下面分享一下我对ConcurrentHashMap
的理解,主要用于个人备忘。如果有不对,请批评。
HashMap
“严重”的勾起了我对HashMap
家族的好奇心,下面分享一下我对ConcurrentHashMap
的理解,主要用于个人备忘。如果有不对,请批评。
想要解锁更多新姿势?请访问https://tengshe789.github.io/
总起
HashMap
是我们平时开发过程中用的比较多的集合,但它是非线程安全的,在涉及到多线程并发的情况,进行get操作有可能会引起死循环,导致CPU利用率接近100%。
因此需要支持线程安全的并发容器 ConcurrentHashMap
。
数据结构
重要成员变量
1 | /** |
table
代表整个哈希表。 默认为null,初始化发生在第一次插入操作,默认大小为16的数组,用来存储Node节点数据,扩容时大小总是2的幂次方。
1 | /** |
nextTable
是一个连接表,用于哈希表扩容,默认为null,扩容时新生成的数组,其大小为原数组的两倍。
1 | /** |
baseCount
保存着整个哈希表中存储的所有的结点的个数总和,有点类似于 HashMap 的 size 属性。 这个数通过CAS算法更新
1 | /** |
初始化哈希表和扩容 rehash 的过程,都需要依赖sizeCtl
。该属性有以下几种取值:
- 0:默认值
- -1:代表哈希表正在进行初始化
- 大于0:相当于 HashMap 中的 threshold,表示阈值
- 小于-1:代表有多个线程正在进行扩容。(譬如:-N 表示有N-1个线程正在进行扩容操作 )
构造方法
1 | public ConcurrentHashMap() { |
构造方法是三个。重点是第二个,带参的构造方法。这个带参的构造方法会调用tableSizeFor()
方法,确保table的大小总是2的幂次方(假设参数为100,最终会调整成256)。算法如下:
1 | /** |
PUT()方法
put()
调用putVal()
方法,让我们看看:
1 | final V putVal(K key, V value, boolean onlyIfAbsent) { |
其实putVal()也多多少少掉用了其他方法,让我们继续探究一下。
CAS(compare and swap)
科普compare and swap,解决多线程并行情况下使用锁造成性能损耗的一种机制,CAS操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在CAS指令之前返回该位置的值。CAS有效地说明了“我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。
spread
首先,第四行出现的int hash = spread(key.hashCode());
这是传统的计算hash的方法。key的hash值高16位不变,低16位与高16位异或作为key的最终hash值。(h >>> 16,表示无符号右移16位,高位补0,任何数跟0异或都是其本身,因此key的hash值高16位不变。)
1 | static final int spread(int h) { |
initTable
第十行, tab = initTable();
这个方法的亮点是,可以让put并发执行,实现table只初始化一次 。
initTable()核心思想就是,只允许一个线程对表进行初始化,如果有其他线程进来了,那么会让其他线程交出 CPU 等待下次系统调度。这样,保证了表同时只会被一个线程初始化。
1 | private final Node<K,V>[] initTable() { |
接下来,第19行。 tab = helpTransfer(tab, f);
这句话。要了解这个,首先需要知道ForwardingNode
这个节点类型。它一个用于连接两个table
的节点类。它包含一个nextTable
指针,用于指向下一张hash表。而且这个节点的key、value、next指针全部为null,它的hash值为MOVED(static final int MOVED
= -1)。
1 | static final class ForwardingNode<K,V> extends Node<K,V> { |
helpTransfer
在扩容操作中,我们需要对每个桶中的结点进行分离和转移。如果某个桶结点中所有节点都已经迁移完成了(已经被转移到新表 nextTable 中了),那么会在原 table 表的该位置挂上一个 ForwardingNode 结点,说明此桶已经完成迁移。
helpTransfer
什么作用呢?是检测到当前哈希表正在扩容,然后让当前线程去协助扩容 !
1 | final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) { |
helpTransfer
精髓的是可以调用多个工作线程一起帮助进行扩容,这样的效率就会更高,而不是只有检查到要扩容的那个线程进行扩容操作,其他线程就要等待扩容操作完成才能工作。
transfer
既然这里涉及到扩容的操作,我们也一起来看看扩容方法transfer()
:
1 | private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) { |
至此,put方法讲完了
参考资料~
感谢
结束
此片完了~ 想要了解更多精彩新姿势?
请访问我的个人博客 本篇为原创内容,已在个人博客率先发表,随后CSDN,segmentfault,掘金,简书,开源中国同步发出。如有雷同,缘分呢兄弟。赶快加个好友~