1. 概述

在Java应用中处理键值对存储时,我们通常会在两个主要选项间抉择:*Hashtable* 和 *ConcurrentHashMap*。

虽然两者都提供线程安全的优势,但底层架构和功能存在显著差异。无论是构建遗留系统还是现代微服务架构,理解这些差异对技术选型至关重要。

本文将深入剖析两者的区别,从性能指标、同步机制到内存占用等维度,助你做出明智选择。

2. Hashtable

Hashtable 是Java中最古老的集合类之一,自JDK 1.0起就存在。它提供键值存储和检索API:

Hashtable<String, String> hashtable = new Hashtable<>();
hashtable.put("Key1", "1");
hashtable.put("Key2", "2");
hashtable.putIfAbsent("Key3", "3");
String value = hashtable.get("Key2");

核心卖点是通过方法级同步实现线程安全put(), putIfAbsent(), get(), remove() 等方法都是同步的。同一时间只有一个线程能执行这些方法,确保数据一致性。

3. ConcurrentHashMap

ConcurrentHashMap 是更现代的替代方案,随Java 5的集合框架引入。

两者都实现Map接口,所以方法签名高度相似:

ConcurrentHashMap<String, String> concurrentHashMap = new ConcurrentHashMap<>();
concurrentHashMap.put("Key1", "1");
concurrentHashMap.put("Key2", "2");
concurrentHashMap.putIfAbsent("Key3", "3");
String value = concurrentHashMap.get("Key2");

4. 核心差异

本节将从并发性、性能、迭代器特性和内存占用等维度对比两者差异。

4.1. 并发机制

Hashtable 通过方法级同步保证线程安全。

ConcurrentHashMap 则提供更高并发级别的线程安全。允许多个线程同时读取和执行有限写入操作,无需锁定整个数据结构。在读多写少的场景中尤其有用。

4.2. 性能表现

虽然两者都保证线程安全,但同步机制导致性能差异显著:

Hashtable 在写入时锁定整张表,阻塞所有读写操作。高并发环境下容易成为瓶颈。

ConcurrentHashMap 允许并发读取和有限并发写入,扩展性更好,实际场景中通常更快。

小数据集时性能差异不明显,但大数据量和高并发下 ConcurrentHashMap 优势明显。

使用JMH基准测试验证(10线程模拟并发,3次预热+5次测量):

@Benchmark
@Group("hashtable")
public void benchmarkHashtablePut() {
    for (int i = 0; i < 10000; i++) {
        hashTable.put(String.valueOf(i), i);
    }
}

@Benchmark
@Group("hashtable")
public void benchmarkHashtableGet(Blackhole blackhole) {
    for (int i = 0; i < 10000; i++) {
        Integer value = hashTable.get(String.valueOf(i));
        blackhole.consume(value);
    }
}

@Benchmark
@Group("concurrentHashMap")
public void benchmarkConcurrentHashMapPut() {
    for (int i = 0; i < 10000; i++) {
        concurrentHashMap.put(String.valueOf(i), i);
    }
}

@Benchmark
@Group("concurrentHashMap")
public void benchmarkConcurrentHashMapGet(Blackhole blackhole) {
    for (int i = 0; i < 10000; i++) {
        Integer value = concurrentHashMap.get(String.valueOf(i));
        blackhole.consume(value);
    }
}

测试结果:

Benchmark                                                        Mode  Cnt   Score   Error
BenchMarkRunner.concurrentHashMap                                avgt    5   1.788 ± 0.406
BenchMarkRunner.concurrentHashMap:benchmarkConcurrentHashMapGet  avgt    5   1.157 ± 0.185
BenchMarkRunner.concurrentHashMap:benchmarkConcurrentHashMapPut  avgt    5   2.419 ± 0.629
BenchMarkRunner.hashtable                                        avgt    5  10.744 ± 0.873
BenchMarkRunner.hashtable:benchmarkHashtableGet                  avgt    5  10.810 ± 1.208
BenchMarkRunner.hashtable:benchmarkHashtablePut                  avgt    5  10.677 ± 0.541

分数越低性能越好,ConcurrentHashMap 在读写操作上全面领先

4.3. Hashtable 迭代器特性

Hashtable 迭代器是"fail-fast"的:创建迭代器后若修改结构,会抛出ConcurrentModificationException。这种机制能快速失败避免不可预测行为。

示例中启动两个线程:

  • iteratorThread: 遍历键并打印(100ms间隔)
  • modifierThread: 50ms后插入新键值对

modifierThread修改时,iteratorThread会抛出异常

Hashtable<String, Integer> hashtable = new Hashtable<>();
hashtable.put("Key1", 1);
hashtable.put("Key2", 2);
hashtable.put("Key3", 3);
AtomicBoolean exceptionCaught = new AtomicBoolean(false);

Thread iteratorThread = new Thread(() -> {
    Iterator<String> it = hashtable.keySet().iterator();
    try {
        while (it.hasNext()) {
            it.next();
            Thread.sleep(100);
        }
    } catch (ConcurrentModificationException e) {
        exceptionCaught.set(true);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});

Thread modifierThread = new Thread(() -> {
    try {
        Thread.sleep(50);
        hashtable.put("Key4", 4);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});

iteratorThread.start();
modifierThread.start();

iteratorThread.join();
modifierThread.join();

assertTrue(exceptionCaught.get());

4.4. ConcurrentHashMap 迭代器特性

Hashtable不同,ConcurrentHashMap 使用"弱一致性"迭代器。

这种迭代器能容忍并发修改,反映创建时的快照状态(可能包含后续修改但不保证)。因此可以在一个线程修改的同时在另一个线程安全遍历。

同样示例:

  • iteratorThread: 遍历键(100ms间隔)
  • modifierThread: 50ms后插入新键值对

不会抛出ConcurrentModificationException,迭代器继续正常工作:

ConcurrentHashMap<String, Integer> concurrentHashMap = new ConcurrentHashMap<>();
concurrentHashMap.put("Key1", 1);
concurrentHashMap.put("Key2", 2);
concurrentHashMap.put("Key3", 3);
AtomicBoolean exceptionCaught = new AtomicBoolean(false);

Thread iteratorThread = new Thread(() -> {
    Iterator<String> it = concurrentHashMap.keySet().iterator();
    try {
        while (it.hasNext()) {
            it.next();
            Thread.sleep(100);
        }
    } catch (ConcurrentModificationException e) {
        exceptionCaught.set(true);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});

Thread modifierThread = new Thread(() -> {
    try {
        Thread.sleep(50);
        concurrentHashMap.put("Key4", 4);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});

iteratorThread.start();
modifierThread.start();

iteratorThread.join();
modifierThread.join();

assertFalse(exceptionCaught.get());

4.5. 内存占用对比

Hashtable 结构简单:数组+链表。每个桶存储一个键值对,只有数组和链表节点的开销。整体内存占用更低

ConcurrentHashMap 更复杂:由多个段(segment)组成,每个段本质是独立哈希表。虽然提升了并发能力,但增加了段对象的内存开销。

每个段维护额外元数据(计数、阈值、负载因子等),动态调整段数和大小时还需维护更多元数据,进一步增加内存消耗。

5. 结论

本文深入分析了HashtableConcurrentHashMap的核心差异。

两者都能线程安全地存储键值对,但ConcurrentHashMap凭借先进的同步机制,在性能和扩展性上通常更胜一筹。

Hashtable在遗留系统或需要方法级同步的场景仍有价值。根据应用实际需求选择,才能避免踩坑

示例代码可在GitHub获取。


原始标题:Difference Between Hashtable and ConcurrentHashMap in Java