HashTable、HashMap、ConcurrentHashMap 区别总结
目录
3. ConcurrentHashMap(JDK1.5 出现)
在 Java 集合框架中,HashTable、HashMap、ConcurrentHashMap 都是基于哈希表实现的键值对存储结构,日常开发、面试中经常被问到它们的区别。很多人容易记混,尤其是后两者的并发特性,今天就从基础到进阶,把三者的区别讲透,没有复杂术语,适合新手复习巩固。
先给大家一个核心定位,一句话分清三者的核心差异,记牢这个,后面的细节就好理解了:
HashMap:非线程安全,效率高,日常单线程场景首选;
HashTable:线程安全,但效率极低,基本被淘汰;
ConcurrentHashMap:线程安全,效率高,并发场景首选。
下面我们从「线程安全」「底层实现」「使用场景」等核心维度,一步步拆解,结合实际使用感受,避免干巴巴的对比。
一、先搞懂:三者的“出身”和核心定位
在讲区别之前,先明确三者的基本定位,知道它们各自是为了解决什么问题而生的,后续的区别就顺理成章了。
1. HashMap(JDK1.2 出现)
最基础、最常用的哈希表实现,核心目标是「高效存储和查询」,完全不考虑线程安全。它的设计初衷就是单线程场景下使用,所以没有任何锁机制,效率最高,但多线程并发修改时,会出现数据错乱、死循环等问题(比如扩容时的环形链表)。
日常开发中,只要没有多线程修改操作,比如单线程读取配置、存储临时数据,用 HashMap 就够了,简单、高效、轻量。
2. HashTable(JDK1.0 出现)
比 HashMap 出现得早,核心目标是「解决线程安全问题」,但它的实现方式很“粗暴”——给整个哈希表加了一把全局锁(synchronized 修饰方法)。也就是说,不管多个线程操作的是哈希表的哪个位置,都会竞争同一把锁,一个线程执行,其他所有线程都要阻塞等待。
这种方式虽然保证了线程安全,但效率极低,尤其是并发量高的时候,所有线程排队执行,相当于“单线程运行”,所以现在基本被淘汰,日常开发中很少用到,除非是极其古老的项目。
3. ConcurrentHashMap(JDK1.5 出现)
既然 HashMap 非线程安全,HashTable 效率低,ConcurrentHashMap 就应运而生了——它兼顾了「线程安全」和「高效并发」。它的核心思路是“分段锁”(JDK1.8 优化为 CAS + synchronized 局部锁),不再是全局锁,而是给哈希表的不同分段(或节点)加锁,多个线程操作不同分段时,互不干扰,可以并行执行,大大提升了并发效率。
现在的并发场景,比如多线程读写缓存、处理请求数据,基本都用 ConcurrentHashMap,是三者中最实用、最常用的。
二、核心区别详解(重点,面试必问)
下面从 6 个关键维度,对比三者的差异,结合通俗解读,不用死记硬背,理解背后的逻辑即可。
1. 线程安全性(最核心区别)
HashMap:非线程安全。多线程并发修改(比如 put、remove)时,会出现数据覆盖、死循环(JDK1.7 及之前)、ConcurrentModificationException(快速失败)等问题;即使是多线程读取,也可能读到脏数据(因为没有可见性保证)。
HashTable:线程安全。所有方法都用 synchronized 修饰,全局一把锁,保证同一时刻只有一个线程操作整个哈希表。但正因为全局锁,并发效率极低。
ConcurrentHashMap:线程安全。JDK1.7 用“分段锁”(Segment),将哈希表分成多个分段,每个分段一把锁,多线程操作不同分段可并行;JDK1.8 优化为“CAS + synchronized 局部锁”,只给当前操作的节点加锁,并发效率进一步提升,比 HashTable 高一个量级。
补充:HashMap 不是完全不能用于多线程,比如多线程只读(不修改),或者用 Collections.synchronizedMap() 包装后使用,但包装后的效率不如 ConcurrentHashMap。
2. 底层实现与锁机制
HashMap:JDK1.7 是“数组 + 链表”; JDK1.8 优化为“数组 + 链表 + 红黑树”(当链表长度超过 8 时,转为红黑树,提升查询效率);无锁机制,完全依赖单线程环境保证安全。
HashTable:底层是“数组 + 链表”(始终没有红黑树优化);全局锁(synchronized 修饰方法),锁粒度极大。
ConcurrentHashMap:
JDK1.7:数组 + Segment(分段)+ 链表,每个 Segment 是一个独立的哈希表,各自有一把锁,锁粒度是“分段”;
JDK1.8:去掉 Segment,改为“数组 + 链表 + 红黑树”,锁粒度是“节点”(只锁当前操作的链表/红黑树节点),用 CAS 保证原子性,synchronized 保证节点锁,效率更高。
3. 允许的键值(null)
这是一个容易忽略的细节,日常使用中很容易踩坑:
HashMap:允许键为 null,允许值为 null(键只能有一个 null,值可以有多个 null);
HashTable:不允许键为 null,也不允许值为 null,否则会抛出 NullPointerException;
ConcurrentHashMap:不允许键为 null,也不允许值为 null(和 HashTable 一致),因为并发场景下,null 会导致无法判断是“键不存在”还是“值为 null”,容易出现逻辑错误。
4. 并发效率
效率排序(从高到低):ConcurrentHashMap > HashMap > HashTable
HashMap:无锁,单线程下效率最高,多线程下不安全,无法用于并发修改;
ConcurrentHashMap:锁粒度小,多线程并发时,可并行操作不同分段/节点,效率接近 HashMap;
HashTable:全局锁,多线程并发时,所有线程排队,效率最低,基本被淘汰。
5. 扩容机制
HashMap:初始容量 16,负载因子 0.75,扩容时容量翻倍(16→32→64...),JDK1.7 扩容时会出现环形链表(死循环),JDK1.8 修复了这个问题;
HashTable:初始容量 11,负载因子 0.75,扩容时容量翻倍 +1(11→23→47...),扩容效率低,且全局锁会导致扩容期间所有线程阻塞;
ConcurrentHashMap:JDK1.7 中,每个 Segment 独立扩容,互不干扰;JDK1.8 扩容时,采用 CAS + 多线程协助扩容,效率比 HashTable 高很多,且不影响其他线程的读写操作。
6. 迭代器特性(快速失败 vs 安全失败)
HashMap:迭代器是“快速失败”(fail-fast),迭代过程中,如果其他线程修改了哈希表(put、remove),会抛出 ConcurrentModificationException;
HashTable:迭代器是“快速失败”,和 HashMap 一致;
ConcurrentHashMap:迭代器是“安全失败”(fail-safe),迭代过程中,其他线程可以修改哈希表,不会抛出异常,因为迭代器遍历的是副本数据,而非原始数据。
三、一张表看懂所有区别(面试直接背)
| 对比维度 | HashMap | HashTable | ConcurrentHashMap |
|---|---|---|---|
| 线程安全 | 非线程安全 | 线程安全(全局锁) | 线程安全(分段锁/CAS+局部锁) |
| 底层实现(JDK1.8) | 数组+链表+红黑树 | 数组+链表 | 数组+链表+红黑树 |
| 允许 null 键/值 | 允许(键1个,值多个) | 不允许 | 不允许 |
| 并发效率 | 单线程最高,多线程不安全 | 极低 | 高(并发首选) |
| 扩容机制 | 容量翻倍,JDK1.8 修复死循环 | 容量翻倍+1,效率低 | 多线程协助扩容,效率高 |
| 迭代器特性 | 快速失败(fail-fast) | 快速失败(fail-fast) | 安全失败(fail-safe) |
| 适用场景 | 单线程存储、查询 | 古老项目,基本淘汰 | 多线程并发读写、缓存等 |
四、实际开发怎么选?(重点,落地性强)
不用纠结,记住这3条,直接对应场景选择,不会出错:
如果是 单线程场景(比如单线程读取配置、处理临时数据):选 HashMap,效率最高,简单易用;
如果是 多线程并发场景(比如多线程读写缓存、处理请求参数):选 ConcurrentHashMap,兼顾线程安全和效率,是目前的最优解;
如果是 古老项目维护,遇到 HashTable:不用刻意替换,只要并发量不高,能正常运行就好;新项目绝对不要用 HashTable。
五、总结(通俗好记)
HashMap 是“单线程效率王”,不安全但够用;HashTable 是“老古董”,安全但低效,基本淘汰;ConcurrentHashMap 是“并发全能王”,既安全又高效,是现在并发场景的首选。
更多推荐



所有评论(0)