← 返回 JUC 列表

ConcurrentHashMap(重中之重)

ConcurrentHashMap(重中之重)

一、基础概念

为什么需要 ConcurrentHashMap?

集合 问题
HashMap 线程不安全,多线程 put 死循环(JDK 7 头插法)或丢失数据
Hashtable 线程安全但性能差,所有方法全表加 synchronized
Collections.synchronizedMap() 同 Hashtable,全表加锁
ConcurrentHashMap 线程安全 + 高性能,分段锁 / CAS 实现

JDK 7 vs JDK 8 核心区别(面试必问)

对比维度 JDK 7 JDK 8
数据结构 Segment 分段锁 + HashEntry Node + 数组 + 链表 + 红黑树
并发粒度 Segment 级别(默认 16 个) Node 桶级别(更细粒度)
锁实现 ReentrantLock(继承锁) synchronized + CAS(优化后的锁)
扩容机制 每个 Segment 内扩容 多线程协助扩容
查询时间复杂度 O(log n) 链表遍历 O(log n) → O(log n) 红黑树优化
size() 统计 先无锁累加 3 次,失败再加锁 baseCount + CounterCell 数组

二、JDK 7 实现(分段锁)

结构

ConcurrentHashMap
    ┌──────────────────────────────────────┐
    │  Segment[] 数组(默认 16)             │
    │  ┌────┐  ┌────┐       ┌────┐        │
    │  │ S0 │  │ S1 │  ...  │ S15│        │
    │  └────┘  └────┘       └────┘        │
    │    │        │            │           │
    │    ▼        ▼            ▼           │
    │  HashEntry[] 数组                    │
    │  ┌──┐ ┌──┐            ┌──┐          │
    │  │H0│ │H1│            │Hn│          │
    │  └──┘ └──┘            └──┘          │
    └──────────────────────────────────────┘

Segment 继承 ReentrantLock,每次操作只锁对应 Segment,其他 Segment 不受影响。

并发度

  • 默认 concurrencyLevel = 16,即最多 16 个线程同时写
  • 参数在初始化时确定,后续不可变

JDK 7 的问题

  • Segment 数量固定,扩容只能提高 HashEntry 数组长度
  • 内存占用大(每个 Segment 都是一个锁对象)
  • 查询效率低(先定位 Segment,再定位 HashEntry)
  • 并发度有上限(最大 64K 个 Segment 受 hash 算法限制)

三、JDK 8 实现(CAS + synchronized)

结构

ConcurrentHashMap(JDK 8)
    ┌──────────────────────────────────────┐
    │  Node[] table(数组)                 │
    │  ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐    │
    │  │  │N0│  │N1│  │  │N2│  │  │  │    │
    │  └──┴┬─┴──┴┬─┴──┴──┴┬─┴──┴──┴──┘    │
    │      │     │        │               │
    │      ▼     ▼        ▼               │
    │   链表  红黑树   链表   红黑树        │
    │   (≤8)  (≥8)   (≤8)  (≥8)          │
    └──────────────────────────────────────┘

额外辅助结构:
    ┌──────────────────────────┐
    │  baseCount  (基础计数)  │
    │  CounterCell[](辅助计数)│ ← 减少 CAS 竞争
    │  sizeCtl    (控制标识)  │ ← -1=正在初始化,>0=扩容阈值
    │  transferIndex(扩容索引)│ ← 多线程扩容分配任务
    │  TreeBin    (红黑树根)  │ ← 链表转红黑树的容器
    └──────────────────────────┘

put() 流程(核心操作,面试必问)

java
final V putVal(K key, V value, boolean onlyIfAbsent) {
    // key 和 value 都不能为 null(区别于 HashMap)
    if (key == null || value == null) throw new NullPointerException();

    int hash = spread(key.hashCode()); // 扰动计算
    int binCount = 0;

    // 自旋(CAS 失败就重试)
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;

        // ① 数组未初始化 → 初始化
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();

        // ② 当前桶为空 → CAS 存放(无锁)
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
                break;
        }

        // ③ 正在扩容 → 协助扩容
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);

        // ④ 发生 hash 冲突 → synchronized 锁住当前桶的 head
        else {
            V oldVal = null;
            synchronized (f) { // 只锁链表/红黑树的头节点
                if (tabAt(tab, i) == f) { // 双重检查
                    if (fh >= 0) { // 链表
                        binCount = 1;
                        for (Node<K,V> e = f;; e = e.next) {
                            // 找到 key 覆盖 value
                            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);
                                break;
                            }
                        }
                    } else if (f instanceof TreeBin) { // 红黑树
                        // 向红黑树插入节点
                    }
                }
            }
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD) // >= 8 转红黑树
                    treeifyBin(tab, i);
                if (oldVal != null) return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount); // 增加计数
    return null;
}

put 流程总结

put(key, value)
    │
    ├── key/value 为 null?→ 抛异常
    │
    ├── ① table 未初始化?→ initTable() 初始化(CAS + volatile)
    │
    ├── ② 桶为空?→ CAS 直接放入(无锁,性能最高)
    │
    ├── ③ 节点是 MOVED 状态?→ helpTransfer() 协助扩容
    │
    ├── ④ 桶非空(冲突)→ synchronized(桶头节点)
    │       ├── 链表 → 遍历,找到覆盖/尾插
    │       └── 红黑树 → treeNode 插入
    │
    └── ⑤ addCount() 更新大小 → 检查是否需要扩容

get() 流程(无锁)

java
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;
        }
        // ③ hash 为负 → 红黑树或正在扩容,用 find() 查找
        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;
}

get() 全程无锁,Node 的 val 和 next 用 volatile 保证可见性。

size() 统计机制

java
// 不加锁的累加方式
// sumCount() 累加 baseCount + CounterCell[]
public int size() {
    long n = sumCount();
    return (n < 0L) ? 0 : (n > Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int)n;
}

// CounterCell 是什么?
// 多个线程同时 addCount() 时,CAS 更新 baseCount。
// 如果 CAS 失败(竞争激烈),不重试而是用 CounterCell 数组分散写入。
// sumCount() 时累加 baseCount + 所有 CounterCell 的值。

四、扩容机制(多线程协助)

什么时候扩容?

  • 元素数量 > sizeCtl(扩容阈值 = 0.75 × 数组长度)
  • 链表长度 >= 8 但数组长度 < 64(先扩容而不是转红黑树)

多线程协助扩容

JDK 8 核心优化:扩容时多个线程可以一起帮忙搬数据!

① 一个线程发现需要扩容 → 创建新的数组(2 倍)
② 将任务按步长分配(默认 stride = 16 个桶)
③ 各个线程领取自己的区间,将 oldTab[i] 转移到 newTab
④ 搬运完后,旧桶设为 ForwardingNode(MOVED 标记)
⑤ 其他线程发现 MOVED 标记 → 协助扩容(helpTransfer)

扩容时的高低位搬运

java
// 扩容时,链表会拆成低位链和高位链
// 低位链:hash & oldCap == 0 → 放在 newTab[i]
// 高位链:hash & oldCap != 0 → 放在 newTab[i + oldCap]

// 例:oldCap = 16,扩容到 32
// 元素 hash 的 index = (n-1) & hash
// 扩容前:hash & 15      → 得到原索引
// 扩容后:hash & 31      → 要么原索引,要么原索引+16
//            ↓
// 用 hash & 16(oldCap)判断:
//   为 0 → 新索引不变
//   为 1 → 新索引 = 原索引 + 16

五、JDK 7/8 对比总结

对比项 JDK 7 JDK 8
并发粒度 Segment(多锁) Node 桶级别(更细)
加锁方式 ReentrantLock synchronized + CAS
数据结构 Segment + HashEntry Node + 链表 + 红黑树
扩容 单线程扩容 多线程协助扩容
查询优化 无(链表查询) 链表 > 8 转红黑树
key/value null 不允许 不允许
迭代器 弱一致性 弱一致性

为什么 JDK 8 用 synchronized 而不是 ReentrantLock?

  • JDK 6+ 对 synchronized 做了重量级优化(锁升级、偏向锁、轻量级锁)
  • synchronized 在 JVM 层面,能根据竞争情况自动优化
  • 代码更简洁,减少了内存占用

注意事项

  1. ConcurrentHashMap 的 key 和 value 都不能为 null(HashMap 允许 key/value 为 null)
    • 原因:无法判断是 value 为 null 还是不存在,多线程场景下有歧义
  2. 弱一致性——迭代器不保证获取到最新的数据,但不会抛 ConcurrentModificationException
  3. size() 是个近似值——不是精确的,因为统计过程没有加锁
  4. 批量操作(putAll/clear)没有原子性保证
  5. 合理设置初始容量——避免频繁扩容