在 Java 并发编程中,ConcurrentHashMap 是线程安全哈希表的核心实现,相比 Hashtable 的全表锁机制,它通过更细粒度的锁设计和数据结构优化,实现了更高的并发性能。
一、JDK7 的 ConcurrentHashMap 实现原理
1.1 核心数据结构
JDK7 中 ConcurrentHashMap 采用分段锁设计,核心数据结构由三层组成:
- Segment 数组:
ConcurrentHashMap的核心锁容器,每个Segment继承自ReentrantLock,独立承担锁功能,默认并发级别为 16(即 16 个Segment)。 - HashEntry 数组:每个
Segment内部维护一个HashEntry数组,作为哈希表的核心存储结构。 - 链表:
HashEntry是链表节点,包含key、hash、next(均为final修饰,保证不变性)和val(volatile修饰,保证可见性)。
1.2 初始化过程
- 默认初始容量为 16,负载因子为 0.75,并发级别为 16。
- 初始化时,先计算
Segment数组的长度(大于等于并发级别的最小 2 的幂),然后初始化第一个Segment,其余Segment采用懒加载机制(首次使用时初始化)。
1.3 put 操作流程
- 计算
key的哈希值,定位到对应的Segment。 - 调用
lock()获取该Segment的独占锁。 - 定位到
Segment内HashEntry数组的索引,遍历链表查找key。 - 若找到
key,直接更新val;若未找到,创建新HashEntry插入链表头部。 - 检查
HashEntry数量是否超过阈值(容量 × 负载因子),若超过则扩容为原容量的 2 倍。 - 调用
unlock()释放锁。
1.4 get 操作流程
- 计算
key的哈希值,定位到Segment和HashEntry数组的索引。 - 遍历链表查找
key,由于HashEntry的val和next均为volatile修饰,保证了可见性,因此无需加锁即可获取最新值。
1.5 扩容机制
扩容仅在单个 Segment 内部进行,不影响其他 Segment。扩容时,创建新的 HashEntry 数组(容量为原数组的 2 倍),将原数组的所有节点重新哈希后迁移到新数组。
二、JDK8 的 ConcurrentHashMap 实现原理
JDK8 对 ConcurrentHashMap 进行了彻底重构,取消了分段锁设计,改用Node 数组 + CAS + Synchronized 实现,并引入红黑树优化链表过长的问题。
2.1 核心数据结构
- Node 数组:核心存储结构,替代了 JDK7 的
Segment数组。Node是链表节点,val和next均为volatile修饰。 - 链表/红黑树:当链表长度 ≥ 8 且数组长度 ≥ 64 时,链表转换为红黑树(
TreeNode);当红黑树节点数 ≤ 6 时,转换回链表。TreeBin用于封装TreeNode,作为红黑树的根节点容器。 - ForwardingNode:扩容时用于标记旧数组的节点,其他线程看到该节点会协助扩容。
2.2 初始化过程
- 默认初始容量为 16,负载因子为 0.75。
Node数组采用懒加载机制,首次执行put操作时才初始化,通过 CAS 控制初始化的并发安全。
2.3 put 操作流程
- 检查
Node数组是否初始化,若未初始化则通过 CAS 初始化。 - 计算
key的哈希值,定位到Node数组的索引。 - 若该索引位置为
null,通过 CAS 直接插入新Node。 - 若该索引位置为
ForwardingNode,说明数组正在扩容,当前线程协助扩容。 - 否则,用
synchronized锁住该索引位置的头节点,遍历链表或红黑树查找key。 - 若找到
key,更新val;若未找到,插入链表尾部或红黑树。 - 检查链表长度是否 ≥ 8,若满足则判断是否需要树化(数组长度 ≥ 64 则树化,否则扩容)。
- 释放锁,检查是否需要扩容(元素数量达到阈值则扩容)。
2.4 get 操作流程
- 计算
key的哈希值,定位到Node数组的索引。 - 检查头节点是否为目标
key,若是则直接返回val。 - 若头节点是
TreeBin,则通过红黑树的find方法查找;否则遍历链表查找。 - 由于
Node的val和next为volatile修饰,保证了可见性,因此无需加锁。
2.5 树化与反树化
- 树化条件:链表长度 ≥ 8 且
Node数组长度 ≥ 64。若数组长度 < 64,则先扩容而非树化。 - 反树化条件:红黑树节点数 ≤ 6 时,转换回链表。
2.6 扩容机制
当 Node 数组的元素数量达到阈值(容量 × 负载因子)时,触发扩容:
- 创建新的
Node数组,容量为原数组的 2 倍。 - 在旧数组的每个索引位置插入
ForwardingNode,标记该位置正在迁移。 - 多个线程可并发参与扩容,每个线程处理旧数组的一部分区间(步长由 CPU 核心数决定),通过 CAS 控制并发安全。
- 迁移完成后,将
ConcurrentHashMap的table引用指向新数组。
三、JDK7 与 JDK8 的核心差异对比
| 维度 | JDK7 | JDK8 |
|---|---|---|
| 锁粒度 | Segment 级别(默认 16 个锁) | Node 级别(锁住数组索引的头节点) |
| 数据结构 | Segment 数组 + HashEntry 数组 + 链表 | Node 数组 + 链表 + 红黑树 |
| 锁实现 | ReentrantLock | CAS + synchronized |
| 扩容方式 | 单个 Segment 独立扩容 | 整个 Node 数组并发扩容 |
| 查找效率 | 链表遍历(O(n)) | 链表遍历(O(n))或红黑树查找(O(logn)) |
| 并发性能 | 受限于 Segment 数量,高并发下性能瓶颈明显 | 锁粒度更细,红黑树优化查找,并发扩容,性能显著提升 |
四、并发安全的保证机制
4.1 JDK7 的并发安全保证
- 分段锁:
Segment继承ReentrantLock,put、remove等修改操作需获取锁,保证原子性。 - volatile 可见性:
HashEntry的val和next为volatile修饰,get操作无需加锁即可读取最新值。 - 不变性:
HashEntry的key、hash、next为final修饰,防止并发修改导致的结构破坏。
4.2 JDK8 的并发安全保证
- volatile 可见性:
Node的val和next为volatile修饰,保证get操作的可见性。 - CAS 原子性:插入头节点、初始化数组、扩容时的并发控制均通过 CAS 实现,保证原子性。
- synchronized 锁:锁住数组索引的头节点,保证链表或红黑树修改操作的原子性。
- 并发扩容:通过
ForwardingNode标记和多线程协作,实现高效的并发扩容。 - TreeBin 状态控制:
TreeBin内部通过volatile状态变量和 CAS 控制红黑树的访问,保证并发安全。
五、代码示例
5.1 使用示例
package com.jam.demo;
import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* ConcurrentHashMap 使用示例
*
* @author ken
*/
@Slf4j
public class ConcurrentHashMapDemo {
public static void main(String[] args) {
Map<String, String> concurrentHashMap = new ConcurrentHashMap<>();
Map<String, String> hashMap = Maps.newHashMap();
concurrentHashMap.put("key1", "value1");
concurrentHashMap.put("key2", "value2");
log.info("获取 key1: {}", concurrentHashMap.get("key1"));
log.info("是否包含 key2: {}", concurrentHashMap.containsKey("key2"));
concurrentHashMap.remove("key1");
log.info("移除 key1 后大小: {}", concurrentHashMap.size());
concurrentHashMap.replace("key2", "newValue2");
log.info("替换 key2 后的值: {}", concurrentHashMap.get("key2"));
}
}
5.2 并发安全测试示例
package com.jam.demo;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* ConcurrentHashMap 并发安全测试示例
*
* @author ken
*/
@Slf4j
public class ConcurrentHashMapConcurrencyTest {
private static final int THREAD_COUNT = 100;
private static final int OPERATIONS_PER_THREAD = 1000;
public static void main(String[] args) throws InterruptedException {
Map<String, Integer> concurrentHashMap = new ConcurrentHashMap<>();
ExecutorService executorService = Executors.newFixedThreadPool(THREAD_COUNT);
CountDownLatch countDownLatch = new CountDownLatch(THREAD_COUNT);
long startTime = System.currentTimeMillis();
for (int i = 0; i < THREAD_COUNT; i++) {
final int threadId = i;
executorService.submit(() -> {
try {
for (int j = 0; j < OPERATIONS_PER_THREAD; j++) {
String key = "key-" + threadId + "-" + j;
concurrentHashMap.put(key, j);
concurrentHashMap.get(key);
}
} finally {
countDownLatch.countDown();
}
});
}
countDownLatch.await();
executorService.shutdown();
long endTime = System.currentTimeMillis();
log.info("总操作数: {}", THREAD_COUNT * OPERATIONS_PER_THREAD * 2);
log.info("最终 Map 大小: {}", concurrentHashMap.size());
log.info("耗时: {} ms", endTime - startTime);
}
}
六、总结
从 JDK7 到 JDK8,ConcurrentHashMap 通过数据结构和锁机制的重构,实现了性能的显著提升。JDK7 的分段锁设计是早期并发优化的经典思路,而 JDK8 则通过细粒度锁、红黑树和并发扩容等技术,进一步释放了并发编程的潜力。需要注意的是,ConcurrentHashMap 只能保证单个操作的线程安全,对于复合操作(如先检查后执行),仍需通过额外的同步机制来保证原子性。