ConcurrentHashMap JDK 8 原理

面试官问:"ConcurrentHashMap 在 JDK 8 是怎么实现线程安全的?"

候选人小余答:"JDK 8 用了 CAS 和 synchronized。"

面试官追问:"CAS 用在哪里?synchronized 用在哪里?"

小余说:"put 的时候用...具体在哪我说不清楚。"

面试官又问:"JDK 7 和 JDK 8 最大的区别是什么?"

小余答不上来。

【面试官心理】 这道题考查的是候选人对 ConcurrentHashMap 演进的理解。能说出"JDK 7 分段锁"和"JDK 8 无锁化"差异的候选人,说明对并发编程有深入研究。

一、JDK 7 vs JDK 8 对比 🔴

维度JDK 7JDK 8
并发机制Segment 分段锁(ReentrantLock)CAS + synchronized
锁粒度段级别(16 个 Segment)桶级别(Node)
读操作无锁无锁(volatile + CAS)
写操作锁段锁桶(首次 CAS 失败后)
扩容单线程扩容支持多线程并发扩容
实现复杂度高(双重数据结构)低(统一 Node 结构)

二、核心数据结构 🔴

2.1 Node 节点

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:保证可见性

    Node(int hash, K key, V val, Node<K, V> next) {
        this.hash = hash;
        this.key = key;
        this.val = val;
        this.next = next;
    }

    // 只读方法
    public final Map.Entry<K, V> next() { return next; }
}

关键valnext 都是 volatile,保证读取线程能看到最新值。

2.2 TreeBin 红黑树节点

static final class TreeBin<K, V> extends Node<K, V> {
    TreeNode<K, V> root;
    TreeNode<K, V> first;
    WTLock waiter; // 等待获取写锁的线程
    int lockState; // 锁状态
    // ...
}

三、put 流程源码解析 🔴

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();

    int hash = spread(key.hashCode()); // 与 HashMap 不同:再次扰动
    int binCount = 0;

    for (Node<K, V>[] tab = table; ; ) {
        Node<K, V> f;
        int n, i, fh;

        // 第一次 put:初始化 table
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();

        // CAS + 无锁:检查桶是否为空
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // CAS 尝试创建新节点(成功则结束)
            if (casTabAt(tab, i, null, new Node<K, V>(hash, key, value, null)))
                break;
        }

        // 扩容帮助(当前线程检测到正在扩容,协助扩容)
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);

        // 桶中有数据:synchronized 加锁
        else {
            V oldVal = null;

            // 🔒 锁住当前桶(而非整个表)
            synchronized (f) {
                // 双重检查:确保桶的头节点没变
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        // 链表处理
                        binCount = 1;
                        for (Node<K, V> e = f; ; ++binCount) {
                            if (e.hash == hash &&
                                ((k = e.key) == key || key.equals(k))) {
                                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 = ((TreeBin<K, V>) f).putTreeVal(hash, key, value);
                        oldVal = p.val;
                    }
                }
            }

            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }

    addCount(1L, binCount);
    return null;
}

四、CAS 的使用场景 🔴

4.1 tabAt:读取桶中的节点

// 读取 table[i] 的值
// volatile 读取(保证可见性)
static final <K, V> Node<K, V> tabAt(Node<K, V>[] tab, int i) {
    return U.getObjectVolatile(tab, ((long) i << ASHIFT) + ABASE);
}

4.2 casTabAt:CAS 设置桶中的节点

// CAS 尝试设置 table[i]
static final <K, V> boolean casTabAt(Node<K, V>[] tab, int i,
                                      Node<K, V> c, Node<K, V> v) {
    return U.compareAndSwapObject(tab, ((long) i << ASHIFT) + ABASE, c, v);
}

4.3 CAS 的典型场景

// 场景一:桶为空时,无锁插入
if (casTabAt(tab, i, null, newNode)) {
    break; // 成功,插入完成
}

// 场景二:初始化 table
private final Node<K, V>[] initTable() {
    Node<K, V>[] tab;
    int sc;
    while ((tab = table) == null || tab.length == 0) {
        if ((sc = sizeCtl) < 0)
            Thread.yield(); // 让出 CPU
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            // CAS 设置 sizeCtl = -1,表示正在初始化
            // 只有一个线程能进入这里
            // ...
        }
    }
}

五、synchronized 的使用场景 🔴

5.1 为什么不用 ReentrantLock

// JDK 7 的 Segment:
class Segment<K, V> extends ReentrantLock {
    // 每个 Segment 是一把锁
}

// JDK 8 改用 synchronized:
synchronized (f) { // 锁住当前桶的头节点
    // ...
}

JDK 8 选择 synchronized 的原因

  1. JDK 6+ 对 synchronized 做了大量优化(偏向锁、轻量级锁、自旋锁)
  2. synchronized 不需要手动释放锁
  3. 锁粒度更细(桶级别,而非段级别)

5.2 锁的代价

// 读多写少场景:大部分读操作无锁
Node<K, V> e = tabAt(tab, i); // volatile 读取,无锁
if (e != null) {
    return e.val; // 无锁读取
}

// 写操作:只锁当前桶
synchronized (f) { // 只锁一个桶
    // ...
}

六、并发扩容机制 🔴

6.1 JDK 7 的单线程扩容

JDK 7 的 Segment 扩容是单线程的,其他线程只能等待。

6.2 JDK 8 的多线程并发扩容

// JDK 8 支持多线程同时 transfer
// 每个线程处理一批桶(默认 16 个)
// 用 sizeCtl 控制并发度

static final int MOVED = -1; // hash == MOVED 表示正在扩容

// 帮助扩容:
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) {
        // 协助 transfer
        // ...
    }
}

七、追问升级

面试官:"ConcurrentHashMap 的 get 操作是无锁的吗?"

// 是的,get 操作完全无锁
public V get(Object key) {
    Node<K, V>[] tab;
    Node<K, V> e, p;
    int n, eh;
    K ek;

    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & (h = spread(key.hashCode())))) != null) {
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        } else if (eh < 0) // 红黑树或 ForwardingNode
            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;
}

// 无锁原因:
// 1. val 和 next 是 volatile,保证可见性
// 2. Node 节点的添加是"发布安全"的(putVal 完全完成后才对其他线程可见)

【面试官心理】 能说出 get 操作无锁原理的候选人,说明对 Java 内存模型和 volatile 有深入理解。这是 P6+ 的标准要求。