ConcurrentHashMap 怎么保证线程安全?从分段锁到 CAS+synchronized

ConcurrentHashMap 怎么保证线程安全?从分段锁到 CAS+synchronized

面试必问的并发集合,从 JDK 7 的分段锁到 JDK 8 的 CAS + synchronized,设计哲学发生了根本性变化。

一、为什么需要 ConcurrentHashMap?

HashMap 在多线程下会出问题(数据覆盖、size 不准、甚至 JDK7 的死循环),而 Hashtable 用 synchronized 锁住整个 table,并发度极低:

scss 复制代码
Hashtable (全表锁):
┌──────────────────────────────────────────┐
│  ┌───┬───┬───┬───┬───┬───┬───┬───┐     │
│  │ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │     │  ← 一个锁锁全部
│  └───┴───┴───┴───┴───┴───┴───┴───┘     │
└──────────────────────────────────────────┘
  10 个线程操作 → 串行执行 → 吞吐量极低

ConcurrentHashMap (分段锁/桶级锁):
┌──────────────────────────────────────────┐
│  ┌───┬───┬───┬───┬───┬───┬───┬───┐     │
│  │ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │     │  ← 每个桶独立锁
│  └─L0─┴─L1─┴─L2─┴─L3─┴─L4─┴─L5─┴─L6─┴─L7┘│
└──────────────────────────────────────────┘
  10 个线程操作不同桶 → 并行执行 → 高吞吐

二、JDK 7:分段锁(Segment)

JDK 7 的 ConcurrentHashMap 采用 Segment + HashEntry 两级哈希结构:

css 复制代码
┌─────────────────────────────────────────────────────┐
│              JDK 7 ConcurrentHashMap 结构             │
├─────────────────────────────────────────────────────┤
│                                                     │
│   Segment[] (默认 16 个段,并发度 = 16)                │
│   ┌──────┬──────┬──────┬──────┬──────┐              │
│   │ Seg0 │ Seg1 │ Seg2 │ Seg3 │ ...  │              │
│   └──┬───┴──┬───┴──────┴──────┴──────┘              │
│      │      │                                        │
│      ▼      ▼                                        │
│   ┌──────┐ ┌──────┐                                │
│   │Entry │ │Entry │  每个 Segment 内部是一个       │
│   │table │ │table │  小型 HashMap                   │
│   └──┬───┘ └──┬───┘                                │
│      │        │                                     │
│   ┌──┴──┐  ┌──┴──┐                                 │
│   │HE 0 │  │HE 0 │  HashEntry 链表                  │
│   └──┬──┘  └──┬──┘                                 │
│      │        │                                     │
│   ┌──┴──┐  ┌──┴──┐                                 │
│   │HE 1 │  │HE 1 │                                 │
│   └─────┘  └─────┘                                 │
│                                                     │
│   Segment 继承 ReentrantLock                       │
│   锁粒度 = Segment 级别                              │
│                                                     │
└─────────────────────────────────────────────────────┘
java 复制代码
// JDK 7 核心结构
static final class Segment<K,V> extends ReentrantLock {
    transient volatile HashEntry<K,V>[] table;
    // put 时 lock(),get 时不需要锁(volatile)
}

static final class HashEntry<K,V> {
    final int hash;
    final K key;
    volatile V value;
    volatile HashEntry<K,V> next;
}

JDK 7 put 链路

java 复制代码
V put(K key, int hash, V value, boolean onlyIfAbsent) {
    // tryLock 尝试获取锁
    HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(hash, key, value);
    // 获取锁后执行插入
    try {
        HashEntry<K,V>[] tab = table;
        int index = (tab.length - 1) & hash;
        HashEntry<K,V> first = entryAt(tab, index);
        // 遍历链表,key 存在则替换,不存在则头插法插入
        // ...
    } finally {
        unlock();
    }
}

JDK 7 get 链路

get 操作不需要加锁,依赖 volatile 保证可见性:

java 复制代码
V get(Object key, int hash) {
    HashEntry<K,V> e = entryAt(tab, (tab.length - 1) & hash);
    while (e != null) {
        if (e.hash == hash && key.equals(e.key))
            return e.value;
        e = e.next;
    }
    return null;
}

三、JDK 8:CAS + synchronized 桶级锁

JDK 8 抛弃了 Segment,改为 Node 数组 + 链表/红黑树 + CAS + synchronized

css 复制代码
┌─────────────────────────────────────────────────────┐
│              JDK 8 ConcurrentHashMap 结构             │
├─────────────────────────────────────────────────────┤
│                                                     │
│   Node[] table (与 HashMap 结构一致)                   │
│   ┌─────┬─────┬─────┬─────┬─────┬─────┐            │
│   │  0  │  1  │  2  │  3  │  4  │ ... │            │
│   └──┬──┴─────┴──┬──┴─────┴──┬──┴─────┘            │
│      │            │            │                      │
│      ▼            ▼            ▼                      │
│   ┌──────┐   ┌──────┐   ┌──────┐                   │
│   │ Node │   │ null │   │ Node │                   │
│   └──┬───┘   └──────┘   └──┬───┘                   │
│      │                     │                        │
│      ▼                     ▼                        │
│   ┌──────┐            ┌──────┐                     │
│   │ Node │            │TreeNd│                     │
│   └──────┘            └──────┘                     │
│                                                     │
│   锁粒度 = 桶级(table[i] 的头节点)                    │
│   并发度 = table.length(默认 16,可随扩容增长)          │
│                                                     │
└─────────────────────────────────────────────────────┘

JDK 7 vs JDK 8 对比

对比维度 JDK 7 JDK 8
数据结构 Segment\[\] + HashEntry\[\] Node\[\] + 链表/红黑树
锁实现 ReentrantLock(Segment) CAS + synchronized(桶头节点)
锁粒度 Segment 级(默认 16 段) 桶级(tablei
并发度 固定 16 随 table 长度增长
get 是否加锁 否(volatile) 否(volatile + CAS)
size 计算 遍历所有 Segment baseCount + CounterCell\[\]
红黑树 有(链表 ≥ 8 时转换)
扩容 Segment 级独立扩容 全表扩容(多线程协助迁移)

四、JDK 8 put 全链路

java 复制代码
final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;

        // 1. table 为空,初始化
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();

        // 2. 桶位置为空,CAS 插入(无锁)
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
                break;
        }
        // 3. 桶头节点是 ForwardingNode(扩容中),协助迁移
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);

        // 4. 桶不为空,synchronized 锁住头节点
        else {
            V oldVal = null;
            synchronized (f) {
                // 双重检查:确保 f 仍然是头节点
                if (tabAt(tab, i) == f) {
                    // 链表处理
                    if (fh >= 0) {
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            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, null);
                                break;
                            }
                        }
                    }
                    // 红黑树处理
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            // 链表长度 ≥ 8,转红黑树
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    // 5. 更新计数
    addCount(1L, binCount);
    return null;
}

put 流程图:

scss 复制代码
┌──────────────┐
│ put(key,val)  │  key/value 不能为 null
└──────┬───────┘
       ▼
┌────────────────┐    是    ┌──────────────┐
│ table 为空?     │────────▶│ initTable()  │ CAS 初始化
└──────┬─────────┘          └──────┬───────┘
       │ 否                         │
       ▼ <─────────────────────────┘
┌────────────────────┐    是    ┌────────────────────┐
│ 桶位置为空?         │────────▶│ CAS 插入新节点       │ 无锁!
└──────┬─────────────┘          └──────┬─────────────┘
       │ 否                           │
       ▼ <────────────────────────────┘
┌────────────────────┐    是    ┌────────────────────┐
│ MOVED(扩容中)?     │────────▶│ helpTransfer()     │ 协助迁移
└──────┬─────────────┘          └──────┬─────────────┘
       │ 否                           │
       ▼ <────────────────────────────┘
┌────────────────────────┐
│ synchronized(头节点)     │ 桶级锁
│ ├─ 链表:遍历+尾插       │
│ └─ 红黑树:putTreeVal   │
└──────┬─────────────────┘
       ▼
┌────────────────────┐    是    ┌────────────────┐
│ 链表 ≥ 8?          │────────▶│ treeifyBin()    │
└──────┬─────────────┘          └──────┬─────────┘
       ▼                               │
┌──────────────┐                       │
│ addCount()   │ ←─────────────────────┘
└──────────────┘

五、CAS 操作详解

ConcurrentHashMap 大量使用 CAS 实现无锁操作:

java 复制代码
// 获取桶位置节点(volatile 读)
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
    return (Node<K,V>)U.getReferenceAcquire(tab, ((long)i << ASHIFT) + ABASE);
}

// CAS 设置桶位置节点
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                    Node<K,V> c, Node<K,V> v) {
    return U.compareAndSetReference(tab, ((long)i << ASHIFT) + ABASE, c, v);
}

// volatile 写入桶位置
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
    U.putReferenceRelease(tab, ((long)i << ASHIFT) + ABASE, v);
}
java 复制代码
CAS 乐观锁原理:

线程1: 读取 tab[3] = null
        CAS(tab, 3, null, newNode) → 成功!

线程2: 读取 tab[3] = null  (在线程1写入前读取)
        CAS(tab, 3, null, newNode) → 失败!
        → 进入下一轮 for 循环重试
        重新读取 tab[3] = 线程1的节点
        → 走 synchronized 分支

六、扩容与多线程协助迁移

JDK 8 的扩容支持多线程协助:

java 复制代码
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    int n = tab.length, stride;
    // 每个线程处理的桶数量(最小 16)
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE;

    if (nextTab == null) {
        // 第一个发起扩容的线程创建新数组
        Node<K,V>[] nt = new Node[n << 1]; // 容量翻倍
        nextTab = nt;
        nextTable = nt;
        transferIndex = n; // 从右往左分配桶
    }

    int nextn = nextTab.length;
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);

    boolean advance = true;
    boolean finishing = false;
    for (int i = 0, bound = 0;;) {
        Node<K,V> f; int fh;
        // 1. 分配迁移区间 [bound, i]
        while (advance) {
            if (--i >= bound || finishing)
                advance = false;
            else if ((nextIndex = transferIndex) <= 0) {
                i = -1;
                advance = false;
            }
            else if (U.compareAndSetInt(this, TRANSFERINDEX,
                     nextIndex, nextBound = nextIndex > stride ?
                     nextIndex - stride : 0)) {
                bound = nextBound;
                i = nextIndex - 1;
                advance = false;
            }
        }

        // 2. 迁移完成
        if (i < 0 || i >= n || i + n >= nextn) {
            if (finishing) {
                nextTable = null;
                table = nextTab;
                sizeCtl = (n << 1) - (n >>> 1); // 新阈值
                return;
            }
            // CAS 减少 sizeCtl,表示一个线程完成迁移
            if (U.compareAndSetInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                    return; // 还有其他线程在迁移
                finishing = advance = true;
                i = n;
            }
        }
        // 3. 空桶,放 ForwardingNode
        else if ((f = tabAt(tab, i)) == null)
            advance = casTabAt(tab, i, null, fwd);
        // 4. 已迁移(ForwardingNode),跳过
        else if ((fh = f.hash) == MOVED)
            advance = true;
        // 5. 迁移链表/红黑树
        else {
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    // 与 HashMap 类似:hash & oldCap 分为低位链和高位链
                    // 迁移完成后在原位置放 ForwardingNode
                }
            }
        }
    }
}
ini 复制代码
多线程协助扩容:

线程1: 负责 [0, 15]
线程2: 负责 [16, 31]
线程3: 负责 [32, 47]

┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ F │ F │ F │ F │ F │ N │ N │ N │ N │ N │ N │ N │ N │ N │ N │ N │
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
  ← 线程1 已完成 →  ← 线程2 正在迁移 →     ← 线程3 待迁移 →

F = ForwardingNode (已迁移)
N = 未迁移节点

遇到 F 的 put 请求 → helpTransfer() 协助迁移

七、size 计算:baseCount + CounterCell

JDK 7 用遍历所有 Segment 的方式计算 size(先乐观读,重试 3 次不一致再加锁)。

JDK 8 借鉴了 LongAdder 的设计:

java 复制代码
// 基础计数
private transient volatile long baseCount;
// 计数桶(分散竞争)
private transient volatile CounterCell[] counterCells;

static final class CounterCell {
    volatile long value;
    CounterCell(long x) { value = x; }
}

private final void addCount(long x, int check) {
    CounterCell[] as; long b, s;
    // 1. 先 CAS 更新 baseCount
    if ((as = counterCells) != null ||
        !U.compareAndSetLong(this, BASECOUNT, b = baseCount, s = b + x)) {
        // 2. baseCount CAS 失败 → 分散到 CounterCell
        CounterCell a; long v; int m;
        boolean uncontended = true;
        if (as == null || (m = as.length - 1) < 0 ||
            (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
            !(uncontended =
              U.compareAndSetLong(a, CELLVALUE, v = a.value, v + x))) {
            fullAddCount(x, uncontended); // 初始化/扩容 CounterCell
            return;
        }
        s = sumCount();
    }
    // 3. 检查是否需要扩容
    if (check >= 0) {
        while (s >= (long)(sc = sizeCtl) && sc < MAXIMUM_CAPACITY) {
            // 触发扩容
        }
    }
}

public int size() {
    long n = sumCount();
    return (n < 0L) ? 0 : (n > Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int)n;
}

final long sumCount() {
    CounterCell[] as = counterCells; CounterCell a;
    long sum = baseCount;
    if (as != null) {
        for (CounterCell cell : as)
            if ((a = cell) != null)
                sum += a.value;
    }
    return sum;
}
css 复制代码
size 计算策略:

高并发场景:
  线程1 → CAS(baseCount) → 成功 → baseCount += 1
  线程2 → CAS(baseCount) → 失败 → CounterCell[2] += 1
  线程3 → CAS(baseCount) → 失败 → CounterCell[5] += 1

  size = baseCount + sum(CounterCell[])
  
  本质: 将一个热点计数分散到多个 cell,减少 CAS 冲突
  类似 LongAdder 的 "分散热点" 思想

八、ForwardingNode:扩容中的哨兵

java 复制代码
static final class ForwardingNode<K,V> extends Node<K,V> {
    final Node<K,V>[] nextTable;
    ForwardingNode(Node<K,V>[] tab) {
        super(MOVED, null, null, null);
        this.nextTable = tab;
    }
}

ForwardingNode 的 hash 值固定为 MOVED = -1,作用是:

  • 标记该桶已完成迁移
  • get 操作遇到 ForwardingNode 时,会转发到 nextTable 查找
  • put 操作遇到 ForwardingNode 时,会协助扩容

九、常见面试追问

Q1:为什么 JDK 8 放弃了 Segment?

  1. Segment 粒度太粗,并发度固定为 16
  2. 两级哈希查找开销大
  3. 桶级锁粒度更细,并发度随 table 增长
  4. synchronized 在 JDK 8 后做了大量优化(锁升级),性能不亚于 ReentrantLock

Q2:ConcurrentHashMap 为什么不允许 null key 和 null value?

HashMap 允许 null,但 ConcurrentHashMap 不允许。原因是 二义性 :get 返回 null 时,无法区分是"不存在"还是"值为 null"。在单线程下可以 containsKey 确认,但在多线程下,containsKey 和 get 之间可能有其他线程修改了 map,导致结果不可靠。

Q3:ConcurrentHashMap 的 get 需要加锁吗?

不需要。Node 的 val 和 next 都是 volatile 的,保证可见性。整个 get 过程是无锁的。

Q4:sizeCtl 是什么?

sizeCtl 是一个多用途的 volatile int:

  • -1:表示正在初始化
  • -(1 + N):表示有 N 个线程正在扩容
  • 0:默认值(未初始化)
  • 正数:表示扩容阈值(初始化或扩容后)

十、总结

arduino 复制代码
JDK 7: Segment(ReentrantLock) + HashEntry → 分段锁,并发度固定
JDK 8: CAS + synchronized(桶头节点) + Node/TreeBin → 桶级锁,并发度动态

put: 空桶CAS → 非空synchronized → 链表/红黑树 → addCount
get: volatile读 → 遍历链表/树 → 无锁
size: baseCount + CounterCell[] → 分散热点
扩容: 多线程协助 → ForwardingNode标记 → 低位链/高位链分离

ConcurrentHashMap 的演进体现了并发设计的核心理念:锁粒度越细,并发度越高;能用无锁就不用锁,必须加锁也要最小化锁范围

相关推荐
SimonKing28 分钟前
升级Spring Boot 4后,从 Jackson 2 到 3,到底有哪些变化
java·后端·程序员
吃饱了得干活1 小时前
一篇讲清楚Spring Boot:自动装配、启动器、过滤器、拦截器、设计模式
java·spring boot·后端
lhldsg2 小时前
AI零售系统实战指南:从架构设计到落地部署全解析
java·人工智能·小程序·uni-app·零售
Interview Aid1123 小时前
TikTok OA 四题分享|半小时内 AC,题目基本都是实现题
java·开发语言·算法·面试·职场和发展
mldong3 小时前
换了工作流引擎,前端一行代码没改
java·架构
微尘寒风10 小时前
【Git】的安装和使用
java·git
y = xⁿ11 小时前
DeepSeek Harness 学习日记:关于Agent接口,工具调用的底层实现
android·java·学习
侧耳倾听11111 小时前
jwt使用简介
java·jwt
顶点多余12 小时前
那些在算法中适合巩固的知识点---1
java·前端·算法