ConcurrentHashMap分段锁机制解析

ConcurrentHashMap通过分段锁+Segment设计实现了高并发性能,与HashTable的全对象加锁形成鲜明对比。本文用图示简洁解析ConcurrentHashMap的核心机制,以及为什么HashTable性能不佳。

前言

上篇说了 HashMap 线程不安全,那线程安全的 Map 怎么选?

HashTable 太慢,synchronized 包装也一般,ConcurrentHashMap 才是王道。

今天简单聊聊 ConcurrentHashMap 的核心设计思想。

HashTable 的性能问题

全对象加锁的弊端

HashTable 的线程安全实现很粗暴:

public synchronized V put(K key, V value) {
    // 整个方法加锁
}

public synchronized V get(Object key) {
    // 整个方法加锁
}
​

这导致的问题:

HashTable并发访问示意图:

时刻1:
┌─────────────────────────────────┐
│         HashTable 🔒            │ ← 整个对象被锁住
│ [0] [1] [2] [3] [4] [5] [6] [7] │
└─────────────────────────────────┘
  ↑
线程T1正在put("a", 1)

时刻2:
┌─────────────────────────────────┐
│         HashTable 🔒            │ ← T2-T5都在等待
│ [0] [1] [2] [3] [4] [5] [6] [7] │
└─────────────────────────────────┘
  ↑                ↑     ↑     ↑
线程T1            T2    T3    T4
put("a",1)       get("x") get("y") put("z",9)
进行中...         等待    等待    等待

结果:即使操作不同位置,也要排队等待!
​

性能瓶颈:

  • 读写互斥:get 时不能 put
  • 写写互斥:同时只能有一个 put
  • 锁粒度太粗:整个表都被锁住
  • 吞吐量低:高并发时大量线程阻塞

ConcurrentHashMap 的分段锁

核心设计理念

ConcurrentHashMap(JDK 1.7)的巧妙之处在于分而治之:

分段锁设计图:

整体结构:
┌─────────┬─────────┬─────────┬─────────┐
│Segment0 │Segment1 │Segment2 │Segment3 │ ← 多个独立的段
│   🔒    │   🔒    │   🔒    │   🔒    │   各自有锁
└─────────┴─────────┴─────────┴─────────┘

每个Segment内部:
Segment0:
┌───┬───┬───┬───┐
│[0]│[1]│[2]│[3]│ ← 类似小HashMap
├───┼───┼───┼───┤
│ A → B │ C │ D │ ← 链表结构
└───┴───┴───┴───┘

Segment1:
┌───┬───┬───┬───┐
│[4]│[5]│[6]│[7]│
├───┼───┼───┼───┤
│ E │ F │ G │ H │
└───┴───┴───┴───┘
​

并发访问优势

ConcurrentHashMap并发访问:

同一时刻可以有多个操作:
┌─────────┬─────────┬─────────┬─────────┐
│Segment0 │Segment1 │Segment2 │Segment3 │
│   🔒    │         │   🔒    │         │
└─────────┴─────────┴─────────┴─────────┘
    ↑                   ↑
  T1操作              T3操作
  put("a",1)          put("x",9)
  
时刻分析:
T1: put("a",1) → hash到Segment0 → 锁住Segment0
T2: get("b")   → hash到Segment1 → 无锁直接读
T3: put("x",9) → hash到Segment2 → 锁住Segment2  
T4: get("y")   → hash到Segment3 → 无锁直接读

结果:4个线程可以并发执行!
​

性能提升:

  • 锁粒度细化:只锁相关的 Segment
  • 读写分离:读操作无锁(大部分情况)
  • 并发度可控:Segment 数量决定最大并发数
  • 热点分散:不同 key 分布在不同段

JDK 1.8 的改进

JDK 1.8 对 ConcurrentHashMap 做了重大改进:

JDK 1.7 vs JDK 1.8:

JDK 1.7 (Segment分段锁):
┌─────────┬─────────┬─────────┬─────────┐
│Segment0 │Segment1 │Segment2 │Segment3 │ ← 固定段数
│   🔒    │   🔒    │   🔒    │   🔒    │   并发度受限
└─────────┴─────────┴─────────┴─────────┘

JDK 1.8 (CAS + synchronized):
┌─────┬─────┬─────┬─────┬─────┬─────┐
│  0  │  1  │  2  │  3  │  4  │  5  │ ← 数组更大
│     │ 🔒  │     │ 🔒  │     │     │   锁更细粒度
└─────┴─────┴─────┴─────┴─────┴─────┘
        ↑           ↑
    只锁链表头    只锁链表头

优势:
- 取消Segment概念,直接锁数组元素
- 并发度等于数组长度,理论上更高
- 结合CAS操作,减少锁竞争
- 支持红黑树优化
​

性能对比总结

三种Map的并发性能对比:

并发读取:
┌────────────┬──────────┬──────────┬───────────────┐
│    操作    │HashTable │ JDK1.7   │   JDK1.8     │
│           │          │ConcHashMap│ ConcHashMap  │
├────────────┼──────────┼──────────┼───────────────┤
│ 4个线程读  │    🔒    │   ✅✅   │    ✅✅✅    │
│ 并发度     │    1     │    4     │     更高      │
│ 锁竞争     │   严重   │   较少   │     最少      │
└────────────┴──────────┴──────────┴───────────────┘

并发写入:
HashTable:    [T1] → [T2] → [T3] → [T4]  (串行)
ConcHashMap:  [T1,T3] + [T2,T4]          (分段并行)
JDK1.8版:     [T1,T2,T3,T4]              (更细粒度)
​

适用场景

选择指南:

HashMap:
✅ 单线程
✅ 确定无并发
❌ 多线程环境

HashTable:
✅ 简单的线程安全需求
✅ 读写操作都很少
❌ 高并发场景

ConcurrentHashMap:
✅ 高并发读写
✅ 读多写少场景
✅ 对性能有要求的多线程应用
❌ 单线程(有额外开销)
​

小结

ConcurrentHashMap 的核心思想是分治:

JDK 1.7 的 Segment 分段锁:

  • 将大 Map 分成多个小 Segment
  • 每个 Segment 独立加锁
  • 大幅提升并发度

JDK 1.8 的优化:

  • 取消 Segment,直接锁数组元素
  • CAS + synchronized 混合使用
  • 并发度更高,性能更好

与 HashTable 的区别:

  • HashTable:全对象锁,并发度=1
  • ConcurrentHashMap:分段锁,并发度=段数

性能提升的关键:

  • 锁粒度细化
  • 读写分离
  • 热点分散
  • 无锁优化

这就是为什么高并发场景下,ConcurrentHashMap 是首选的线程安全 Map 实现。

更多推荐

章节目录