在Java面试中,Map系列是高频考点,尤其是HashMap、Hashtable、ConcurrentHashMap三者的区别。本文从底层结构、线程安全、性能、适用场景等维度,结合JDK源码,帮你彻底吃透这些核心概念。无论你是准备面试还是日常开发,都能从中获得实用认知。

在Java开发中,Map是最常用的集合之一,而HashMap、Hashtable、ConcurrentHashMap 三者更是面试高频考点。很多新手容易混淆它们的用法,甚至在项目中用错导致线上问题。本文从底层实现、线程安全、性能、使用场景等维度,拆解三者核心区别,搭配源码分析和面试官追问,帮你吃透考点、避开坑!
一、三者核心区别总览(先建立认知)
HashMap、Hashtable、ConcurrentHashMap 都实现了 Map 接口,用于存储键值对,但在线程安全、性能、底层结构等方面差异显著,直接决定了它们的使用场景。先通过一张流程图快速梳理核心差异:
- HashMap:非线程安全,效率高,允许 null 键值,JDK8 采用数组+链表/红黑树。
- Hashtable:线程安全(全局锁),效率低,不允许 null 键值,数组+链表。
- ConcurrentHashMap:线程安全(分段锁/CAS),效率高,不允许 null 键值,JDK8 采用数组+链表/红黑树+CAS+同步锁。
小提示:建议收藏本文,面试前快速过一遍,轻松应对 Map 相关考题。
二、逐一看透:三者核心特性拆解
2.1 HashMap(最常用,非线程安全)
HashMap 是日常开发中使用频率最高的 Map 实现,主打高效、灵活,但不保证线程安全,也是面试中考查最深的一个。
核心特性:
- 线程安全:非线程安全,多线程环境下修改(如 put、remove)可能导致死循环、数据丢失(JDK7 及之前)、数据错乱(JDK8)。
- null 支持:允许 null 键和 null 值(仅一个 null 键,多个 null 值)。
- 底层结构:JDK7 是“数组+链表”(拉链法),JDK8 优化为“数组+链表/红黑树”(当链表长度超过 8 且数组容量 ≥ 64 时,转为红黑树,提升查询效率)。
- 初始容量与扩容:初始容量 16,负载因子 0.75,扩容时容量翻倍(2 的幂次)。
- 效率:无锁设计,单线程下查询、插入、删除效率极高,时间复杂度接近 O(1)。
源码片段(JDK8 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;
// 数组为空则初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 哈希值计算索引,无冲突直接插入
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
// 有冲突,判断是链表还是红黑树
Node<K,V> e; K k;
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 链表遍历,尾插法(JDK8优化,避免死循环)
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 覆盖已有键的值
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 超过负载因子,扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
面试官追问环节(高频):
- 追问 1:JDK7 和 JDK8 中 HashMap 的区别有哪些?
✅ 答:① 底层结构:JDK7 数组+链表,JDK8 数组+链表/红黑树;② 插入方式:JDK7 头插法(多线程易死循环),JDK8 尾插法;③ 哈希计算:JDK8 简化扰动函数;④ 扩容机制:JDK8 扩容时无需重新计算哈希值(利用原哈希值高位判断)。 - 追问 2:HashMap 为什么不允许重复键?如何保证?
✅ 答:通过哈希值 + equals 方法判断键是否重复。put 时,先通过哈希值找到索引,再遍历链表/红黑树,若存在“哈希值相等且 equals 为 true”的键,则覆盖原值,否则插入新节点。
2.2 Hashtable(线程安全,已过时)
Hashtable 是早期 Java 提供的线程安全 Map 实现,底层设计较老旧,目前已基本被 ConcurrentHashMap 替代,仅在遗留项目中可能见到。
核心特性:
- 线程安全:通过给所有方法加 synchronized 锁(全局锁)实现,即整个 Hashtable 对象加锁。
- null 支持:不允许 null 键和 null 值,否则抛出 NullPointerException。
- 底层结构:始终是“数组+链表”,无红黑树优化,查询效率低于 JDK8 的 HashMap 和 ConcurrentHashMap。
- 初始容量与扩容:初始容量 11,负载因子 0.75,扩容时容量 = 原容量 * 2 + 1(非 2 的幂次,哈希冲突概率高于 HashMap)。
- 效率:全局锁导致多线程并发访问时效率极低,相当于单线程执行。
关键问题:为什么 Hashtable 不推荐使用?
- 效率低:全局synchronized锁,多线程竞争时阻塞严重,性能远不如ConcurrentHashMap;
- 设计老旧:无红黑树优化,查询效率低,扩容机制不合理;
- 不支持null键值:使用场景受限,不如HashMap灵活。
面试官追问环节(高频):
- 追问:Hashtable 和 HashMap 的线程安全差异,本质是什么?
✅ 答:本质是锁粒度不同。Hashtable 采用全局锁(锁整个对象),所有线程操作都需竞争同一把锁;HashMap 无锁,非线程安全。而 ConcurrentHashMap 通过细分锁粒度(分段锁/CAS),实现了高效的线程安全。
2.3 ConcurrentHashMap(高效线程安全,推荐使用)
ConcurrentHashMap 是 JDK5 引入的,专门用于高并发场景,兼顾线程安全和性能,是目前多线程环境下的首选 Map 实现。
核心特性(JDK8 vs JDK7):
- JDK7 版本:底层结构为“分段数组+链表”,将数组分为 16 个分段(Segment),每个分段对应一把锁。线程安全通过分段锁机制实现,多个线程可同时操作不同分段,并发效率高于 Hashtable。缺点:仍有锁竞争,且结构复杂。
- JDK8 版本(重大优化):底层结构与 HashMap 一致,“数组+链表/红黑树”,移除分段锁。线程安全采用“CAS + 同步锁(synchronized 修饰链表头/红黑树根节点)”实现,锁粒度进一步细化(仅锁冲突的节点),并发效率大幅提升。
- null 支持:不允许 null 键和 null 值(避免并发场景下的空指针问题)。
- 效率:多线程环境下,查询、插入、删除效率接近 HashMap,远高于 Hashtable。
源码片段(JDK8 put 方法核心逻辑,体现线程安全):
public V put(K key, V value) {
return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
// 禁止null键值
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(); // 初始化数组,CAS保证线程安全
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; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
// 有冲突,对链表头/红黑树根节点加锁
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.value;
if (!onlyIfAbsent)
e.value = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key, value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) {
oldVal = p.value;
if (!onlyIfAbsent)
p.value = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
面试官追问环节(高频):
- 追问 1:JDK8 中 ConcurrentHashMap 为什么移除分段锁?改用 CAS + synchronized 的优势是什么?
✅ 答:① 移除分段锁原因:分段锁虽比全局锁好,但仍有锁竞争,且结构复杂;② 优势:CAS 无锁操作,适合无冲突场景;synchronized 仅锁冲突节点,锁粒度极细,同时 JDK8 对 synchronized 做了优化(偏向锁、轻量级锁),性能开销低。 - 追问 2:ConcurrentHashMap 为什么不允许 null 键值?
✅ 答:避免并发场景下的歧义。比如 get(key) 返回 null 时,无法判断是“键不存在”还是“键对应的值是 null”,多线程环境下可能导致业务逻辑错误。
三、三者核心区别对比表(面试速记)
| 特性 | HashMap | Hashtable | ConcurrentHashMap |
|---|---|---|---|
| 线程安全 | 非线程安全 | 线程安全(全局锁) | 线程安全(CAS+同步锁) |
| null支持 | 允许null键和null值 | 不允许null键和null值 | 不允许null键和null值 |
| 底层结构(JDK8) | 数组+链表/红黑树 | 数组+链表 | 数组+链表/红黑树 |
| 锁机制 | 无锁 | 全局synchronized锁 | CAS+节点级synchronized锁 |
| 初始容量 | 16 | 11 | 16 |
| 扩容机制 | 翻倍(2的幂次) | 原容量*2+1 | 翻倍(2的幂次) |
| 并发效率 | 单线程极高,多线程不安全 | 极低 | 高(多线程首选) |
| 适用场景 | 单线程环境、非并发场景 | 遗留项目、低并发场景(不推荐) | 多线程高并发场景 |
面试速记:单线程用 HashMap,高并发用 ConcurrentHashMap,Hashtable 直接放弃!
[AFFILIATE_SLOT_2]四、总结
本文从底层实现、线程安全、性能、使用场景等维度,详细拆解了 HashMap、Hashtable、ConcurrentHashMap 的核心区别,搭配源码分析和面试官追问,帮你吃透 Map 面试高频考点。
- HashMap:单线程首选,高效灵活,允许 null 键值,非线程安全。
- Hashtable:线程安全但效率极低,不允许 null 键值,已过时,不推荐使用。
- ConcurrentHashMap:高并发首选,兼顾线程安全和性能,不允许 null 键值,JDK8 优化后性能大幅提升。
如果你觉得本文对你有帮助,欢迎点赞、收藏,关注我(予枫),后续持续分享 Java 面试干货和源码解析~
浙公网安备 33010602011771号