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:
    • 若底层是 HashMapLinkedHashMap,可以有一个 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. 性能对比

我们使用 JMHConcurrentHashMapCollections.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. 小结

在这篇文章中,我们详细比较了 ConcurrentHashMapCollections.synchronizedMap() 在功能、线程安全性、null 支持以及性能方面的差异。

总结如下:

特性 Collections.synchronizedMap() ConcurrentHashMap
线程安全 ✅ 是 ✅ 是
读写锁机制 全局锁 分段锁
null 支持 视底层 Map 而定 ❌ 不支持
迭代时修改 抛出异常 容忍
性能表现 ❌ 较低 ✅ 更优

源码示例可从 GitHub 获取:https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-collections-maps-3


原始标题:Collections.synchronizedMap vs. ConcurrentHashMap