一、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 + 红黑树,锁粒度更细,结构更简单,性能更优。