在 Java 集合框架中,HashMap、Hashtable、ConcurrentHashMap 都是基于哈希表实现的键值对容器,三者最核心的差异集中在线程安全机制、锁粒度设计、并发性能三个维度,也是 Java 并发面试的高频基础考点。本文从底层实现到特性差异逐一拆解,帮你彻底理清三者的区别与选型逻辑。
一、各实现详细解析
1. HashMap:单线程性能首选
HashMap 是日常开发最常用的哈希表实现,不做任何线程安全处理,因此单线程环境下没有锁开销,性能最高。
核心特性
- 底层结构:JDK1.8 之后采用「数组 + 链表 / 红黑树」实现。当链表长度超过 8 且数组长度大于 64 时,链表会转换为红黑树,解决哈希冲突严重时的查询性能退化问题。
- 空值支持:允许最多 1 个 key 为 null,允许多个 value 为 null。
- 并发风险:多线程环境下会出现数据覆盖、size 计数不准、扩容死循环(JDK1.7 头插法)等异常,禁止在并发场景下直接使用。
2. Hashtable
Hashtable 是 JDK 早期提供的线程安全哈希表,实现方式非常粗暴:所有核心方法都加上 synchronized 关键字,直接锁整个 Hashtable 对象。
核心特性
- 锁粒度极粗:整张表只有一把锁。无论线程操作哪个位置的数据,都会产生锁竞争,并发性能极差。
- 扩容开销大:触发扩容时,由单个线程完成全部元素的拷贝迁移,过程耗时久。
- 不支持 null:key 和 value 都不允许为 null,否则会抛出空指针异常。
- 现状:锁设计过于原始,性能低下,目前已经被 ConcurrentHashMap 完全替代,仅作为历史遗留类存在,不推荐在新代码中使用。
3. ConcurrentHashMap:高并发场景的最优解
ConcurrentHashMap 是为并发场景设计的线程安全哈希表。
以Java1.8为例,彻底取消了分段锁,改用更细粒度的桶级锁,并做了多处优化:
- 读操作无锁:读取数据时不加锁,通过 volatile 关键字保证数据的内存可见性,直接从主内存读取最新值。
- 写操作锁桶:仅对当前操作的哈希桶(链表头结点)加 synchronized 锁,只有两个线程恰好操作同一个桶时才会冲突,锁粒度远小于分段锁。
- CAS 辅助更新:size 计数等属性通过 CAS 原子更新,避免重量级锁。
- 化整为零扩容:扩容时新老数组同时存在,每个操作哈希表的线程都会协助迁移一部分元素,分散扩容开销,避免单线程长时间阻塞。扩容期间插入只写入新数组,查询需要同时检索新老数组,全部元素迁移完成后再删除老数组。
- 不支持 null:和 Hashtable 一样禁止 null 键值。原因是并发场景下,get 返回 null 无法区分「值本身是 null」还是「key 不存在」,会产生语义歧义。
二、核心演进逻辑:锁粒度不断细化
三者的性能差异,本质上是锁粒度不断细化的演进过程:
- Hashtable:全表一把锁 → 所有操作都冲突,性能最差
- ConcurrentHashMap 1.8:桶级锁 → 同桶才冲突,性能最优
锁的粒度越细,并发冲突的概率就越低,高并发下的性能表现就越好。
三、选型总结
- 单线程环境:优先使用 HashMap,无锁开销,性能最高。
- 多线程环境:使用 ConcurrentHashMap,兼顾线程安全与并发性能,是目前的工业标准方案。
- Hashtable 仅作为历史知识了解,不要在新项目中使用。