跳转至

ConcurrentHashMap:CAS 加锁写入

一、put 流程

public V put(K key, V value) {
    return putVal(key, value, false);
}

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 直接插入,不用锁:

casTabAt(tab, i, null, new Node<>(...));

因为没有竞争,CAS 成功概率高。

2. 桶非空

锁桶的头节点:

synchronized (f) {
    // f 是这个桶的第一个节点
}

锁粒度是一个桶,不是整个表。不同桶的 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 桶级锁,并发度高。