ConcurrentHashMap:CAS 加锁写入¶
一、put 流程¶
putVal 核心¶
final V putVal(K key, V value, boolean onlyIfAbsent) {
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(); // 1. 初始化
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 2. 桶空,CAS 插入
if (casTabAt(tab, i, null, new Node<>(hash, key, value, null)))
break;
} else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f); // 3. 扩容中,帮忙
else {
// 4. 桶非空,synchronized 锁头节点
V oldVal;
synchronized (f) {
if (tabAt(tab, i) == f) {
// 链表/红黑树插入
}
}
}
}
return null;
}
二、三种情况¶
1. 桶空¶
用 CAS 直接插入,不用锁:
因为没有竞争,CAS 成功概率高。
2. 桶非空¶
锁桶的头节点:
锁粒度是一个桶,不是整个表。不同桶的 put 不互斥。
3. 正在扩容¶
遇到 hash = MOVED (-1) 的节点,说明在扩容,当前线程帮忙迁移数据。
三、为什么不用分段锁¶
JDK 7 的 Segment 是 16 段,最多 16 个线程并发写。JDK 8 每个桶一把锁,并发度 = 桶数(16、32、64...)。
四、读不加锁¶
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 && ...) {
e = tabAt(tab, (n - 1) & h); // volatile 读
// 遍历链表/树
}
}
val 和 next 都是 volatile,读不加锁,直接读最新值。
对比
- JDK 7:ReentrantLock 分段,16 段。
- JDK 8:CAS + synchronized 桶级锁,并发度高。