在 Java 中,ConcurrentHashMap
的 1.7 和 1.8 版本在内部实现、性能优化和功能特性上有显著区别,主要体现在以下方面:
1. 数据结构与分段锁机制
-
JDK 1.7
- 分段锁(Segment) :将整个哈希表划分为多个
Segment
,每个Segment
继承自ReentrantLock
,是一个独立的哈希表,拥有自己的锁。 - 锁粒度 :锁的粒度是
Segment
级别,不同Segment
可以并发操作,但同一Segment
内的操作需要加锁。 - 数据结构 :每个
Segment
内部是一个数组 + 链表的结构,与HashMap
类似。 - 缺点 :锁的粒度较粗,在高并发场景下,如果多个线程操作同一个
Segment
,仍然会竞争锁,导致性能瓶颈。
- 分段锁(Segment) :将整个哈希表划分为多个
-
JDK 1.8
- 移除分段锁 :不再使用
Segment
,而是采用Node
数组 + 链表/红黑树的结构,直接对数组的每个桶(bucket
)加锁。 - 锁粒度 :锁的粒度细化到数组的每个桶(
Node
数组的每个元素),使用synchronized
或 CAS(Compare-And-Swap)操作来实现线程安全。 - 数据结构:当链表长度超过阈值(默认 8)时,链表会转换为红黑树,以提高查询效率。
- 优点:锁的粒度更细,减少了锁竞争,提高了并发性能。
- 移除分段锁 :不再使用
2. 锁的实现方式
-
JDK 1.7
- 使用
ReentrantLock
实现分段锁,每个Segment
是一个独立的锁。 - 写操作需要先定位到
Segment
,然后获取该Segment
的锁,操作完成后释放锁。
- 使用
-
JDK 1.8
- 锁的实现方式更加灵活:
- CAS 操作:用于无锁的初始化、扩容等操作。
- synchronized:用于对数组的每个桶加锁,锁的粒度更细。
- 写操作直接定位到桶(
bucket
),然后对桶加锁,操作完成后释放锁。
- 锁的实现方式更加灵活:
3. 扩容机制
-
JDK 1.7
- 分段扩容 :每个
Segment
独立扩容,扩容时需要锁住整个Segment
,其他线程无法访问该Segment
。 - 扩容过程 :扩容时,将
Segment
中的数据重新哈希到新的数组中,过程较为复杂。
- 分段扩容 :每个
-
JDK 1.8
- 全局扩容 :整个
ConcurrentHashMap
作为一个整体进行扩容,采用多线程并行扩容的方式。 - 扩容过程 :
- 扩容时,会创建一个新的数组(
nextTable
),大小为原数组的两倍。 - 多线程协助扩容,每个线程负责迁移部分数据。
- 使用
ForwardingNode
作为占位符,标记正在迁移的桶,其他线程可以跳过这些桶。
- 扩容时,会创建一个新的数组(
- 优点:扩容过程更加高效,减少了锁竞争。
- 全局扩容 :整个
4. 链表转红黑树
-
JDK 1.7
- 仅使用链表解决哈希冲突,当链表长度过长时,查询性能会下降(时间复杂度为 O(n))。
-
JDK 1.8
- 引入红黑树:当链表长度超过阈值(默认 8)且数组长度大于 64 时,链表会转换为红黑树。
- 优点:红黑树的查询时间复杂度为 O(log n),提高了查询效率。
- 退化条件:当红黑树节点数减少到一定程度(默认 6)时,会退化为链表。
5. 性能优化
- JDK 1.8 的改进 :
- 锁粒度更细:减少了锁竞争,提高了并发性能。
- CAS 操作:用于无锁的初始化、扩容等操作,减少了锁的开销。
- 红黑树:提高了查询效率,特别是在高冲突场景下。
- 并行扩容:多线程协助扩容,减少了扩容时的停顿时间。
6. 代码实现简化
- JDK 1.8
- 代码实现更加简洁,移除了
Segment
相关的代码,逻辑更加清晰。 - 提供了更多的原子操作方法,如
computeIfAbsent
、merge
等,简化了并发编程的复杂性。
- 代码实现更加简洁,移除了
总结
- JDK 1.7 的
ConcurrentHashMap
通过分段锁机制实现了线程安全,但锁的粒度较粗,在高并发场景下可能存在性能瓶颈。 - JDK 1.8 的
ConcurrentHashMap
通过移除分段锁、引入 CAS 操作、细粒度锁和红黑树等优化,显著提高了并发性能和查询效率,代码实现也更加简洁。
我正在程序员刷题神器面试鸭上高效准备面试,9000+ 高频面试真题、800 万字优质题解,覆盖主流编程方向,跟我一起刷原题、过面试:点此进入