1. 引言
本文将深入探讨如何使用 ConcurrentHashMap
类实现线程安全的哈希表读写操作。作为 Java 并发编程中的核心工具,它提供了高效的并发访问能力,特别适合高吞吐量场景。
2. 核心概念
ConcurrentHashMap
是 ConcurrentMap
接口的主要实现类,属于 Java 提供的线程安全集合之一。它底层基于普通哈希表实现,与 Hashtable
类似但存在关键差异,我们将在后续章节详细分析。
2.1 关键方法
ConcurrentHashMap
API 提供了多个实用方法,本文重点分析以下两个核心方法:
- ✅
get(K key)
:读取指定键的值(读操作) - ✅
computeIfPresent(K key, BiFunction<K, V, V> remappingFunction)
:当键存在时,应用重映射函数更新值(写操作)
这些方法的具体应用将在第 3 节通过代码示例展示。
2.2 为什么选择 ConcurrentHashMap
与普通 HashMap
相比,ConcurrentHashMap
的核心优势在于:
- ✅ 读操作完全并发:任意数量的线程可同时读取同一键
- ✅ 写操作高度并发:仅在键级别阻塞,而非整个映射表
⚠️ 关键特性:读操作永不阻塞且不被写操作阻塞,写操作仅在键级别阻塞其他写操作。这种设计在高吞吐量和最终一致性场景中至关重要。
对比其他线程安全方案:
实现方式 | 并发粒度 | 性能影响 |
---|---|---|
HashTable /synchronizedMap |
整表锁定 | ❌ 严重阻塞 |
ConcurrentHashMap |
键级别锁定 | ✅ 高效并发 |
结论:在多线程环境中,ConcurrentHashMap
通过细粒度锁机制显著优于传统方案,是高并发场景的首选。
3. 线程安全操作
ConcurrentHashMap
内置了线程安全保证,能有效规避常见的并发陷阱。下面通过数字频率统计的测试用例演示其工作原理:
public class ConcurrentHashMapUnitTest {
private Map<Integer, Integer> frequencyMap;
@BeforeEach
public void setup() {
frequencyMap = new ConcurrentHashMap<>();
frequencyMap.put(0, 0);
frequencyMap.put(1, 0);
frequencyMap.put(2, 0);
}
@AfterEach
public void teardown() {
frequencyMap.clear();
}
private static void sleep(int timeout) {
try {
TimeUnit.SECONDS.sleep(timeout);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
3.1 读操作
ConcurrentHashMap
允许完全并发的读操作,这意味着:
- ✅ 多个线程可同时读取同一键
- ✅ 读操作永不阻塞写操作
- ⚠️ 可能读取到"旧"数据(最终一致性)
以下测试演示了写操作过程中读操作的行为:
@Test
public void givenOneThreadIsWriting_whenAnotherThreadReads_thenGetCorrectValue() throws Exception {
ExecutorService threadExecutor = Executors.newFixedThreadPool(3);
Runnable writeAfter1Sec = () -> frequencyMap.computeIfPresent(1, (k, v) -> {
sleep(1);
return frequencyMap.get(k) + 1;
});
Callable<Integer> readNow = () -> frequencyMap.get(1);
Callable<Integer> readAfter1001Ms = () -> {
TimeUnit.MILLISECONDS.sleep(1001);
return frequencyMap.get(1);
};
threadExecutor.submit(writeAfter1Sec);
List<Future<Integer>> results = threadExecutor.invokeAll(asList(readNow, readAfter1001Ms));
assertEquals(0, results.get(0).get());
assertEquals(1, results.get(1).get());
if (threadExecutor.awaitTermination(2, TimeUnit.SECONDS)) {
threadExecutor.shutdown();
}
}
执行流程解析:
- 写线程延迟 1 秒更新键
1
的值 - 读线程立即读取 → 获取旧值
0
- 延迟读线程在 1001ms 后读取 → 获取新值
1
💡 踩坑提示:如果需要强一致性读(阻塞式),可使用
computeIfPresent
配合恒等函数替代get()
,但会牺牲读取性能。
3.2 写操作
ConcurrentHashMap
的写操作具有以下特性:
- ✅ 同一键的写操作串行化(保证一致性)
- ✅ 不同键的写操作完全并发(提升吞吐量)
同一键写入测试
@Test
public void givenOneThreadIsWriting_whenAnotherThreadWritesAtSameKey_thenWaitAndGetCorrectValue() throws Exception {
ExecutorService threadExecutor = Executors.newFixedThreadPool(2);
Callable<Integer> writeAfter5Sec = () -> frequencyMap.computeIfPresent(1, (k, v) -> {
sleep(5);
return frequencyMap.get(k) + 1;
});
Callable<Integer> writeAfter1Sec = () -> frequencyMap.computeIfPresent(1, (k, v) -> {
sleep(1);
return frequencyMap.get(k) + 1;
});
List<Future<Integer>> results = threadExecutor.invokeAll(asList(writeAfter5Sec, writeAfter1Sec));
assertEquals(1, results.get(0).get());
assertEquals(2, results.get(1).get());
if (threadExecutor.awaitTermination(2, TimeUnit.SECONDS)) {
threadExecutor.shutdown();
}
}
关键结论:
- 第一个写线程(5秒延迟)获取锁后阻塞其他写操作
- 第二个写线程必须等待锁释放
- 最终结果按提交顺序返回:
1
→2
不同键写入测试
@Test
public void givenOneThreadIsWriting_whenAnotherThreadWritesAtDifferentKey_thenNotWaitAndGetCorrectValue() throws Exception {
ExecutorService threadExecutor = Executors.newFixedThreadPool(2);
Callable<Integer> writeAfter5Sec = () -> frequencyMap.computeIfPresent(1, (k, v) -> {
sleep(5);
return frequencyMap.get(k) + 1;
});
AtomicLong time = new AtomicLong(System.currentTimeMillis());
Callable<Integer> writeAfter1Sec = () -> frequencyMap.computeIfPresent(2, (k, v) -> {
sleep(1);
time.set((System.currentTimeMillis() - time.get()) / 1000);
return frequencyMap.get(k) + 1;
});
threadExecutor.invokeAll(asList(writeAfter5Sec, writeAfter1Sec));
assertEquals(1, time.get());
if (threadExecutor.awaitTermination(2, TimeUnit.SECONDS)) {
threadExecutor.shutdown();
}
}
性能验证:
- 第二个写线程操作不同键(
2
),无需等待 - 实际执行时间仅 1 秒(而非 5 秒)
- 证明不同键的写操作真正并行
✅ 简单粗暴的结论:
ConcurrentHashMap
通过键级锁机制,实现了写操作的高并发,性能远超整表锁方案。
4. 总结
本文深入分析了 ConcurrentHashMap
的读写特性:
- 读操作:完全并发,非阻塞,提供最终一致性
- 写操作:键级锁保证一致性,不同键的写操作真正并行
- 性能优势:在高并发场景下显著优于
HashTable
和synchronizedMap
💡 开发建议:当需要在多线程环境中使用哈希表时,优先考虑
ConcurrentHashMap
,除非有特殊的一致性要求。
完整示例代码可在 GitHub 仓库 获取。