面试知识库
极高 困难

ConcurrentHashMap原理#

一句话答案#

JDK8 用 CAS + synchronized 锁单个 Node 实现并发安全,数组+链表+红黑树结构,锁粒度细化到桶级别,不再受固定 Segment 数限制。

核心要点

JDK 7:Segment 分段锁

ConcurrentHashMap
├── Segment[0](继承 ReentrantLock)
│   └── HashEntry[] table(小 HashMap)
├── Segment[1](继承 ReentrantLock)
│   └── HashEntry[] table
...
└── Segment[15](默认 16 个 Segment)
plaintext
  • 将整个 Map 分为 N 个 Segment(默认16),每个 Segment 独立加锁
  • 不同 Segment 的操作可以并发,同一 Segment 串行
  • 并发度 = Segment 数量 = 16

JDK 8:CAS + synchronized(桶级别细粒度锁)

底层结构与 HashMap 一致:Node[] table + 链表/红黑树
关键变化:
  - 锁粒度从 Segment 细化到单个桶(数组槽)
  - 初始化:CAS 保证 table 只被初始化一次
  - 空桶插入:CAS 无锁插入
  - 非空桶操作:synchronized 锁住桶的头节点
plaintext

JDK 8 的改进:

维度JDK 7JDK 8
锁机制ReentrantLock(Segment级)synchronized(桶级)+ CAS
锁粒度一个 Segment(多个桶)单个桶(最细粒度)
并发度固定(Segment数量,默认16)锁粒度为单个桶,不再有固定上限(实际受热点桶冲突、扩容等影响)
数据结构数组 + 链表数组 + 链表 + 红黑树
size()先不加锁尝试,失败再全Segment加锁CounterCell(LongAdder思想)

JDK 8 的 CAS 操作:

// 空桶插入:无锁
if (casTabAt(tab, i, null, new Node<>(hash, key, value)))
    break;

// 非空桶插入:加锁(锁桶头节点)
synchronized (f) {  // f = table[i]
    // 在链表/红黑树中操作
}
java

为什么 JDK 8 用 synchronized 而不是 ReentrantLock JDK 6 后 synchronized 经过大量优化(偏向锁/轻量级锁/锁升级),在竞争不激烈时性能与 ReentrantLock 相当,且 JVM 能更好地优化 synchronized(如锁消除、锁粗化)。

面试回答(2分钟版)

ConcurrentHashMap是线程安全的HashMap,JDK7和JDK8的实现方案有本质区别。JDK7采用Segment分段锁机制,将整个Map分成默认16个Segment,每个Segment继承ReentrantLock相当于一个独立的小HashMap,不同Segment可以并发操作,并发度等于Segment数量即16。JDK8做了彻底重构,底层结构与HashMap一致采用数组加链表加红黑树,锁粒度从Segment级别细化到单个桶。具体策略是:对空桶的插入操作使用CAS无锁方式完成,对非空桶的操作则用synchronized锁住桶的头节点Node,这样锁粒度细化到桶级别,不再受固定Segment数量限制,比JDK7提升了一个数量级。JDK8选择synchronized而非ReentrantLock是因为JDK6之后synchronized经过偏向锁、轻量级锁、锁升级等大量优化,在竞争不激烈时性能已经和ReentrantLock持平,且JVM能对synchronized做锁消除和锁粗化等更深层优化。size()方法也做了改进,使用baseCount加CounterCell数组的方式统计,借鉴了LongAdder的分散热点思想,避免了JDK7中多次尝试后全段加锁的开销。

追问与易错

追问方向:

  • “JDK7 和 JDK8 的实现有什么区别?”→ JDK7 用 Segment 分段锁(继承 ReentrantLock),并发度固定 16;JDK8 改为 CAS + synchronized 锁单个桶头节点,锁粒度细化到桶级别
  • “size() 是怎么统计的?”→ 使用 baseCount + CounterCell 数组,借鉴 LongAdder 分散热点思想,CAS 更新 baseCount 失败时分散到 CounterCell 数组,最终 sum 求和
  • “能保证复合操作的原子性吗?”→ 不能,单个方法如 putIfAbsent/computeIfAbsent 是原子的,但多个方法组合(如先 get 再 put)不是原子的,需要自行加锁或使用 compute 系列方法

易错点:

  • ❌ “JDK8 的 ConcurrentHashMap 不用锁了”——synchronized 锁的是链表头节点
  • ❌ 认为 ConcurrentHashMap 的迭代器是强一致的——是弱一致性