ConcurrentHashMap 为什么线程安全?JDK 1.7 和 1.8 有什么区别?

1 阅读5分钟

一、ConcurrentHashMap 为什么线程安全

它能线程安全,主要靠这几种机制共同实现:

1)volatile 保证可见性

ConcurrentHashMap 中很多关键字段都用了 volatile,比如数组引用、节点值等。

作用是:

  • 一个线程修改后,其他线程能马上看到最新值
  • 防止指令重排序带来的并发问题

比如一个线程 put 了新节点,另一个线程 get 时能看到最新结构,而不是旧数据。


2)CAS 保证原子操作

CAS(Compare And Swap)是一种无锁原子操作。

它的思想是:

  • 先比较某个位置当前值是不是预期值
  • 如果是,就更新
  • 如果不是,说明有别的线程改过了,当前线程重试

ConcurrentHashMap 中,很多场景都用了 CAS,例如:

  • 初始化 table
  • 向空桶位置插入节点
  • 扩容时状态控制

这样做的好处是:

  • 避免一上来就加锁
  • 降低线程竞争
  • 提高吞吐量

3)加锁保证复合操作安全

虽然 CAS 很强,但它只适合单个变量的原子更新。

对于链表插入、红黑树调整、扩容迁移这类复杂操作,还是需要加锁。

所以 ConcurrentHashMap 不是完全无锁,而是:

  • 能 CAS 的地方尽量 CAS
  • 必须保护临界区时再加锁

这就是它比 Hashtable 性能高的关键:锁的粒度更细


4)读操作基本不加锁

get() 在大多数情况下是不加锁的。

原因:

  • 节点结构设计保证读取时可见
  • 使用 volatile 保证读取最新值
  • 读线程不会阻塞写线程

所以 ConcurrentHashMap 特别适合“读多写少”或者高并发读场景。


二、JDK 1.7 的 ConcurrentHashMap

JDK 1.7 采用的是 Segment 分段锁 机制。

1)整体结构

可以理解成:

  • 整个 ConcurrentHashMap 被分成若干个 Segment
  • 每个 Segment 里面维护一个 HashEntry[]
  • 每个 Segment 本质上类似一个小型 HashTable

结构可以粗略理解为:

ConcurrentHashMap
   -> Segment[]
       -> Segment
           -> HashEntry[]

2)线程安全怎么实现

JDK 1.7 中:

  • 每个 Segment 继承了 ReentrantLock
  • 写操作时,只锁当前所在的 Segment
  • 不同 Segment 之间互不影响,可以并发执行

这就是所谓的 分段锁

例如:

  • 线程A操作第1段
  • 线程B操作第5段

这两个线程可以同时进行,因为锁不同。


3)优点

相比 Hashtable 整表一把锁,JDK 1.7 的 ConcurrentHashMap 并发能力大大提升。

因为:

  • Hashtable:所有线程竞争同一把锁
  • ConcurrentHashMap 1.7:不同段竞争不同锁

4)缺点

分段锁虽然比整表锁好,但也有问题:

并发度受 Segment 数量限制

默认并发级别有限,不是理论上的无限并发。

结构复杂

Segment + HashEntry[] 两层结构比较重。

扩容效率一般

扩容是基于段进行的,不够灵活。


三、JDK 1.8 的 ConcurrentHashMap

JDK 1.8 对实现做了较大重构,不再使用 Segment 分段锁。

1)整体结构变化

JDK 1.8 结构更像 HashMap

Node[]

桶中元素可能是:

  • 链表
  • 红黑树

也就是:

  • 冲突少时,用链表
  • 冲突严重时,链表转红黑树

这点和 JDK 1.8 的 HashMap 很像。


2)线程安全怎么实现

JDK 1.8 采用的是:

  • CAS + synchronized
  • 锁粒度细化到 桶/bin 级别

具体来说:

当桶为空时

直接用 CAS 插入,不加锁。

当桶不为空时

对该桶头节点加 synchronized 锁,执行链表插入/更新或树化操作。

这意味着:

  • 不是锁整张表
  • 不是锁一大段 Segment
  • 而是锁当前冲突桶

所以并发粒度更细了。


3)读操作

get() 基本还是无锁的,依赖 volatile 和合理的数据结构设计。


4)扩容机制

JDK 1.8 的扩容更先进,支持多线程协助扩容(helpTransfer)。

意思是:

  • 一个线程发现需要扩容时开始迁移
  • 其他线程也可以参与迁移部分数据
  • 提升扩容效率

四、JDK 1.7 和 1.8 的核心区别

下面是最关键的对比:

1)锁模型不同

JDK 1.7

  • 使用 Segment 分段锁
  • 每个 Segment 一把 ReentrantLock

JDK 1.8

  • 使用 CAS + synchronized
  • 锁粒度是桶级别,不再有 Segment

2)数据结构不同

JDK 1.7

  • Segment[] + HashEntry[] + 链表

JDK 1.8

  • Node[] + 链表 + 红黑树

3)并发度不同

JDK 1.7

并发度取决于 Segment 数量。

JDK 1.8

并发控制更细,理论上并发性能更好,尤其在热点分布较均匀时。


4)锁的使用方式不同

JDK 1.7

显式锁 ReentrantLock

JDK 1.8

更偏向:

  • 空桶 CAS
  • 冲突桶 synchronized

注意:虽然用了 synchronized,但 1.8 并不比 1.7 差,反而通常更好。因为 JDK 1.6 以后 JVM 对 synchronized 优化已经很成熟了。


5)哈希冲突处理不同

JDK 1.7

只有链表

JDK 1.8

链表长度过长时转红黑树,查询效率更高
时间复杂度从最坏 O(n) 优化为 O(log n)


五、面试时怎么回答更好

ConcurrentHashMap 线程安全的原因是它结合了 volatile、CAS 和局部加锁机制。读操作大多无锁,通过 volatile 保证可见性;写操作在简单场景下用 CAS 保证原子性,在链表插入、树化、扩容等复杂场景下再加锁,因此既保证线程安全,又提高了并发性能。

JDK 1.7 的 ConcurrentHashMap 基于 Segment 分段锁实现,底层是 Segment 数组,每个 Segment 继承 ReentrantLock,相当于把整个 Map 分成多个小 HashTable,每次只锁一个段。

JDK 1.8 则取消了 Segment,改成 Node 数组 + 链表 + 红黑树,采用 CAS + synchronized 实现并发控制,锁粒度细化到桶级别,并且支持链表转红黑树和多线程协助扩容,所以结构更简单,并发性能通常更好。


六、总结

ConcurrentHashMap 为什么线程安全?

因为它通过 volatile 保证可见性,通过 CAS 保证原子更新,通过局部加锁保证复杂写操作安全。

1.7 和 1.8 最大区别是什么?

1.7 是 Segment 分段锁,1.8 是 CAS + synchronized + 红黑树,锁粒度更细,结构更简单,性能更优。