文章目录
一、概述
关联文章:
- HashMap(一) — HashMap 源码分析
- HashMap(二) — 浅析hash函数及tableSizeFor函数
- HashMap(三) — 高并发场景下的问题分析
- 可重入锁(ReentrantLock)源码解析
- ConcurrentHashMap 源码分析(JDK1.7)
1.1 背景
由 HashMap(三) — 高并发场景下的问题分析 我们知道,在高并发下 HashMap是线程不安全的,主要原因是在同一个桶中并发操作会带来问题,为此我们只需要解决并发操作同一个桶的安全性问题即可,因此在SDK提供的ConcurrentHashMap类中通过分段锁(JDK1.7)或CAS+Sync(JDK1.8)来保证操作的安全性。
1.2 数据结构
- JDK 1.7 HashMap 是通过 数组+链表来实现的,而ConcurrenthashMap在此基础上使用了分段锁来实现,即最终的数据结构为 Segment数组 + HashEntry数组 + HashEntry链表。ConcurrentHashMap 源码分析(JDK1.7)
- JDK 1.8 HashMap 是通过 数组+链表+红黑树来实现的,而ConcurrenthashMap在此基础上放弃了原有的分段锁机制,采用CAS+Sync同步块的方案来实现。
ConcurrenthashMap 的数据结构如下图所示:
1.3 预备知识
下面关于 Unsafe 和 Integer 类中几个方法的含义,请参考ConcurrentHashMap 源码分析(JDK1.7)。
Unsafe.arrayBaseOffset(Class<?> var)
&Unsafe.arrayIndexScale(Class<?> var1)
Integer.numberOfLeadingZeros(int i)
&31 - Integer.numberOfLeadingZeros(int)
二、源码分析
JDK1.8版本中,我们主要分析如下几个方法:
- 静态代码块
- 构造方法
ConcurrentHashMap.put(K key, V value)
:存数据ConcurrentHashMap.get(Object key)
:取数据ConcurrentHashMap.addCount(long x, int check)
:计数+扩容ConcurrentHashMap.helpTransfer(Node<K,V>[] tab, Node<K,V> f)
:数据迁移
2.1 静态代码块
通过 Unsafe 类获取数组相关信息。
static {
try {
U = sun.misc.Unsafe.getUnsafe();
// ...
Class<?> ak = Node[].class;
// 计算出Node[]第一个元素的偏移地址
ABASE = U.arrayBaseOffset(ak);
// 计算出Node[]中每个元素的大小
int scale = U.arrayIndexScale(ak);
// 校验两个数组元素占用内存大小是否是2^n。
if ((scale & (scale - 1)) != 0)
throw new Error("data type scale not a power of two");
// 计算位运算时的偏移量
ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);
} catch (Exception e) {
throw new Error(e);
}
}
2.2 构造方法
// volatile修饰,保证Node[]扩容时,其它线程可见。
transient volatile Node<K,V>[] table;
private transient volatile Node<K,V>[] nextTable;
private transient volatile long baseCount;
private transient volatile int sizeCtl;
public ConcurrentHashMap() {}
// 与JDK1.7不同,构造中没有初始化容器。
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (initialCapacity < concurrencyLevel) // Use at least as many bins
initialCapacity = concurrencyLevel; // as estimated threads
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
int cap = (size >= (long)MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY : tableSizeFor((int)size);
this.sizeCtl = cap;
}
// ConcurrentHashMap.Node.class
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val; // volatile 保证节点数据的可见性
volatile Node<K,V> next; // volatile 保证下个节点的可见性
}
小结:
- 与JDK1.7的ConcurrentHashMap不同,构造中没有初始化容器。
volatile
修饰 Node<K,V>[],保证当前线程创建数组后可以被其它线程看到。- JDK1.8的ConcurrentHashMap与HashMap类似。
2.3 ConcurrentHashMap.put(K key, V value)
// ConcurrentHashMap.class
public V put(K key, V value) {
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
// 1.hash计算
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 2.Node[]不存在,则初始化Node数组,并进入下次for循环。
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 3.tab[i]处的头节点为null时,则直接通过CAS操作添加Node。
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
// 表示当前节点正在迁移
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
// 都不满足时,synchronized 锁住f节点(tab[index]头节点)
synchronized (f) {
if (tabAt(tab, i) == f) {
// 头节点为链表Node节点。
if (fh >= 0) {
binCount = 1;
// 遍历查找。找到了就更新value,没找到就在链表末尾新增Node。
for (Node<K,V> e = f;; ++binCount) {
K ek;
// 匹配到Node,则更新值
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
// 没有匹配到Node,则新增Node
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
// 头节点为红黑树
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
// 在红黑树中查找。找到了就更新值,没找到就新增TreeBin节点。
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
// 遍历链表时binCount会自增,所以当链表长度>=8时,需要转为红黑树。
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
// size计数(与JDK1.7的方案不同)
addCount(1L, binCount);
return null;
}
put操作流程:
- step1:计算 hash 值,并开始遍历Table。
- step2:当 Table 未初始化时,则初始化Table并通过 CAS 赋值。初始化结束后则进入下次for循环。
- step3:当 Table 初始化时,获取 Table[i] 头节点,并判断是否为Null。
- step4:当第3步 Table[i] == NULL 成立时,则直接新建一个Node并添加到 Table[i] 的位置,并执行step8。
- step5:当第3步 Table[i] = NULL 不成立时,则继续判断 Table[i] 头节点的hash值是否为 MOVE(正在迁移数据)。如果为MOVE,则本次for循环执行数据迁移任务。
- step6:当Table[i] 头节点的hash值不为 MOVE时,开始加锁并执行添加逻辑 (按照链表/红黑树)。
- step7:判断 Table[i] 头节点类型 (链表/红黑树)。
- 链表:优先查询,如果没有匹配到则在链表末尾新增节点添加数据。
- 红黑树:优先查询,如果没有匹配到则新增红黑树节点。
- step8:最后执行计数和扩容判断逻辑。如果存在扩容,则完成数据迁移。
put操作流程图:
2.4 ConcurrentHashMap.get(Object key)
// hash为负值的几种可能性。
static final int MOVED = -1; // hash for forwarding nodes
static final int TREEBIN = -2; // hash for roots of trees
static final int RESERVED = -3; // hash for transient reservations
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
// hash计算
int h = spread(key.hashCode());
// tabAt(tab, (n - 1) & h)计算在table中的位置,并获得头节点。
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
// hash值相同,则进一步判断key是否相同,key相同则直接返回。
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
//hash值为负值表示正在扩容,这个时候查的是ForwardingNode的find方法来定位到nextTable来
//eh=-1,说明该节点是一个ForwardingNode,正在迁移,此时调用ForwardingNode的find方法去nextTable里找。
//eh=-2,说明该节点是一个TreeBin,此时调用TreeBin的find方法遍历红黑树,由于红黑树有可能正在旋转变色,所以find里会有读写锁。
//eh>=0,说明该节点下挂的是一个链表,直接遍历该链表即可。
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
// hash值相同且key不同,则遍历链表继续查找。
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
2.5 ConcurrentHashMap.addCount(long x, int check)
主要功能: 计数+数据扩容
参考:ConcurrentHashMap addCount分析
2.6 ConcurrentHashMap.helpTransfer(Node<K,V>[] tab, Node<K,V> f)
主要功能: 数据迁移
参考:ConcurrentHashMap transfer分析
三、小结
- JDK1.8的 ConcurrentHashMap 摒弃了JDK1.7的 Segment 分段锁方案,采用
Node+CAS+Synchronied
的方案保证数据的安全操作。 - JDK1.8的
ConcurrentHashMap.put()
流程与JDK1.8的HashMap.put()
流程比较接近,在 HashMap 的基础上增加了并发迁移逻辑和同步代码块(Synchronied
)。 - JDK1.7的 ConcurrentHashMap 需要两次hash计算,而JDK1.8版本只需要计算一次。
- JDK1.7的 Segment 分段锁不能扩容,而JDK1.8的 Node[] 可以实现扩容,且操作粒度更细(Node[]中每一个头节点都可以成为一个锁)。
- JDK1.8 Size的计算也与 JDK1.7 计算方式不同。
- JDK1.8 链表新增Node时采用尾插法,而 JDK1.7 链表新增Node时采用头插法 。
- 缺点:扩容(数据迁移)和size的计算方式太复杂。