ConcurrentHashMap 并发机制与 synchronized 决策

java-concurrency 📚 learning hash-map-concurrent · concurrenthashmap · jdk8 · cas · volatile · synchronized · reentrantlock · transfer-index · longadder · basecount · countercells

TL;DR(30 秒扫完)

  • JDK7 分段锁:16 个 Segment,每个 Segment 是 ReentrantLock + 小表,并发度上限 16
  • JDK8 桶级锁:Node[] table + volatile 桶头 + 桶级 synchronized,并发度 = 表大小
  • putVal 五分支:空桶 CAS 无锁 → FORWARD_NODE 协助扩容 → key 相同覆盖 → TREEBIN 树内 → 链表尾插
  • size 统计:baseCount CAS + counterCells[] 分片累加(LongAdder 思路)
  • 协助扩容:transferIndex 从右向左迁移、每线程 STRIDE=16 桶、FORWARD_NODE 标记
  • 为什么选 synchronized 不选 ReentrantLock:JDK6+ 锁升级追平 ReentrantLock;语义简单、JIT 友好、无 AQS 队列开销

关键结论

结论 AJDK8 ConcurrentHashMap 核心是"桶级 synchronized + CAS 混合"——空桶 CAS 无锁,非空桶锁桶头
结论 B读操作走 volatile 完全无锁,写冲突粒度只锁一个桶,读多写少场景性能极好
结论 Csize() 返回值是"某个瞬间的近似值",不保证强一致(LongAdder 分片累加)
结论 DJDK8 选 synchronized 的关键是 JDK6 引入锁升级让 synchronized 性能追平 ReentrantLock,且语义/JIT/内存都更优

完整讲解(费曼四步)

STEP 1 · 概念

ConcurrentHashMap 是 Java 中最常用的并发 Map,从 JDK7 到 JDK8 有重大演进。JDK7 用"分段锁"(Segment 数组 + 每个 Segment 内独立小 HashEntry 数组 + ReentrantLock),并发度上限 16。JDK8 彻底重构,去掉了 Segment,改用"桶级 synchronized + CAS 混合",配合 volatile 桶头、LongAdder 式分片计数、多线程协助扩容等机制,把并发度提升到表容量级别。

STEP 2 · 大白话

JDK7 类比:想象一个图书馆,分成 16 个大阅览室(Segment),每个阅览室有自己的管理员(ReentrantLock)。你想借书,只要去对应阅览室拿管理员的钥匙就能读写,不同阅览室的读者可以同时进行。缺点:只有 16 个阅览室,超过 16 个人同时来就排队了。
JDK8 类比:图书馆拆掉了大阅览室的墙壁,改成一个巨大的开放书架(Node 数组),每个书架格子(桶)前只锁那一格。你要写的时候用智能钥匙(CAS)插到空格里,不用找管理员;格子非空时才锁那一格(synchronized),不影响旁边。统计书总量时把每个管理员的计数分开累加(LongAdder 分片),最后汇总——这就是"分片计数"。

STEP 3 · 底层

JDK7 结构

// JDK7
public class ConcurrentHashMap extends AbstractMap
        implements Map, Cloneable, java.io.Serializable {
    
    private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
    
    transient Segment[] segments;
    transient int sizeCount;  // 分段总和
    
    // 每个 Segment 是独立的锁 + 小表
    static final class Segment extends ReentrantLock
            implements java.io.Serializable {
        transient HashEntry[] table;  // 小 HashEntry 数组
        transient int count;          // 本段 size
        // ...
    }
}
并发度上限:Segment 数量(默认 16),最多 16 个线程同时写。 Segment 需要 ReentrantLock 的原因:
  • 需要 tryLock() 非阻塞尝试加锁(避免死等)
  • 需要 lockInterruptibly() 可中断(应对长任务)
  • 需要公平锁选项(new ReentrantLock(true))

JDK8 结构

// JDK8
public class ConcurrentHashMap<K,V>
        extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Cloneable,
        Serializable {
    
    private transient volatile Node<K,V>[] table;   // 桶数组,volatile
    private transient volatile int sizeCtl;          // -1: 首次初始化;<0: 扩容中
    private transient volatile long baseCount;       // 计数基值(LongAdder 思路)
    private transient volatile CounterCell[] counterCells;  // 分片计数
    
    // 无 Segment,直接用 Node 数组
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        volatile V value;      // volatile
        volatile Node<K,V> next;  // volatile
        // ...
    }
}
核心变化:
  • 去掉 Segment,用一个大表
  • 桶头 volatile 保证可见性
  • value 和 next 都 volatile(读无需加锁)
  • 计数用 LongAdder 分片思路

putVal 完整流程(JDK8)

final V putVal(K key, V value, boolean onlyIfAbsent) {
    // ① hash 计算
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        
        // ② 空表初始化(CAS 竞争)
        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, null)))
                break;   // CAS 成功,跳出
        }
        // ④ FORWARD_NODE:正在扩容,协助或等待
        else if ((fh = f.hash) == MOVED) {
            helpTransfer(tab, f);
            continue;
        }
        // ⑤ 桶非空:synchronized 桶头
        else {
            V oldVal = null;
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    if (binCount++ >= TREEIFY_THRESHOLD) {
                        // 树化
                        if (fh >= 0) treeifyBin(tab, i);
                    }
                    else if (fh >= 0) {
                        // 链表尾插
                        for (Node<K,V> e = null, p = f;; p = p.next) {
                            if (p == null) {
                                f.next = new Node<K,V>(hash, key, value, null);
                                break;
                            }
                            else if (p.hash == hash &&
                                     ((k = p.key) == key ||
                                      (key != null && key.equals(k))))
                                break;
                        }
                    }
                    else
                        ((TreeNode<K,V>)f).putTreeVal(this, tab, hash, key, value);
                }
            }
        }
    }
    addCount(1L, null);
    return null;
}
putVal 五分支流程:
put(key, value)
  │
  ├─ 桶为空 ──→ CAS 写入(无锁,fast path)
  │
  ├─ FORWARD_NODE ──→ helpTransfer 或等待扩容
  │
  └─ 桶非空 ──→ synchronized(桶头)
                 ├─ key 相同 → 覆盖 value
                 ├─ TREEBIN → 树内 put
                 └─ 链表 → 尾插
  │
  └─ addCount(1L, null) 累加计数

size 统计(LongAdder 思路)

// 类似 LongAdder 的分片累加
private long sumCount() {
    CounterCell[] cs = counterCells;
    long sum = baseCount;
    if (cs != null) {
        for (int i = 0; i < cs.length; ++i) {
            CounterCell c = cs[i];
            if (c != null)
                sum += c.value;
        }
    }
    return sum;
}

// addCount 的分片累加逻辑
final void addCount(long x, CounterCell[] cells) {
    long s;
    CounterCell[] cs;
    CounterCell c;
    int h;
    if ((cells == null && (s = baseCount + x) >= 0 &&
         (cells = counterCells) == null) ||
        // baseCount CAS 无竞争:直接更新
        casBaseCount(baseCount, s = baseCount, s + x)) {
        // 竞争了才分到 counterCells
        if (x != 0 &&
            (cells == null || cells.length == 0 || (cs = counterCells) == null ||
             (c = cs[(h = threadIndex.get()) & (cs.length - 1)]) == null ||
             !c.casValue(c.v, c.v + x))) {
            // 初始化或扩容 counterCells
            fullAddCount(x, cells, cs, c, h);
        }
    }
}
核心思想:
  • 无竞争:baseCount 单变量 CAS 直接更新
  • 有竞争:降级到 counterCells[] 数组分片累加
  • 类似 LongAdder:写性能高,sum() 时遍历累加
  • size() 返回值是"某个瞬间的近似值",不保证强一致

协助扩容 helpTransfer

final Node<K,V>[] transfer(Node<K,V>[] tab, Node<K,V>[] nextTab,
                           Node<K,V>[] firstTab, int index, int bounded) {
    int n = tab.length, stride = (n >>> 2) + (n >>> 3) + 1;  // STRIDE: 每个线程负责 16 个桶(n=16 时)
    if (stride < 16) stride = 16;
    if (index < n) {
        Node<K,V> head = null, tail = null;
        int cl = 0;
        do {
            if (tabAt(tab, i) == null)
                casTabAt(tab, i, null, new ForwardingNode());  // FORWARD_NODE 标记
            else {
                // 遍历链表,按 e.hash & n 拆分到 loHead/hiHead
                // ...
            }
            if ((i += stride) >= n)
                break;
        } while (true);
        return nextTab;
    }
    else {
        // 已迁移完,检查是否可以完成扩容
        if (firstTab != null) {
            if (transferIndex <= 1) {
                table = nextTab;
                sizeCtl = (int)((long)(n << 1)) + 1;
            }
        }
    }
}
协助扩容流程:
resize 触发 → sizeCtl = -n (n 是目标容量,负值表示扩容中)
  │
  ├─ 主线程从 transferIndex (n-1) 开始迁移 16 个桶到 transferIndex-16
  │
  ├─ 已迁移桶用 FORWARD_NODE (hash = MOVED = -1) 标记
  │
  ├─ 其他线程调用 put 时:
  │    ├─ 遇到 FORWARD_NODE → helpTransfer 协助迁移
  │    └─ 读操作可 helpTransfer 或走 find 兜底查新旧两个位置
  │
  └─ 迁移完成 → table = nextTab,sizeCtl 恢复为正
关键点:
  • transferIndex 从右向左递减(避免和 put 的桶定位冲突)
  • 每个线程负责 16 个桶(STRIDE)
  • FORWARD_NODE 是特殊的"已迁移标记"节点
  • 扩容期间读操作不阻塞,通过 helpTransfer 或 find 兜底

弱一致迭代器

public VolatileIterationMap {
    // 迭代器基于快照,允许遍历时修改,不抛 CME
    public final V[] copyEntries() {
        // ...
    }
}
特点:
  • 基于桶数组快照,遍历时允许并发修改
  • 不抛 ConcurrentModificationException
  • 可能读到修改前后的任意快照
  • 适合读多写少的高并发统计场景

JDK8 为什么选 synchronized 不选 ReentrantLock

技术演进:
版本synchronized 性能ReentrantLock 优势
JDK5 及以前只有重量级锁,性能差 10xAQS 无竞争 CAS,性能优
JDK6引入锁升级(偏向锁→轻量级锁→重量级锁),性能追平略优
JDK7与 ReentrantLock 相当tryLock/公平锁/可中断等高级特性
JDK8完全等价桶级锁语义简单,synchronized 更合适
为什么 JDK7 必须用 ReentrantLock:
  • Segment 需要 tryLock() 非阻塞尝试加锁(避免死等)
  • 需要 lockInterruptibly() 可中断
  • 需要公平锁模式(可选)
  • 需要 lock(long, TimeUnit) 超时加锁
为什么 JDK8 可以切回 synchronized:
  • 技术演进:JDK6+ 锁升级让 synchronized 性能追平甚至超过 ReentrantLock
  • 场景匹配:桶级锁竞争极短(微秒级),偏向锁/轻量级锁快速路径正好命中
  • 实现简洁:无对象分配、无状态机、无手动 unlock 遗漏风险
  • JIT 优化:逃逸分析可消除 synchronized(lock elimination)、锁粗化;ReentrantLock 是库实现无法穿透
  • 内存占用:synchronized 用 Mark Word 内联存储锁信息(0 额外对象);ReentrantLock 要 new 对象 + AQS state + 队列
  • JDK8 桶级锁不需要 ReentrantLock 的高级特性:无死等风险、无需可中断、无需公平锁
锁升级四条路径(JDK6+):
无锁状态(Mark Word 存 hashCode)
    ↓ 第一次加锁
偏向锁(Mark Word 存线程 ID,无竞争 CPU 直返,零开销)
    ↓ 出现第二个线程竞争
轻量级锁(CAS 尝试,失败则自旋最多 10 次)
    ↓ 竞争激烈或自旋失败
重量级锁(真实 OS mutex,线程 park)
JDK15 变化:JDK15 默认禁用偏向锁(JEP 374);JDK18 彻底移除偏向锁。原因:偏向锁撤销成本高(全局 safepoint + 重偏向),现代 JIT 已能更好地处理无竞争锁。

与 synchronized 和 ReentrantLock 的定位

场景推荐
默认选择synchronized(简洁、JIT 友好)
需要 tryLock() 非阻塞ReentrantLock
需要可中断ReentrantLock.lockInterruptibly()
需要公平锁ReentrantLock(true)
需要超时加锁ReentrantLock.tryLock(timeout, unit)
需要多个 ConditionReentrantLock.newCondition()
读多写少ReadWriteLock

STEP 4 · 简化

一句话总结:JDK8 ConcurrentHashMap = 桶级 synchronized + CAS + volatile 桶头 + LongAdder 分片计数 + 多线程协助扩容;选 synchronized 是因为 JDK6+ 锁升级追平 ReentrantLock 且语义更简单。

延伸追问

ConcurrentHashMap JDK7 vs JDK8 的锁结构有什么变化?为什么?
JDK7 Segment(默认 16 个)→ JDK8 桶头 synchronized。Segment 是 JDK5/6 时代的产物,写多场景是瓶颈;桶级锁粒度更细;JDK6+ synchronized 已优化,性能相当
ConcurrentHashMap 的 size() 为什么可能不准?
baseCount + counterCells[] 分片累加,size() 遍历各分片时其他线程可能正在更新;返回值是"某个瞬间的近似值"。如果需要强一致,用 getMap().values() 之类的快照
computeIfAbsent 在 ConcurrentHashMap 上有什么特殊行为?
桶为空 CAS 写入;桶非空锁住桶头,计算+插入在同一锁内完成。注意:在 computeIfAbsent 的函数中不能再调用同一个 map 的其他方法(会死锁),也不能返回 null
JDK8 扩容时多线程如何协作?
transferIndex 从 table.length-1 递减,每次减 STRIDE=16;每个线程从 transferIndex 处开始迁移 16 个桶;已迁移桶用 FORWARD_NODE 标记;其他线程调用 put 时如果发现 FORWARD_NODE,调 helpTransfer 协助迁移或 find 兜底
什么时候必须用 ReentrantLock 而不是 synchronized?
需要 tryLock() 非阻塞;需要 lockInterruptibly() 可中断;需要公平锁;需要多个 Condition 条件变量;需要 lock(long, TimeUnit) 超时加锁
为什么 JDK8 桶级锁不需要 ReentrantLock 的高级特性?
桶级锁竞争极短(微秒级),无阻塞死等风险;不需要可中断(迁移是短任务);不需要公平锁(吞吐优先);单个桶一个条件就够了
ConcurrentHashMap 和 Collections.synchronizedMap 怎么选?
高并发选 ConcurrentHashMap;低并发简单场景用 synchronizedMap(但迭代仍需外层加锁)

速查表

JDK7 结构:16 个 Segment(ReentrantLock + 小表)
JDK8 结构:Node[] table(volatile)+ 桶头 synchronized + CAS

putVal 五分支:
  空桶 → CAS 无锁写入
  FORWARD_NODE → helpTransfer
  key 相同 → synchronized 覆盖
  TREEBIN → synchronized 树内 put
  链表 → synchronized 尾插

size 统计:baseCount CAS + counterCells[] 分片(LongAdder 思路)
  → 返回值是"近似值"

协助扩容:
  transferIndex 从右向左,STRIDE=16/线程
  FORWARD_NODE 标记已迁移
  读操作 helpTransfer 或 find 兜底

弱一致迭代:
  基于快照,遍历中修改不抛 CME

JDK8 选 synchronized 的 5 大原因:
  ① JDK6+ 锁升级追平 ReentrantLock
  ② 桶级锁竞争短,偏向锁/轻量级锁快速路径正好命中
  ③ 语义简洁(无手动 unlock)
  ④ JIT 优化友好(逃逸分析、锁消除、锁粗化)
  ⑤ 内存占用小(Mark Word 内联 vs AQS 队列)

关键常量:
  TREEIFY_THRESHOLD=8, MIN_TREEIFY_CAPACITY=64
  MOVED hash=-1 (FORWARD_NODE)
  MAXIMUM_CAPACITY=1<<30, DEFAULT_INITIAL_CAPACITY=16

关联题目

  • [ ] 《ConcurrentHashMap是如何保证线程安全的?》— 2026-10-09 Round 1 Q8, ⭐⭐⭐(完全没提 CAS/volatile/协助扩容/LongAdder 分片计数)
  • [ ] 《ConcurrentHashMap为什么在JDK 1.8中使用synchronized而不是ReentrantLock》— 2026-10-09 Round 1 Q9, ⭐⭐⭐⭐(讲清 JDK6 锁升级核心,但未讲 JDK7 为什么用 ReentrantLock)

关联知识

JDK8 CHM = 桶级 synchronized + CAS + volatile 桶头 + LongAdder 分片计数 + 多线程协助扩容;去掉了 JDK7 的 Segment,并发度从 16 提升到表容量
图书馆拆掉大阅览室:每格书架独立上锁;空格子用智能钥匙(CAS)直接放;统计书数用分片累加(LongAdder);扩容时多个管理员一起搬书
✦ 记 忆 口 诀 ✦
空桶 CAS 无锁;非空 synchronized 桶头;size 分片累加;扩容 transferIndex 从右向左 STRIDE=16
关键可视化
putVal 五分支流程
flowchart TB
  START["put(key, value)"] --> HASH["spread(key.hashCode())"]
  HASH --> T{"table 空?"}
  T -- 是 --> INIT["initTable<br/>CAS 初始化"]
  T -- 否 --> EMPTY{"桶为空?"}
  EMPTY -- 是 --> CAS["① CAS 写入<br/>casTabAt(tab, i, null, newNode)<br/>无锁 fast path"]
  EMPTY -- 否 --> FORWARD{"FORWARD_NODE?"}
  FORWARD -- 是 --> HELP["② helpTransfer<br/>协助扩容"]
  FORWARD -- 否 --> SYNC["③ synchronized(桶头)"]
  SYNC --> SPLIT{"节点类型"}
  SPLIT -- key 相同 --> OVER["覆盖 value"]
  SPLIT -- TREEBIN --> TREE["树内 put"]
  SPLIT -- 链表 --> TAIL["尾插新节点<br/>链长≥8 转树"]
  CAS --> COUNT["addCount(1L)<br/>LongAdder 分片"]
  OVER --> COUNT
  TREE --> COUNT
  TAIL --> COUNT
size 统计:LongAdder 分片累加
flowchart TB
  ADD["addCount(1L)"] --> C1{"baseCount<br/>CAS 无竞争?"}
  C1 -- 是 --> UP["直接更新 baseCount"]
  C1 -- 否 --> C2["竞争<br/>降级到 counterCells[]"]
  C2 --> CELL["选 counterCells[threadIndex & (len-1)]"]
  CELL --> C3{"cell CAS 成功?"}
  C3 -- 是 --> DONE1["cell.value += 1"]
  C3 -- 否 --> C4["初始化/扩容 counterCells<br/>fullAddCount"]
  DONE1 --> SUM["sumCount()<br/>= baseCount + sum(counterCells[i].value)"]
  UP --> SUM
  SUM --> NOTE["返回值是'某个瞬间的近似值'<br/>不保证强一致"]
协助扩容 helpTransfer
flowchart TB
  START["resize 触发"] --> SET["sizeCtl = -n<br/>负值表示扩容中<br/>transferIndex = n-1"]
  SET --> WORK["主线程从 n-1 开始<br/>每线程负责 16 个桶 STRIDE"]
  WORK --> FORWARD["已迁移桶用 FORWARD_NODE<br/>(hash = MOVED = -1)标记"]
  FORWARD --> OTHER{"其他线程调用 put/get?"}
  OTHER -- put --> CHECK{"遇到 FORWARD_NODE?"}
  CHECK -- 是 --> HELP["helpTransfer<br/>协助迁移"]
  CHECK -- 否 --> NORMAL["正常 put"]
  OTHER -- get --> FIND["find 兜底<br/>查新旧两个位置"]
  HELP --> END["transferIndex <= 1<br/>table = nextTab<br/>sizeCtl = n<<1 + 1"]
  NORMAL --> END
  FIND --> END
JDK7 Segment vs JDK8 桶级锁
flowchart LR
  subgraph JDK7["JDK7 ConcurrentHashMap"]
    J1["16 个 Segment"]
    J2["每个 Segment = ReentrantLock<br/>+ 小 HashEntry 表"]
    J3["并发度上限:16"]
    J4["需要 tryLock/lockInterruptibly<br/>公平锁等高级特性"]
    J5["内存:Segment 数组 + 16 把锁"]
  end
  subgraph JDK8["JDK8 ConcurrentHashMap"]
    K1["Node[] table<br/>+ volatile 桶头"]
    K2["桶级 synchronized + CAS"]
    K3["并发度 = 表容量"]
    K4["桶级锁语义简单<br/>无需 ReentrantLock 高级特性"]
    K5["LongAdder 分片计数<br/>协助扩容"]
  end
  JDK7 -.演进.-> JDK8
JDK8 为什么选 synchronized 不用 ReentrantLock?
flowchart TB
  WHY["5 大原因"] --> R1["① JDK6+ 锁升级<br/>偏向锁→轻量级→重量级<br/>性能追平 ReentrantLock"]
  WHY --> R2["② 桶级锁竞争短(微秒级)<br/>偏向锁/轻量级锁快速路径<br/>正好命中"]
  WHY --> R3["③ 语义简洁<br/>自动加解锁<br/>无 unlock 遗漏风险"]
  WHY --> R4["④ JIT 优化友好<br/>逃逸分析消除锁、锁粗化<br/>ReentrantLock 无法穿透"]
  WHY --> R5["⑤ 内存占用小<br/>Mark Word 内联<br/>vs AQS 队列要额外对象"]
  WHY2["JDK7 为什么用 ReentrantLock?"] --> R6["Segment 需要 tryLock 非阻塞<br/>lockInterruptibly 可中断<br/>公平锁 超时加锁<br/>都是 ReentrantLock 独有"]
JDK6 synchronized 锁升级四条路径
flowchart LR
  L0["无锁状态<br/>Mark Word 存 hashCode"] --> L1["偏向锁<br/>Mark Word 存线程 ID<br/>无竞争 CPU 直返<br/>零开销"]
  L1 --> L2["轻量级锁<br/>CAS 尝试<br/>失败自旋最多 10 次"]
  L2 --> L3["重量级锁<br/>真实 OS mutex<br/>线程 park"]
  L3 -.单向升级不可逆.-> END["JDK15 默认禁用偏向锁<br/>JDK18 彻底移除"]
知识关系

🔄 延伸(Extends)

red-black-tree

⚡ 对比(Contrast)

synchronized vs ReentrantLock— JDK8 CHM 选 synchronized 是因为 JDK6 锁升级追平 ReentrantLock,语义简洁 + JIT 友好JDK7 用 ReentrantLock— JDK7 Segment 需要 tryLock/lockInterruptibly/公平锁等 ReentrantLock 独有特性
🎯 概念 📏 规则 ⚠️ 误区 🔍 追问 ✨ 口诀 共 0 张卡,点击翻面