ConcurrentHashMap JDK 7 分段锁

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

候选人小汤答:"用分段锁。"

面试官追问:"具体是怎么实现的?"

小汤说:"分成多个 Segment..."

面试官追问:"每个 Segment 相当于什么?"

小汤答不上来。

【面试官心理】 JDK 7 的分段锁是 ConcurrentHashMap 的经典设计。能说出 Segment 继承 ReentrantLock、16 个分段、并发度为 16 的候选人,说明对 JDK 7 并发集合有了解。

一、分段锁设计 🔴

// JDK 7 的 ConcurrentHashMap 结构
public class ConcurrentHashMap<K, V>
        extends AbstractMap<K, V>
        implements ConcurrentMap<K, V> {

    // 默认分段数:16
    static final int DEFAULT_CONCURRENCY_LEVEL = 16;

    // Segment 数组(不可扩容)
    final Segment<K, V>[] segments;

    // Segment 继承 ReentrantLock
    static final class Segment<K, V> extends ReentrantLock {
        transient volatile HashEntry<K, V>[] table;
        transient int count;
        // ...
    }
}

1.1 数据结构图

ConcurrentHashMap (JDK 7)
┌─────────────────────────────────────────┐
│  Segment[] (16 个,分段锁)               │
│  ┌─────┬─────┬─────┬─────┬─────┬───┐    │
│  │ Seg0│ Seg1│ Seg2│ Seg3│ ... │Seg15│    │
│  └──┬──┴──┬──┴──┬──┴──┬──┴─────┴───┘    │
│     │     │     │     │                   │
│     ▼     ▼     ▼     ▼                   │
│  ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐          │
│  │ Hash│ │ Hash│ │ Hash│ │ Hash│  ← 链表  │
│  │Entry│ │Entry│ │Entry│ │Entry│          │
│  └─────┘ └─────┘ └─────┘ └─────┘          │
└─────────────────────────────────────────┘

并发度:最多 16 个线程同时写入(理想情况)

二、put 流程 🔴

public V put(K key, V value) {
    // 1. 计算 key 的 hash
    int hash = hash(key);

    // 2. 定位 Segment
    int segIdx = (hash >>> 28) & (segmentMask); // segmentMask = 15
    Segment<K, V> segment = segments[segIdx];

    // 3. 对 Segment 加锁
    segment.lock();

    try {
        // 4. 在 Segment 内部操作 HashMap
        HashEntry<K, V>[] tab = segment.table;
        int index = (tab.length - 1) & hash;
        HashEntry<K, V> first = tab[index];

        // 5. 遍历链表查找/插入
        for (HashEntry<K, V> e = first; ; ) {
            if (e != null) {
                if (e.key == key || key.equals(e.key)) {
                    V oldValue = e.value;
                    e.value = value;
                    return oldValue;
                }
                e = e.next;
            } else {
                // 插入新节点
                tab[index] = new HashEntry<>(key, value, first);
                break;
            }
        }
    } finally {
        // 6. 解锁
        segment.unlock();
    }
}

三、JDK 7 vs JDK 8 对比 🟡

维度JDK 7 分段锁JDK 8 CAS+synchronized
锁粒度Segment 级别(粗粒度)Node 级别(细粒度)
并发度固定 16(可配置)动态,取决于实际数据分布
读操作无锁无锁(volatile)
写操作加锁 Segment首次 CAS 失败后,锁单个 Node
实现复杂度高(双重结构)低(统一 Node 结构)
内存开销高(每个 Segment 独立结构)低(只有数据节点)

四、为什么 JDK 8 改进了 🟡

// JDK 7 分段锁的问题:

// 问题一:并发度固定
// 即使数据很少,也只能有 16 个线程并发
// 如果写入集中在 1 个 Segment,并发度降为 1

// 问题二:初始化开销
// ConcurrentHashMap 构造时会创建所有 Segment

// 问题三:内存浪费
// 每个 Segment 都有独立的 table
// 即使某个 Segment 没有数据,也占用内存

// JDK 8 改进:
// 1. 去掉 Segment,直接用 Node
// 2. CAS + synchronized 代替 ReentrantLock
// 3. 支持多线程并发扩容
// 4. 内存效率更高