← 每日面试题 学习看板

HashMap、Hashtable和ConcurrentHashMap的区别?

面试题/00java/01Java 基础/05 集合/HashMap、Hashtable和ConcurrentHashMap的区别?.md

面试记录 2026-10-09

题目:HashMap、Hashtable 和 ConcurrentHashMap 的区别?

难度:low

标签:java/collections, general/knowledge

评分:⭐⭐⭐☆☆(3/5)

我的回答

Hashmap,Hashtable,ConcurrentHashMap 三者核心底层都是 HashMap,都是用来存储 key value 类型的内容。最大的差异其实是对于并发的支持。HashMap 本身不支持并发,多线程并发,它是线程多线程下不安全的。而 Hashtable 跟 ConcurrentHashMap 都是多线程下线程安全的,但是它们两个的机制不一样,Hashtable 是在整体上整个方法上就添加了 synchronized 锁。所冲突的,在进行对应的数组上添加数据的时候才会添加锁。也就是说 ConcurrentHashMap 的加锁力度要比 Hashtable 的要少。目前 JDK 已经不推荐使用 Hashtable,因为它加锁的力度太大,效率太低了。常规正常情况下还是使用 HashMap,然后只有在存在并发的场景使用 ConcurrentHashMap。

点评

优点:

不足:

补充:

完整讲解

第一部分:知识重点剖析

核心考点定位:验证是否掌握三种 Map 的线程安全模型差异、JDK 演进、以及并发场景的正确选型。

知识点拆解:

  1. 底层原理:三者本质上都是哈希表实现,差异主要在锁模型和空值策略。HashMap 单线程裸奔,Hashtable 全表锁 + 拒绝 null,ConcurrentHashMap 分段→桶级细粒度锁 + 允许更高并发。
  2. 并发模型差异:Hashtable 把每个方法都 synchronized,锁的是 this(整个表),任何读/写都串行化;ConcurrentHashMap JDK7 用 16 个 Segment,每个 Segment 是独立的锁对象,JDK8 进一步细化到桶头节点,读操作几乎无锁(volatile 读 + CAS),只有同桶写入才竞争。
  3. 设计权衡:全表锁简单但吞吐瓶颈;分段锁粒度可控但 1.7 时代有 Segment 上限;桶级锁配合 CAS 是 JDK8+ 的甜蜜点。选型的本质是"锁粒度 vs 正确性成本"的权衡。
  4. 工程实践:默认用 HashMap;单线程转并发优先 ConcurrentHashMap;只需要"包一层同步"的低并发场景可用 Collections.synchronizedMap;Hashtable 只在对接遗留 API 时保留。
  5. 迭代器语义:HashMap fail-fast(ConcurrentModificationException);Hashtable 事实上弱一致但被官方标注为不可靠;ConcurrentHashMap 弱一致迭代器是设计保证,不抛 CME。
按「底层原理 → 设计权衡 → 工程实践」递进排列。

第二部分:考察方向与延伸递进

考察方向:

延伸递进面试题:

  1. ConcurrentHashMap JDK7 和 JDK8 锁结构有什么变化?为什么这样改?
  1. ConcurrentHashMap 如何保证 size() 是准的?
  1. 什么时候用 Collections.synchronizedMap,什么时候用 ConcurrentHashMap?
  1. 为什么 HashMap 在 JDK8 引入红黑树但 Hashtable 一直没有?

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,这三个 Map 底层都是哈希表结构,最大差异在并发模型和空值策略。

HashMap 单线程裸奔,不保证线程安全,但性能最好;允许 1 个 null key 和多个 null value,默认容量 16,JDK8 里当桶内链表长度超过 8 且整个表容量超过 64 时会转成红黑树。

Hashtable 是 JDK1.0 的遗留物,把每个公共方法都加了 synchronized,锁的是整张表,读也串行化;同时它不接受 null key 或 null value,会抛 NPE。JDK5 之后官方就不推荐使用它了。

ConcurrentHashMap 是并发场景的默认选择。它的演进很有意思:JDK1.7 用 16 个 Segment 分段锁,每个 Segment 内部还有一张小 HashMap,一次加锁只锁一个 Segment;JDK1.8 进一步去掉 Segment,改用桶头节点的 synchronized 加 CAS——空桶写入用 CAS,非空桶写入锁桶头,扩容时用 sizeCtl 协助多线程迁移节点。这样写冲突的粒度细化到桶级,读操作几乎无锁。

迭代器也有差异:HashMap 和 Hashtable 都是 fail-fast,遍历时被修改会抛 ConcurrentModificationException;ConcurrentHashMap 是弱一致迭代器,遍历时允许修改,不抛异常,但可能读到修改前后的任意快照——这在读多写少的高并发统计场景非常合适。

选型上,单线程用 HashMap;只在极少数低并发场景需要线程安全时可以用 Collections.synchronizedMap 包一层;一旦进入真正并发场景,直接用 ConcurrentHashMap;Hashtable 只在对接历史 API 时才见到。