CurrentHashMap1.7中segement的扩容过程
JDK 1.7 ConcurrentHashMap 的 Segment 扩容:通俗详解
一、先打个比方:一排带锁的抽屉柜
想象你有一排 16 个带独立锁的抽屉柜(这就是 16 个 Segment):
┌─────────┬─────────┬─────────┬─────────┐
│ 柜子 0 │ 柜子 1 │ 柜子 2 │ ... │
│ 🔒 │ 🔒 │ 🔒 │ │
│ [][ ] │ [][ ] │ [][ ] │ │ ← 每个柜子里有若干小格子(HashEntry 数组)
│ [][ ] │ [][ ] │ [][ ] │ │
└─────────┴─────────┴─────────┴─────────┘
每个柜子独立上锁、独立管理。一个柜子满了,只换这个柜子,其他柜子该干嘛干嘛。
二、Segment 里面到底是什么?
每个 Segment 本质上就是一个缩小版的 HashMap:
// 简化版结构
static final class Segment<K,V> extends ReentrantLock {
transient volatile HashEntry<K,V>[] table; // 小数组,存数据
transient int count; // 这个 Segment 里有多少元素
transient int threshold; // 扩容阈值 = 容量 × 负载因子
final float loadFactor; // 负载因子(默认 0.75)
}
关键点:每个 Segment 有自己的 table 数组、自己的 count、自己的 threshold。
三、什么时候触发扩容?
假设柜子 3 的配置是:
- 小格子数量(容量):4 个
- 负载因子:0.75
- 扩容阈值:
4 × 0.75 = 3
柜子 3 的内部:
┌─────┬─────┬─────┬─────┐
│ [A] │ [B] │null │ [C] │ ← 已经有 3 个元素了
└─────┴─────┴─────┴─────┘
这时候线程甲往柜子 3 里再塞一个元素 D:
count = 4 > threshold = 3 → 触发扩容!
注意:只有柜子 3 满了,柜子 0、1、2、4...15 完全没感觉。
四、扩容的具体过程(只锁这一个 Segment)
第一步:抢锁
// 线程甲要对 Segment[3] 扩容,先拿到这个 Segment 的独占锁
segment.lock(); // 其他想操作柜子 3 的线程阻塞等待
其他线程:
- 想操作柜子 3 的 → 排队等锁
- 想操作柜子 0、1、2、4...的 → 直接进去,完全不受影响
第二步:创建新数组(容量翻倍)
旧数组:4 个格子
新数组:8 个格子(2 倍)
第三步:Rehash + 迁移数据
这是最关键的一步。每个元素要重新算位置,因为数组长度变了,hash 取模的结果也变了。
旧数组(4格) 新数组(8格)
┌─────┬─────┬─────┬─────┐ ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│ [A] │ [B] │null │ [C] │ → │ [A] │null │ [B] │null │null │null │ [C] │null │
└─────┴─────┴─────┴─────┘ └─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
假设:
- A 的 hash % 4 = 0,hash % 8 = 0 → 还在 0 号位
- B 的 hash % 4 = 1,hash % 8 = 2 → 搬到 2 号位
- C 的 hash % 4 = 3,hash % 8 = 6 → 搬到 6 号位
迁移时是倒序摘链:
旧位置 0: A → D → null
↓
新位置 0: D → A → null (头插法,顺序反转)
第四步:替换引用,释放锁
table = newTable; // 把旧数组指向新数组
threshold = newThr; // 更新阈值
segment.unlock(); // 释放锁,其他线程可以进来操作了
五、一张图看完全过程
扩容前:Segment[3] 满了,其他 Segment 正常运作
┌─────────┬─────────┬─────────────────┬─────────┐
│ 柜子 0 │ 柜子 1 │ 柜子 3 🔒 │ 柜子 7 │
│ 正常读写 │ 正常读写 │ 【正在扩容】 │ 正常读写 │
│ │ │ 旧: [A][B][_][C]│ │
│ │ │ 新: [A][_][B][_] │ │
│ │ │ [_][_][C][_] │ │
└─────────┴─────────┴─────────────────┴─────────┘
↑
其他 Segment 的线程完全不知道发生了扩容,
它们连锁都不用抢,直接读写自己的 Segment。
六、为什么 1.7 这样设计的好处?
| 设计 | 效果 |
|---|---|
| Segment 独立扩容 | 不会"牵一发而动全身",只动一个柜子 |
| 只锁一个 Segment | 其他 15 个 Segment 并发度不受影响 |
| 每个 Segment 自己的阈值 | 按需扩容,不会浪费内存 |
| Rehash 范围小 | 只迁移一个 Segment 的数据,不是全表迁移 |
七、和 1.8 的对比(加深理解)
| JDK 1.7 | JDK 1.8 | |
|---|---|---|
| 结构 | Segment 数组 → 每个 Segment 里有 HashEntry 数组 | 直接一个大 Node 数组 |
| 锁 | 锁整个 Segment(可能包含多个 bucket) | 锁单个 bucket 的头节点 |
| 扩容 | 单个 Segment 独立扩容 | 整个数组一起扩容(全表迁移) |
| 并发度 | 最多 16(默认 Segment 数) | 理论上数组长度就是并发度 |
1.7 的"劣势":
- Segment 数组大小一旦定死(默认 16),并发度就上限了。
- 如果数据都集中在一个 Segment 里,那跟单线程 HashMap 差不多。
1.8 为什么改成全表扩容?
- 因为 1.8 锁粒度细到了 bucket 级别,扩容时可以用 ForwardingNode 做渐进式迁移,配合 transferIndex 多线程协作,反而比 1.7 的 Segment 扩容更高效。
八、一句话总结
JDK 1.7 的 Segment 扩容就像:一排带锁的储物柜,某个柜子塞满了,管理员只打开那一个柜子的锁,把里面的东西搬到更大的新柜子里。隔壁柜子的人该存存、该取取,完全不受影响。每个柜子有自己的"满了吗"标准,满了就各自独立换大柜子。

浙公网安备 33010602011771号