ConcurrentHashMap使用CAS操作和synchronized关键字,保证了并发安全,它不允许任何记录的键或者值为null。
此外,虽然ConcurrentHashMap类名中带有HashMap字样,但它并不是HashMap的子类。
在JDK 8-11中,ConcurrentHashMap::tryPresize方法存在bug(JDK-8214427),该bug在JDK 12中已被修复。此外,JDK12对ConcurrentHashMap进行了部分调整,因此,本文将基于JDK 12来进行分析。
存储结构
ConcurrentHashMap的存储结构与HashMap基本相同,都是数组+单链表+红黑树。
数组
与HashMap相比,数组table使用了volatile关键字。
1 | transient volatile Node<K,V>[] table; |
单链表
与HashMap相比,val和next字段均使用了volatile关键字。
1 | static class Node<K,V> implements Map.Entry<K,V> { |
红黑树
1 | static final class TreeNode<K,V> extends Node<K,V> { |
辅助结构
- 特殊hash值
1 | static final int MOVED = -1; // hash for forwarding nodes |
- 用于扩容
1 | // 当执行扩容操作时,插在桶的头部,hash值为-1 |
- 用于红黑树
1 | // 当桶中为红黑树时,插在桶的根部,hash值为-2 |
- 占位:用于序列化和
1 | // hash值为-3 |
构造方法
1 | public ConcurrentHashMap(int initialCapacity, |
插入操作
put方法
1 | public V put(K key, V value) { |
实际执行插入操作的是putVal方法。该方法内部使用了CAS操作和同步代码块。
1 | // onlyIfAbsent: 若为true,则只有当key不存在时,才会执行插入。 |
一、循环执行如下过程:
1.若哈希表尚未初始化,则先进行初始化操作;否则,执行2。
2.计算当前项所在的桶下标。若桶为空,则使用CAS操作,将当前项设置为桶中的第一个元素;否则,执行3。
3.若哈希表正在执行迁移操作,则让当前线程帮助执行该过程;否则,执行4。
4.若当前项为桶中的第一个结点,并且不允许执行更新操作,则循环结束;否则,执行5。
5.锁住桶中的第一个结点,然后根据桶中第一个结点的hash值判断桶中的数据结构类型。若为单链表(hash >= 0),则从前往后寻找当前项的插入(或者所在)位置,执行7;否则,执行6。
6.若桶中为红黑树,则根据二叉查找树的性质,寻找当前项的插入(或者所在)位置,执行7。
7.若桶中的元素>=树化阈值(8),则尝试将单链表转变为红黑树。
二、更新哈希表中的元素个数。
重散列
1 | // 返回的值为非负 |
初始化操作
若表为空(延迟初始化)
1 | // 当sizeCtl为负数时,表示哈希表正在被初始化或者正在扩容:-1表示初始化;否则,为-(1+正在扩容的线程数量); |
CAS操作
- 什么是CAS操作
CAS(Compare And Swap,比较并交换)是一种无锁算法。CAS操作包含有3个操作数:内存位置V,预期原值E,新值N。当且仅当内存位置V符合预期值E时,才会用N更新V;否则,说明已经有其他线程做了更新,则当前线程什么都不做。
- 当桶为空时,putVal方法使用CAS操作设置桶中的第一个结点,该操作由sun.misc.Unsafe类实现。
1 | private static final sun.misc.Unsafe U; |
单链表与红黑树的相互转化
单链表=>红黑树
1 | private final void treeifyBin(Node<K,V>[] tab, int index) { |
红黑树=>单链表
1 | static <K,V> Node<K,V> untreeify(Node<K,V> b) { |
按键取值
由于Node的val和next属性均使用了volatile关键字修饰,因此,get方法没有加锁。
1 | public V get(Object key) { |
删除操作
1 | public V remove(Object key) { |
实际执行删除操作的是replaceNode方法。
1 | // value表示待设置的新值,若为null,表示删除该记录;cv表示key原来的值 |