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. 结论
本文深入分析了Hashtable和ConcurrentHashMap的核心差异。
两者都能线程安全地存储键值对,但ConcurrentHashMap凭借先进的同步机制,在性能和扩展性上通常更胜一筹。
Hashtable在遗留系统或需要方法级同步的场景仍有价值。根据应用实际需求选择,才能避免踩坑。
示例代码可在GitHub获取。