1. 概述
本文将对比 Collections.synchronizedMap()
和 ConcurrentHashMap
这两种实现线程安全 Map 的方式。
此外,我们还会通过性能测试比较它们在读写操作上的表现。
2. 核心差异
✅ Collections.synchronizedMap()
和 ConcurrentHashMap
都能提供线程安全的操作支持。
但两者的实现机制和使用场景有所不同。
2.1. Collections.synchronizedMap()
Collections.synchronizedMap()
是 Java 提供的一个工具方法,用于将任意 Map
包装为线程安全版本。它返回一个被包装过的 Map
实例,所有访问都会同步到原始的 Map
上。
⚠️ 但它本质上是对整个 Map 加锁,无论是读还是写,都需要获取同一把锁,这在高并发下会成为性能瓶颈。
2.2. ConcurrentHashMap
ConcurrentHashMap
是 JDK 1.5 引入的高性能并发 Map 实现,专为多线程环境设计。
它的特点是:
- ✅ 读操作不加锁:大多数情况下无需阻塞
- ✅ 写操作分段加锁:默认 16 段(segment),允许多个线程同时写入不同段
- ❌ 不允许 key 或 value 为
null
2.3. ConcurrentModificationException
对于普通的 HashMap
或者 synchronizedMap
,在迭代过程中进行结构性修改(如添加、删除元素)会抛出 ConcurrentModificationException
。
@Test(expected = ConcurrentModificationException.class)
public void whenRemoveAndAddOnHashMap_thenConcurrentModificationError() {
Map<Integer, String> map = new HashMap<>();
map.put(1, "baeldung");
map.put(2, "HashMap");
Map<Integer, String> synchronizedMap = Collections.synchronizedMap(map);
Iterator<Entry<Integer, String>> iterator = synchronizedMap.entrySet().iterator();
while (iterator.hasNext()) {
synchronizedMap.put(3, "Modification");
iterator.next();
}
}
而 ConcurrentHashMap
则不会抛异常,即使在迭代过程中修改也不会报错:
Map<Integer, String> map = new ConcurrentHashMap<>();
map.put(1, "baeldung");
map.put(2, "HashMap");
Iterator<Entry<Integer, String>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
map.put(3, "Modification");
iterator.next();
}
Assert.assertEquals(3, map.size());
2.4. null 值支持
- ❌
ConcurrentHashMap
完全不支持 null 键或值:
@Test(expected = NullPointerException.class)
public void allowNullKey_In_ConcurrentHasMap() {
Map<String, Integer> map = new ConcurrentHashMap<>();
map.put(null, 1);
}
- ✅ 而
Collections.synchronizedMap()
的 null 支持取决于底层 Map:- 若底层是
HashMap
或LinkedHashMap
,可以有一个 null 键和多个 null 值; - 若底层是
TreeMap
,则不能有 null 键,但可以有 null 值。
- 若底层是
验证示例:
Map<String, Integer> map = Collections
.synchronizedMap(new HashMap<String, Integer>());
map.put(null, 1);
Assert.assertTrue(map.get(null).equals(1));
3. 性能对比
我们使用 JMH 对 ConcurrentHashMap
和 Collections.synchronizedMap()
进行了基准测试,测试内容包括随机读写操作。
3.1. 测试代码
@Benchmark
public void randomReadAndWriteSynchronizedMap() {
Map<String, Integer> map = Collections.synchronizedMap(new HashMap<String, Integer>());
performReadAndWriteTest(map);
}
@Benchmark
public void randomReadAndWriteConcurrentHashMap() {
Map<String, Integer> map = new ConcurrentHashMap<>();
performReadAndWriteTest(map);
}
private void performReadAndWriteTest(final Map<String, Integer> map) {
for (int i = 0; i < TEST_NO_ITEMS; i++) {
Integer randNumber = (int) Math.ceil(Math.random() * TEST_NO_ITEMS);
map.get(String.valueOf(randNumber));
map.put(String.valueOf(randNumber), randNumber);
}
}
3.2. 测试结果(单位:ns/op)
Benchmark 名称 | 平均耗时 (ns/op) |
---|---|
randomReadAndWriteConcurrentHashMap | 3,061,555 |
randomReadAndWriteSynchronizedMap | 3,234,465 |
randomReadConcurrentHashMap | 2,728,614 |
randomReadSynchronizedMap | 3,471,147 |
randomWriteConcurrentHashMap | 3,081,447 |
randomWriteSynchronizedMap | 3,385,768 |
✅ 可见,ConcurrentHashMap
在各种操作上都优于 Collections.synchronizedMap()
。
4. 使用建议
- ✅ 如果你追求极致性能,尤其是在写操作频繁的场景下,优先选择
ConcurrentHashMap
- ⚠️ 如果你需要更强的一致性语义(比如严格串行化访问),并且对性能要求不高,则可以选择
Collections.synchronizedMap()
因为 synchronizedMap()
所有操作都要锁住整个对象,而 ConcurrentHashMap
支持更细粒度的并发控制。
5. 小结
在这篇文章中,我们详细比较了 ConcurrentHashMap
和 Collections.synchronizedMap()
在功能、线程安全性、null 支持以及性能方面的差异。
总结如下:
特性 | Collections.synchronizedMap() |
ConcurrentHashMap |
---|---|---|
线程安全 | ✅ 是 | ✅ 是 |
读写锁机制 | 全局锁 | 分段锁 |
null 支持 | 视底层 Map 而定 | ❌ 不支持 |
迭代时修改 | 抛出异常 | 容忍 |
性能表现 | ❌ 较低 | ✅ 更优 |
源码示例可从 GitHub 获取:https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-collections-maps-3