问题
ConcurrentHashMap在JDK1.7和1.8中有什么不同?
答案
一、核心差异对比表
| 对比维度 | JDK 1.7 | JDK 1.8 |
|---|---|---|
| 数据结构 | Segment数组 + HashEntry数组 + 链表 | Node数组 + 链表 + 红黑树 |
| 锁机制 | ReentrantLock 分段锁 | CAS + synchronized |
| 锁粒度 | Segment级别(粗) | Node节点级别(细) |
| 最大并发度 | Segment数量(默认16) | 数组长度(理论上无上限) |
| 扩容方式 | 单线程扩容 | 多线程协作扩容 |
| 查询性能 | O(n) 最坏情况 | O(log n) 红黑树优化 |
二、JDK 1.7 实现
1. 数据结构
// 分段锁结构
final Segment<K,V>[] segments; // Segment数组
static final class Segment<K,V> extends ReentrantLock {
transient volatile HashEntry<K,V>[] table; // 每个Segment包含一个HashEntry数组
transient int count;
}
static final class HashEntry<K,V> {
final int hash;
final K key;
volatile V value;
volatile HashEntry<K,V> next;
}
特点:
- 整个 ConcurrentHashMap 分为多个 Segment(默认16个)
- 每个 Segment 相当于一个小的 HashMap
- Segment 继承 ReentrantLock,本身就是一把锁
2. 锁机制
public V put(K key, V value) {
Segment<K,V> s;
// 1. 定位到具体的Segment
int hash = hash(key);
int j = (hash >>> segmentShift) & segmentMask;
s = ensureSegment(j);
// 2. 在Segment内部加锁
return s.put(key, hash, value, false);
}
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// 尝试获取锁,失败则自旋尝试
HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);
// 获取锁后进行插入操作
// ...
}
缺点:
- 并发度受限于 Segment 数量
- 不同的 key 可能落在同一个 Segment,产生锁竞争
三、JDK 1.8 实现
1. 数据结构
// 直接使用Node数组,类似HashMap
transient volatile Node<K,V>[] table;
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
}
// 红黑树节点
static final class TreeNode<K,V> extends Node<K,V> {
TreeNode<K,V> parent;
TreeNode<K,V> left;
TreeNode<K,V> right;
// ...
}
改进:
- 取消 Segment 分段设计
- 链表长度 ≥ 8 且数组长度 ≥ 64 时转红黑树
2. 锁机制
final V putVal(K key, V value, boolean onlyIfAbsent) {
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable(); // 初始化
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// CAS插入,不加锁
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break;
} else {
// synchronized锁住数组中的Node节点
synchronized (f) {
if (tabAt(tab, i) == f) {
// 链表或红黑树插入
}
}
}
}
}
优势:
- CAS 操作:桶位为空时,无锁插入,性能更高
- synchronized 优化:JDK 1.6 后 synchronized 性能大幅提升(锁升级机制)
- 锁粒度更细:锁的是数组索引位置,而非整个 Segment
四、关键改进点分析
1. 并发度提升
// JDK 1.7:最大并发度 = Segment数量(默认16)
// 16个线程同时操作不同Segment才能完全并发
// JDK 1.8:最大并发度 = 数组长度
// 理论上数组有多长,就能支持多少并发操作
2. 扩容优化
// JDK 1.7:单线程扩容
// 某个Segment扩容时,其他线程访问该Segment会阻塞
// JDK 1.8:多线程协作扩容
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
// 多个线程可以同时迁移数据
// 使用ForwardingNode标记已迁移的桶位
}
3. 内存占用
- JDK 1.7:即使元素很少,也需要初始化所有 Segment
- JDK 1.8:延迟初始化,更节省内存
五、性能对比
- 高并发场景:JDK 1.8 的细粒度锁性能更优
- 低冲突场景:JDK 1.8 的 CAS 操作减少锁开销
- 查询性能:JDK 1.8 的红黑树避免极端情况下的 O(n) 查询
六、答题总结
JDK 1.7 → JDK 1.8 的主要变化:
- 取消分段锁:Segment 数组 + ReentrantLock → Node 数组 + synchronized
- 锁粒度优化:Segment 级别 → Node 节点级别
- 引入红黑树:链表过长时转红黑树,提升查询性能
- 引入 CAS:空桶位插入使用 CAS,减少锁竞争
- 协作扩容:支持多线程同时参与扩容,效率更高
- 并发度提升:从固定的 Segment 数量提升到数组长度
总体而言,JDK 1.8 版本在并发性能、扩展性、内存占用等方面都有显著提升。