第23题:ConcurrentHashMap的底层原理是什么
📚 回答:
-
JDK1.7 版本:
- 底层结构 :基于分段锁(Segment)+ 链表实现。
- 核心原理 :
ConcurrentHashMap将整个数组分为多个段(Segment),每个段是一个独立的ReentrantLock控制的哈希表。- 每个
Segment内部通过链表存储冲突的元素,锁粒度为Segment级别(默认16个段)。 - 不同线程可以同时操作不同的
Segment,从而提高了并发性能。
- 初始化过程 :
- 调用无参构造时,默认创建长度为16的
Segment数组(即16个分段锁)。 - 插入元素时,先根据
key计算目标Segment的位置下标。如果该位置为空,则通过CAS(Compare-And-Swap)方式创建并插入一个Segment对象。 - 插入具体元素时,会对目标
Segment加锁,确保线程安全。
- 调用无参构造时,默认创建长度为16的
-
JDK1.8 版本:
-
底层结构 :基于数组 + 链表 + 红黑树实现,去掉了分段锁设计,改用CAS + synchronized细化锁粒度。
-
核心原理:
- 调用无参构造时,不会立即初始化数组。首次调用
put方法时才会创建长度为16的数组。 - 插入元素时:
- 如果目标位置为空,使用CAS方式插入新节点。
- 如果发生哈希冲突,链表节点会以链表或红黑树的形式存储。
- 当链表长度超过阈值(默认8)时,自动转换为红黑树,提升查询效率。
- 锁粒度细化到头节点 :在链表或红黑树插入时,使用
synchronized对链表头节点加锁,进一步提高并发性能。
💡 代码示例 :
以下伪代码展示了JDK1.8中
put方法的核心逻辑: - 调用无参构造时,不会立即初始化数组。首次调用
-
java
if (table == null || table.length == 0) {
// CAS初始化数组(非构造函数)
initTable();
}
int index = (n - 1) & hash;
Node<K,V> f = tabAt(tab, index);
if (f == null) {
// CAS尝试插入新节点
if (casTabAt(tab, index, null, newNode)) {
break;
}
} else {
// 锁住头节点处理冲突
synchronized (f) {
if (tabAt(tab, index) == f) { // 双重检查
if (f.hash >= 0) { // 链表节点
// 遍历链表插入或树化
} else { // 红黑树节点
// 树操作逻辑
}
}
}
}
💡 面试官视角:
-
面试官可能会问"为什么JDK1.8去掉分段锁?"
答:因为分段锁虽然提升了并发性能,但锁粒度仍然较大。JDK1.8通过CAS和
synchronized锁细化到头节点,进一步提升了并发性能。 -
面试官可能会追问"红黑树的引入有什么好处?"
答:当链表长度过长时,查询效率从O(n)提升到O(log n),显著优化了性能。
📌 专栏 :大白话说Java面试题 --- 01-Java基础篇