引言
在 Java 集合框架中,HashMap、HashTable 和 ConcurrentHashMap 都是基于哈希表实现的 Map 接口实现类,它们在多线程环境下的表现、性能特征和适用场景有着显著差异。理解这三者的区别,对于编写高效、安全的并发程序至关重要。本文将深入剖析它们的底层实现、线程安全性、性能表现以及使用场景。
1. 核心特性对比概览
| 特性 | HashMap | HashTable | ConcurrentHashMap |
|---|---|---|---|
| 线程安全 | 否 | 是(方法级 synchronized) | 是(分段锁/CAS) |
| 允许 null 键/值 | 是 | 否 | 否(键值均不允许) |
| 初始容量 | 16 | 11 | 16 |
| 扩容因子 | 0.75 | 0.75 | 0.75 |
| 迭代器 | Fail-Fast | Fail-Fast | Weakly Consistent |
| 继承体系 | AbstractMap | Dictionary | AbstractMap |
| Java 版本 | 1.2+ | 1.0+ | 1.5+ |
| 性能 | 单线程最快 | 多线程性能差 | 高并发性能优秀 |
2. HashMap:非线程安全的哈希表
2.1 基本特性
HashMap 是 Java 中最常用的 Map 实现,它基于哈希表(数组+链表/红黑树)实现,提供了常数时间复杂度的基本操作(get 和 put)。
2.2 关键实现细节
java
// HashMap 的 put 方法核心逻辑(简化版)
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 懒加载:第一次 put 时才初始化数组
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 计算索引位置:(n-1) & hash
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null); // 直接插入
else {
// 处理哈希冲突
// ... 链表/红黑树插入逻辑
}
// 检查是否需要扩容
if (++size > threshold)
resize();
return null;
}
2.3 线程安全问题
HashMap 不是线程安全的,在多线程环境下同时修改可能导致:
- 数据丢失:多个线程同时 put 可能覆盖彼此的数据
- 死循环:JDK 1.7 及之前版本在扩容时可能形成环形链表
- 大小不一致:size 字段更新不同步
java
// 线程不安全的示例
public class HashMapThreadUnsafeDemo {
public static void main(String[] args) throws InterruptedException {
Map<String, Integer> map = new HashMap<>();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
map.put("key" + i, i);
}
});
Thread t2 = new Thread(() -> {
for (int i = 1000; i < 2000; i++) {
map.put("key" + i, i);
}
});
t1.start();
t2.start();
t1.join();
t2.join();
// 结果可能小于 2000,存在数据丢失
System.out.println("Map size: " + map.size());
}
}
3. HashTable:线程安全的遗留类
3.1 设计特点
HashTable 是 Java 早期的线程安全 Map 实现,通过在方法上添加 synchronized 关键字实现线程安全。
java
// HashTable 的 put 方法(简化)
public synchronized V put(K key, V value) {
// 检查 value 不能为 null
if (value == null) {
throw new NullPointerException();
}
// 确保 key 不为 null
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
// ... 插入逻辑
}
3.2 性能瓶颈
由于所有方法都是 synchronized 的,HashTable 存在严重的性能问题:
- 锁粒度粗:整个对象一把锁,并发度低
- 竞争激烈:多个线程无法同时读写
- 吞吐量低:高并发场景下性能急剧下降
3.3 使用限制
- 不允许
null键和null值 - 初始容量为 11(质数),扩容为
2n+1 - 继承自
Dictionary类(已过时)
4. ConcurrentHashMap:高并发优化方案
4.1 演进历程
- JDK 1.5-1.7:分段锁(Segment)实现
- JDK 1.8+:CAS + synchronized 优化
4.2 JDK 1.8 实现原理
java
// ConcurrentHashMap 的 put 方法核心(简化)
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable(); // 懒初始化
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// CAS 尝试插入新节点
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
break;
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f); // 协助扩容
else {
synchronized (f) { // 锁住链表头/树根
// ... 链表/红黑树插入
}
}
}
addCount(1L, binCount);
return null;
}
4.3 核心优化技术
- CAS 操作:无锁化初始化、计数更新
- 细粒度锁:只锁单个桶(链表头/树根)
- 扩容协助:多线程协同完成扩容
- 计数分离 :使用
LongAdder思想统计 size
4.4 迭代器特性
ConcurrentHashMap 使用弱一致性迭代器,迭代过程中可以反映创建迭代器之后的部分修改,但不保证反映所有修改。
5. 性能对比测试
5.1 测试场景设计
java
public class MapPerformanceTest {
private static final int THREAD_COUNT = 10;
private static final int OPERATION_COUNT = 100000;
public static void testMap(Map<String, Integer> map, String mapName) {
long start = System.currentTimeMillis();
ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);
for (int i = 0; i < THREAD_COUNT; i++) {
final int threadId = i;
executor.submit(() -> {
for (int j = 0; j < OPERATION_COUNT; j++) {
String key = "thread-" + threadId + "-key-" + j;
map.put(key, j);
map.get(key);
}
});
}
executor.shutdown();
try {
executor.awaitTermination(1, TimeUnit.HOURS);
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println(mapName + " 耗时: " + (end - start) + "ms");
}
public static void main(String[] args) {
// 预热
testMap(new HashMap<>(), "HashMap (单线程)");
// 多线程测试
testMap(new Hashtable<>(), "Hashtable");
testMap(new ConcurrentHashMap<>(), "ConcurrentHashMap");
testMap(Collections.synchronizedMap(new HashMap<>()), "Collections.synchronizedMap");
}
}
5.2 预期性能排序(高并发场景)
ConcurrentHashMap > Collections.synchronizedMap ≈ HashTable
6. 使用场景建议
6.1 选择 HashMap 当:
- 单线程环境
- 需要最高性能
- 允许 null 键值
- 不需要线程安全
6.2 选择 HashTable 当:
- 维护遗留代码
- 简单的同步需求(低并发)
- 明确禁止 null 值
6.3 选择 ConcurrentHashMap 当:
- 高并发读写场景
- 需要高吞吐量
- 读多写少的场景
- 需要弱一致性迭代
7. 常见面试问题
7.1 HashMap 的扩容机制
java
// HashMap 扩容核心逻辑
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 容量翻倍:newCap = oldCap << 1
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // 阈值翻倍
}
// ... 其他初始化逻辑
// 重新哈希所有元素
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // 链表重哈希
// JDK 1.8 优化:无需重新计算哈希
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
// 判断元素是否需要移动到新位置
if ((e.hash & oldCap) == 0) {
// 留在原索引
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
// 移动到新索引(原索引+oldCap)
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// ... 设置新表
}
}
}
}
return newTab;
}
7.2 ConcurrentHashMap 的 size() 方法
ConcurrentHashMap 的 size() 方法并不完全精确,它通过 baseCount 和 CounterCell 数组来统计,采用类似 LongAdder 的分段计数策略,在并发更新时性能更好。
8. 最佳实践
-
初始化容量:预估元素数量,避免频繁扩容
java// 预估 1000 个元素,负载因子 0.75 int expectedSize = 1000; int initialCapacity = (int) (expectedSize / 0.75f) + 1; Map<String, Object> map = new HashMap<>(initialCapacity); -
键对象设计 :重写
hashCode()和equals()方法javapublic class CustomKey { private final String id; private final int version; @Override public int hashCode() { return Objects.hash(id, version); } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; CustomKey that = (CustomKey) obj; return version == that.version && Objects.equals(id, that.id); } } -
并发控制:根据场景选择合适的并发容器
java// 读多写少:ConcurrentHashMap Map<String, Object> cache = new ConcurrentHashMap<>(); // 写多读少:考虑 CopyOnWriteArrayList 等 // 需要排序:ConcurrentSkipListMap
总结
HashMap、HashTable 和 ConcurrentHashMap 各有其适用场景。在现代 Java 开发中:
- 单线程环境 首选
HashMap - 低并发同步 可使用
Collections.synchronizedMap(new HashMap<>()) - 高并发场景 必须使用
ConcurrentHashMap - 遗留系统维护 才考虑
HashTable
理解它们的底层实现差异,能够帮助我们在实际开发中做出更合适的技术选型,编写出既安全又高效的程序。