一、引言
在 Java 集合框架中,HashMap、HashTable和ConcurrentHashMap是三个核心的哈希表实现,它们在并发性、线程安全性、性能和使用场景上有着显著差异。理解这些区别对于编写高效、健壮的 Java 程序至关重要。本文将深入剖析这三者的底层原理、特性对比以及适用场景。
二、核心特性概览
| 特性 | HashMap | HashTable | ConcurrentHashMap |
|---|---|---|---|
| 线程安全 | 否 | 是(方法级 synchronized) | 是(分段锁/CAS) |
| 允许 null 键/值 | 是 | 否 | 否(键值均不允许) |
| 继承体系 | 继承 AbstractMap | 继承 Dictionary | 继承 AbstractMap |
| 默认初始容量 | 16 | 11 | 16 |
| 扩容机制 | 2 的幂次方 | 2n+1 | 2 的幂次方 |
| 迭代器 | 快速失败(Fail-Fast) | 快速失败(Fail-Fast) | 弱一致性(Weakly Consistent) |
| 性能(单线程) | 高 | 较低 | 较高 |
| 性能(高并发) | 不安全,需外部同步 | 低(锁竞争严重) | 高(锁粒度细) |
| Java 版本 | 1.2+ | 1.0+ | 1.5+ |
三、HashMap 深度解析
3.1 基本特性
HashMap是 Java 集合框架中最常用的哈希表实现,基于数组+链表/红黑树(JDK 8+)的结构。它不保证元素的顺序,且允许一个null键和多个null值。
3.2 线程安全性
HashMap是非线程安全的。在多线程环境下,如果多个线程同时修改HashMap(如 put、remove),可能会导致数据不一致、死循环(JDK 7 及之前)或元素丢失等问题。
3.3 扩容机制
当元素数量超过容量与负载因子(默认 0.75)的乘积时,HashMap会进行扩容,容量变为原来的 2 倍。扩容时,会重新计算所有元素的位置(rehash)。
3.4 代码示例
import java.util.HashMap; public class HashMapDemo { public static void main(String[] args) { HashMap<String, Integer> map = new HashMap<>(); map.put("apple", 1); map.put("banana", 2); map.put(null, 3); // 允许 null 键 map.put("orange", null); // 允许 null 值 System.out.println(map.get("apple")); // 输出: 1 System.out.println(map.containsKey(null)); // 输出: true } }四、HashTable 深度解析
4.1 基本特性
HashTable是 Java 早期的哈希表实现,属于遗留类(Legacy Class)。它是线程安全的,通过在方法上添加synchronized关键字实现。不允许null键和null值。
4.2 线程安全性实现
HashTable使用对象级别的锁(synchronized 方法),即对整个哈希表加锁。这保证了线程安全,但在高并发场景下会导致严重的锁竞争,性能低下。
4.3 与 HashMap 的主要区别
- 线程安全:
HashTable线程安全,HashMap非线程安全。 - Null 值:
HashTable不允许 null 键/值,HashMap允许。 - 继承父类:
HashTable继承Dictionary,HashMap继承AbstractMap。 - 迭代器:
HashTable使用Enumeration,HashMap使用Iterator。
4.4 代码示例
import java.util.Hashtable; public class HashTableDemo { public static void main(String[] args) { Hashtable<String, Integer> table = new Hashtable<>(); table.put("apple", 1); table.put("banana", 2); // table.put(null, 3); // 抛出 NullPointerException // table.put("orange", null); // 抛出 NullPointerException System.out.println(table.get("apple")); // 输出: 1 } }五、ConcurrentHashMap 深度解析
5.1 基本特性
ConcurrentHashMap是 Java 5 引入的并发哈希表,专为高并发场景设计。它提供了比HashTable更好的并发性能,同时保证了线程安全。
5.2 线程安全性实现(演进)
- JDK 7:采用分段锁(Segment)机制,将整个哈希表分成多个段(Segment),每个段独立加锁,提高了并发度。
- JDK 8 及以后:摒弃分段锁,采用
synchronized+CAS(Compare-And-Swap)实现。锁的粒度更细(锁住单个链表头节点或红黑树根节点),并发性能进一步提升。
5.3 关键特性
- 不允许 null 键/值:设计上避免二义性(无法区分 key 不存在还是值为 null)。
- 弱一致性迭代器:迭代器创建后,可能反映或可能不反映更新操作,但不会抛出
ConcurrentModificationException。 - 高并发读:读操作通常不需要加锁,性能接近
HashMap。
5.4 代码示例
import java.util.concurrent.ConcurrentHashMap; public class ConcurrentHashMapDemo { public static void main(String[] args) { ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>(); map.put("apple", 1); map.put("banana", 2); // map.put(null, 3); // 抛出 NullPointerException // map.put("orange", null); // 抛出 NullPointerException // 线程安全的复合操作 map.computeIfAbsent("pear", k -> 4); System.out.println(map.get("pear")); // 输出: 4 } }六、三者对比与选型建议
6.1 性能对比
- 单线程环境:
HashMap>ConcurrentHashMap>HashTable - 低并发读写:
ConcurrentHashMap>HashMap(需外部同步)>HashTable - 高并发读写:
ConcurrentHashMap显著优于HashTable
6.2 选型指南
- 单线程应用:优先使用
HashMap,性能最佳。 - 低并发,需要线程安全:可使用
Collections.synchronizedMap(new HashMap<>())或ConcurrentHashMap。 - 高并发场景:必须使用
ConcurrentHashMap。 - 遗留系统或特定 API 要求:才考虑使用
HashTable。 - 需要 null 键/值:只能选择
HashMap。
6.3 常见面试题要点
- HashMap 线程不安全的表现:扩容时可能形成环形链表(JDK 7)、数据覆盖、size 不准确。
- ConcurrentHashMap 如何保证线程安全:JDK 7 分段锁,JDK 8+ synchronized + CAS + volatile。
- 为什么 ConcurrentHashMap 不允许 null:避免二义性,简化并发逻辑。
- 快速失败 vs 弱一致性迭代器:前者在检测到并发修改时立即抛出异常,后者容忍并发修改,但不保证实时性。
七、总结
HashMap、HashTable和ConcurrentHashMap分别代表了非线程安全、粗粒度线程安全和细粒度高性能线程安全三种设计哲学。在现代 Java 开发中,HashMap是单线程首选,ConcurrentHashMap是多线程标配,而HashTable已基本被淘汰。理解其底层实现和适用场景,有助于我们根据实际需求做出最合适的技术选型。