---
title: ConcurrentHashMap 并发机制与 synchronized 决策
type: concept
domain: java-concurrency
tags: [concurrenthashmap, jdk8, cas, volatile, synchronized, reentrantlock, transfer-index, longadder, basecount, countercells]
status: learning
created: 2026-10-09
last_reviewed: 2026-10-09
method: code-reading + feynman
related_questions:
  - "ConcurrentHashMap是如何保证线程安全的？"
  - "ConcurrentHashMap为什么在JDK 1.8中使用`synchronized`而不是`ReentrantLock`"
related_knowledge:
  - ./hash-map-evolution.md
  - ./jmm-volatile-cas.md
  - ./synchronized-mechanism.md
  - ./aqs-mechanism.md
anki_cards: 8
interview_rounds:
  - "2026-10-09-round-1-Q8"
  - "2026-10-09-round-1-Q9"
---

# ConcurrentHashMap 并发机制与 synchronized 决策

> JDK8 用"CAS + synchronized + volatile"混合替代 JDK7 的"Segment + ReentrantLock"，把并发度从 16 提升到表大小；size 统计用 LongAdder 思路分片累加；协助扩容让多线程一起迁移桶。

## 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 队列开销

## 关键结论

- **结论 A**：JDK8 ConcurrentHashMap 核心是"桶级 synchronized + CAS 混合"——空桶 CAS 无锁，非空桶锁桶头
- **结论 B**：读操作走 volatile 完全无锁，写冲突粒度只锁一个桶，读多写少场景性能极好
- **结论 C**：size() 返回值是"某个瞬间的近似值"，不保证强一致（LongAdder 分片累加）
- **结论 D**：JDK8 选 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 结构

```java
// 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 结构

```java
// 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）

```java
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 思路）

```java
// 类似 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

```java
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 兜底

#### 弱一致迭代器

```java
public VolatileIterationMap {
    // 迭代器基于快照，允许遍历时修改，不抛 CME
    public final V[] copyEntries() {
        // ...
    }
}
```

**特点**：
- 基于桶数组快照，遍历时允许并发修改
- 不抛 `ConcurrentModificationException`
- 可能读到修改前后的任意快照
- 适合读多写少的高并发统计场景

#### JDK8 为什么选 synchronized 不选 ReentrantLock

**技术演进**：

| 版本 | synchronized 性能 | ReentrantLock 优势 |
|------|-----------------|-------------------|
| JDK5 及以前 | 只有重量级锁，性能差 10x | AQS 无竞争 CAS，性能优 |
| JDK6 | 引入锁升级（偏向锁→轻量级锁→重量级锁），性能追平 | 略优 |
| JDK7 | 与 ReentrantLock 相当 | tryLock/公平锁/可中断等高级特性 |
| JDK8 | 完全等价 | 桶级锁语义简单，synchronized 更合适 |

**为什么 JDK7 必须用 ReentrantLock**：
- Segment 需要 `tryLock()` 非阻塞尝试加锁（避免死等）
- 需要 `lockInterruptibly()` 可中断
- 需要公平锁模式（可选）
- 需要 `lock(long, TimeUnit)` 超时加锁

**为什么 JDK8 可以切回 synchronized**：
1. **技术演进**：JDK6+ 锁升级让 synchronized 性能追平甚至超过 ReentrantLock
2. **场景匹配**：桶级锁竞争极短（微秒级），偏向锁/轻量级锁快速路径正好命中
3. **实现简洁**：无对象分配、无状态机、无手动 unlock 遗漏风险
4. **JIT 优化**：逃逸分析可消除 synchronized（lock elimination）、锁粗化；ReentrantLock 是库实现无法穿透
5. **内存占用**：synchronized 用 Mark Word 内联存储锁信息（0 额外对象）；ReentrantLock 要 new 对象 + AQS state + 队列
6. **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)` |
| 需要多个 Condition | `ReentrantLock.newCondition()` |
| 读多写少 | `ReadWriteLock` |

### STEP 4 · 简化

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

## 常见误区

- **误区 1**：ConcurrentHashMap 强一致 → 错，size() 返回值是近似值，读操作返回快照
- **误区 2**：`computeIfAbsent` 内部可以调用其他 Map 方法 → 会死锁，JDK8 桶级 synchronized 保证同一桶只有一个线程操作
- **误区 3**：ConcurrentHashMap 允许 null key/value → 错，key/value 都不允许 null（会 NPE），因为无法区分"key 不存在"和"key 映射到 null"
- **误区 4**：JDK8 ConcurrentHashMap 用 ReentrantLock → 错，用 synchronized（JDK7 用 ReentrantLock）
- **误区 5**：协助扩容会阻塞读操作 → 错，读操作走 helpTransfer 或 find 兜底，几乎不阻塞

## 延伸追问

1. **ConcurrentHashMap JDK7 vs JDK8 的锁结构有什么变化？为什么？**
   - JDK7 Segment（默认 16 个）→ JDK8 桶头 synchronized。Segment 是 JDK5/6 时代的产物，写多场景是瓶颈；桶级锁粒度更细；JDK6+ synchronized 已优化，性能相当

2. **ConcurrentHashMap 的 size() 为什么可能不准？**
   - `baseCount` + `counterCells[]` 分片累加，size() 遍历各分片时其他线程可能正在更新；返回值是"某个瞬间的近似值"。如果需要强一致，用 `getMap().values()` 之类的快照

3. **`computeIfAbsent` 在 ConcurrentHashMap 上有什么特殊行为？**
   - 桶为空 CAS 写入；桶非空锁住桶头，计算+插入在同一锁内完成。注意：在 computeIfAbsent 的函数中不能再调用同一个 map 的其他方法（会死锁），也不能返回 null

4. **JDK8 扩容时多线程如何协作？**
   - `transferIndex` 从 table.length-1 递减，每次减 STRIDE=16；每个线程从 transferIndex 处开始迁移 16 个桶；已迁移桶用 FORWARD_NODE 标记；其他线程调用 put 时如果发现 FORWARD_NODE，调 helpTransfer 协助迁移或 find 兜底

5. **什么时候必须用 ReentrantLock 而不是 synchronized？**
   - 需要 tryLock() 非阻塞；需要 lockInterruptibly() 可中断；需要公平锁；需要多个 Condition 条件变量；需要 lock(long, TimeUnit) 超时加锁

6. **为什么 JDK8 桶级锁不需要 ReentrantLock 的高级特性？**
   - 桶级锁竞争极短（微秒级），无阻塞死等风险；不需要可中断（迁移是短任务）；不需要公平锁（吞吐优先）；单个桶一个条件就够了

7. **ConcurrentHashMap 和 Collections.synchronizedMap 怎么选？**
   - 高并发选 ConcurrentHashMap；低并发简单场景用 synchronizedMap（但迭代仍需外层加锁）

## 速查表（面试前 60 秒扫完）

```
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）

## 关联知识

- [HashMap 内部机制与 JDK7→JDK8 演进](./hash-map-evolution.md)
- [JMM 与 volatile/CAS 底层机制](./jmm-volatile-cas.md)
- [synchronized 实现与三特性](./synchronized-mechanism.md)
- [AQS 四大核心机制](./aqs-mechanism.md)

## Anki 候选卡片

1. **正**：JDK8 ConcurrentHashMap 的 putVal 五分支？
   **反**：① 空桶 CAS 无锁写入；② FORWARD_NODE 协助扩容；③ key 相同 synchronized 覆盖；④ TREEBIN 树内 synchronized put；⑤ 链表尾插 synchronized

2. **正**：ConcurrentHashMap 的 size() 统计机制？
   **反**：`baseCount` CAS 无竞争直接更新 + `counterCells[]` 分片累加（LongAdder 思路）；size() 返回"近似值"

3. **正**：JDK8 ConcurrentHashMap 协助扩容的机制？
   **反**：`transferIndex` 从右向左递减，每个线程 STRIDE=16 桶；已迁移桶用 FORWARD_NODE (hash=-1) 标记；其他线程 helpTransfer 协助或 find 兜底

4. **正**：JDK8 ConcurrentHashMap 为什么用 synchronized 不用 ReentrantLock？
   **反**：① JDK6+ 锁升级追平 ReentrantLock；② 桶级锁竞争短，偏向/轻量级快速路径正好命中；③ 语义简洁无手动 unlock；④ JIT 优化友好（逃逸分析、锁消除）；⑤ 内存占用小（Mark Word 内联 vs AQS 队列）

5. **正**：JDK7 为什么必须用 ReentrantLock？
   **反**：Segment 需要 tryLock() 非阻塞、lockInterruptibly() 可中断、公平锁选项、超时加锁——都是 ReentrantLock 独有的高级特性

6. **正**：JDK6 synchronized 锁升级四条路径？
   **反**：无锁 → 偏向锁（Mark Word 存线程 ID） → 轻量级锁（CAS 自旋） → 重量级锁（OS mutex）；升级单向不可逆

7. **正**：ConcurrentHashMap 允许 null 吗？
   **反**：key 和 value 都不允许 null（会 NPE），因为无法区分"key 不存在"和"key 映射到 null"

8. **正**：`computeIfAbsent` 在 ConcurrentHashMap 上的关键约束？
   **反**：桶为空 CAS 写入；桶非空 synchronized 桶头保证原子性；**注意**：不能在里面调用同一个 Map 的其他方法（死锁），也不能返回 null

---

*最后更新：2026-10-09*
