ConcurrentHashMap 是 Java 并发包 (java.util.concurrent) 中提供的一个线程安全的哈希表实现。它通过多种并发控制机制来实现高效的线程安全操作。以下从源码角度分析 ConcurrentHashMap 如何实现线程安全。
ConcurrentHashMap 的底层数据结构是一个 数组 + 链表 + 红黑树 的组合。与 HashMap 类似,但在并发控制上做了优化。
核心数据结构:
Node
TreeNode
Table:一个 volatile 修饰的 Node
transient volatile Node<K,V>[] table;
ConcurrentHashMap 通过以下几种机制实现线程安全:
在 JDK 1.7 中,ConcurrentHashMap 使用分段锁(Segment)来实现线程安全。它将整个哈希表分成多个段(Segment),每个段独立加锁,不同段之间可以并发操作。
Segment:继承自 ReentrantLock,每个段是一个独立的哈希表。
锁粒度:锁的粒度从整个表缩小到段,提高了并发性能。
在 JDK 1.8 中,ConcurrentHashMap 抛弃了分段锁,改为使用 CAS(Compare-And-Swap) 和 synchronized 来实现线程安全。
CAS:用于无锁化的更新操作(如插入、替换)。
synchronized:用于锁定单个桶(链表或红黑树的头节点)。
以下从 JDK 1.8 的源码角度分析 ConcurrentHashMap 的线程安全实现。
插入操作的核心逻辑在 putVal 方法中。
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode()); // 计算哈希值
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable(); // 初始化表
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 如果桶为空,使用 CAS 插入新节点,如果这里失败就不会break,会循环一次,下一次就无法进入该分支
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
break; // 插入成功
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f); // 协助扩容
else {
V oldVal = null;
synchronized (f) { // 锁定桶的头节点
//这里比较关键,他只锁了一个节点的对象,如果你并发操作其他对象,理论上也可以获取到锁
if (tabAt(tab, i) == f) {
if (fh >= 0) { // 链表
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
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;
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;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i); // 链表转红黑树
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount); // 更新元素计数
return null;
}
CAS 插入:如果桶为空,使用 casTabAt 方法通过 CAS 插入新节点。
锁桶头节点:如果桶不为空,使用 synchronized 锁定桶的头节点,然后遍历链表或红黑树进行插入。
读取操作是无锁的,直接通过 volatile 关键字保证可见性。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
//其他代码
}
transient volatile Node<K,V>[] table;
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode()); // 计算哈希值
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) { // 获取桶的头节点
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val; // 直接返回
}
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null; // 红黑树查找
while ((e = e.next) != null) { // 链表查找
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
volatile 保证可见性:table 和 Node 的 val 字段都是 volatile 的,确保读取操作能够看到最新的值。
ConcurrentHashMap 的扩容是通过多线程协作完成的。
transfer 方法:将旧表的元素迁移到新表。
helpTransfer 方法:其他线程协助扩容。
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
Node<K,V>[] nextTab; int sc;
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
int rs = resizeStamp(tab.length);
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab);
break;
}
}
return nextTab;
}
return table;
}
ConcurrentHashMap 通过以下机制实现线程安全:
CAS:用于无锁化的更新操作。
synchronized:锁定单个桶的头节点,减少锁粒度。
volatile:保证变量的可见性。
多线程协作扩容:提高扩容效率。
相比于 Hashtable 和 Collections.synchronizedMap,ConcurrentHashMap 在并发性能上有显著优势,适合高并发场景。