HashMap、HashTable、ConcurrentHashMap 都是以哈希表为底层实现的 K-V 存储容器,面试高频对比点,核心差异集中在线程安全、null 值支持、扩容机制、锁粒度、性能、继承关系 。
1. 基础概念
- HashTable
- JDK1.0 诞生,属于古老集合类
- 线程安全,锁是对象锁(synchronized 修饰方法)
- 底层:数组 + 链表(JDK1.8 没有升级红黑树)
- HashMap
- JDK1.2 诞生,日常开发最常用
- 非线程安全
- JDK1.7:数组 + 链表;JDK1.8:数组 + 链表 / 红黑树(链表长度≥8 转红黑树,≤6 退化为链表)
- ConcurrentHashMap
- JDK1.5 引入,并发场景替代 HashTable
- 线程安全,分段锁(JDK1.7)/ CAS + synchronized 锁桶头节点(JDK1.8)
- JDK1.7:Segment 分段数组 + HashEntry;JDK1.8:结构和 HashMap 保持一致(数组 + 链表 + 红黑树)
2. 逐项对比
2.1 线程安全
- HashTable :线程安全 ,但是所有
put/get/remove等方法都加synchronized,锁整个哈希表对象。多个线程竞争同一把锁,并发下阻塞严重,性能差。 - HashMap :线程不安全 ,无任何同步措施,多线程并发读写会出现数据丢失、死循环(JDK1.7 扩容链表头插法)、脏读,并发场景禁止直接使用。
- ConcurrentHashMap :线程安全的哈希表
- JDK1.7:Segment 分段锁,将整个哈希表分成多个段,不同段之间可以并发读写,同一 Segment 内互斥。
- JDK1.8:效率更高,按照桶级别加锁,降低锁冲突概率 。(抛弃 Segment,采用 CAS + synchronized,只锁住当前要操作的桶(数组下标位置的头节点),锁粒度极小,并发性能远高于 HashTable。)
线程安全代码演示:
HashMap 在多线程并发 put 会发生数据丢失,HashTable、ConcurrentHashMap 保证数据安全。
java
package org.Thread;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class MapThreadSafeTest {
private static final int Count=10000;
//接收任意一种 Map,开启两个线程往里面 put 数据,
// 等两个线程全部跑完,打印最终 size。
public static void testMap(Map<Integer,Integer> map) throws InterruptedException {
Thread t1=new Thread(()->{
for (int i = 0; i < Count; i++) {
map.put(i,i);
}
});
Thread t2=new Thread(()->{
for (int i = Count; i < Count*2; i++) {
map.put(i,i);
}
});
t1.start();
t2.start();
t1.join();
t2.join();
//等线程跑完之后,打印map里面的元素
System.out.println(map.getClass().getSimpleName()+"size:"+map.size());
}
public static void main(String[] args) throws InterruptedException {
//测试HashMap:非线程安全,大概率丢失数据,输出size<20000
testMap(new HashMap<>());
//测试Hashtable:线程安全,最终size一定等于20000
testMap(new Hashtable<>());
//测试ConcurrentHashMap:线程安全,最终size一定等于20000
testMap(new ConcurrentHashMap<>());
}
}
结果如下:

通过结果我们能知道 HashMap是线程不安全,而 Hashtable 和 ConcurrentHashMap是线程安全的
2.2 是否允许 null key /null value
- HashTable :key 和 value 都不能为 null 。put 时会直接判断,空值抛出
NullPointerException。 - HashMap :允许一个 null key,多个 null value。null key 固定放在下标 0 位置。
- ConcurrentHashMap :key 和 value 都不允许 null 。原因:并发场景下无法区分
get(key) == null是 key 不存在,还是存入的 value 本身就是 null,存在歧义。
2.3 继承与实现关系
- HashTable:继承 Dictionary(古老抽象类),实现 Map
- HashMap:继承 AbstractMap,实现 Map
- ConcurrentHashMap:实现 ConcurrentMap(继承 Map)
Dictionary 是遗留类,现在基本废弃,推荐使用 AbstractMap。
2.4 初始容量 & 扩容规则
- HashTable :默认初始容量 11 ;扩容公式:
newCapacity = oldCapacity * 2 + 1,容量始终是奇数。 - HashMap :默认初始容量 16 ;容量必须是 2 的幂;扩容:
oldCapacity << 1(翻倍)。 容量为 2 的幂是为了(n - 1) & hash代替取模运算,提升哈希寻址效率。 - ConcurrentHashMap
- JDK1.7:Segment 默认 16 段,每个 Segment 初始容量 2;
- JDK1.8:和 HashMap 一致,默认容量 16,2 的幂,扩容翻倍。
2.5 遍历方式 & 快速失败
- HashTable、HashMap 的迭代器是
fail-fast(快速失败):遍历过程中其他线程修改结构(新增删除),抛出ConcurrentModificationException。 - ConcurrentHashMap 迭代器是
weakly consistent(弱一致性):遍历期间其他线程的修改不一定立刻感知,不会抛出并发修改异常,适合并发遍历。
2.6 使用场景
- HashMap:单线程环境,优先使用;查询插入性能最好。多线程下需要外部手动加锁。
- HashTable:基本淘汰。并发性能太差,新项目不使用。
- ConcurrentHashMap:多线程并发读写场景,优先选择。
3. JDK1.8 重点变化小结(高频面试)
- HashMap:链表过长转为红黑树,优化长链表查询 O (n) -> O (logn)
- ConcurrentHashMap:移除 Segment 分段锁,改用 CAS + 锁桶头节点,锁粒度大幅降低,性能提升
- HashTable:没有改动,依旧是方法上加 synchronized,锁整张表
4. 总结
- HashMap:单线程首选,无锁,支持 null,性能高,线程不安全。
- HashTable:古董线程安全 Map,锁整张表,不支持 null,并发弱,基本废弃。
- ConcurrentHashMap:并发场景首选,细粒度锁 / CAS,不支持 null,兼顾安全和并发性能。