HashMap线程安全问题与解决方案

HashMap线程安全问题与解决方案
pfa目录
HashMap 线程安全问题与解决方案
一句话定位
HashMap 是线程不安全的,多线程环境下扩容会导致 1.7 环形链表死循环、1.8 数据覆盖/丢失;解决方案用 ConcurrentHashMap。
一、HashMap 扩容时线程不安全在哪?
场景 1:JDK 1.7 环形链表导致死循环
| 现象 | CPU 100%,线程卡在 HashMap.get() 方法里 |
|---|---|
| 根源 | 头插法 + 多线程同时扩容 → 链表形成环 |
| 版本 | 只在 JDK 1.7 及之前存在,1.8 改为尾插法解决了这个问题 |
场景 2:数据覆盖/丢失
| 现象 | 多线程 put 时,某些元素丢失了,或者 size 计数不对 |
|---|---|
| 根源 | 不是原子操作:size++、判断数组位置为空后并发插入 |
| 版本 | JDK 1.7 和 1.8 都存在这个问题 |
二、JDK 1.7 死循环问题复现
核心原因:头插法 + 多线程扩容
1.7 的扩容迁移用的是头插法,多线程同时迁移同一个链表时,顺序会反转,多个线程交错执行就可能形成环形链表。
详细过程
假设有两个线程 T1 和 T2,同时扩容同一个链表:
1 | 原链表:A → B → C → null |
当调用 get 查找一个不存在的 key 时,遍历链表会陷入死循环,CPU 100%。
三、JDK 1.8 还有线程安全问题吗?
1.8 用尾插法解决了环形链表死循环问题,但依然线程不安全:
问题 1:size 计数丢失
1 | // size++ 不是原子操作 |
- 线程 A 读取 size=10,准备加1
- 线程 B 读取 size=10,准备加1
- 结果两个线程加完后 size=11,少加了一次
问题 2:put 元素覆盖
1 | // 判断数组位置为空后可能被插入 |
- 线程 A 判断 tab[i] == null,准备插入
- 线程 B 也判断 tab[i] == null,插入
- 结果 A 的值被 B 覆盖了
四、线程安全的解决方案
方案 1:Collections.synchronizedMap
1 | Map<String, String> map = Collections.synchronizedMap(new HashMap<>()); |
- 原理:包装一层,所有方法加
synchronized锁,锁的是整个 map 对象 - 优点:简单,一行代码
- 缺点:性能差,多线程只能串行执行,锁粒度太大
方案 2:Hashtable
1 | Map<String, String> map = new Hashtable<>(); |
- 原理:所有方法加
synchronized锁,锁的是整个 Hashtable 对象 - 优点:古老但稳定
- 缺点:性能差,不允许 key/value 为 null,很少用了
方案 3:ConcurrentHashMap(推荐)
| JDK 版本 | 实现原理 |
|---|---|
| 1.7 | 分段锁(Segment),默认 16 段 |
| 1.8+ | CAS + synchronized,锁粒度是数组的头节点/红黑树根节点 |
JDK 1.8 的 ConcurrentHashMap 核心优化:
- 读操作无锁(volatile 保证可见性)
- 写操作锁住链表头或红黑树根
- 锁粒度更小:只锁同一个索引位置的节点
- 空指针检查:key 和 value 都不能为空
1 | // 推荐用法 |
五、面试回答话术(结构版)
回答 1:HashMap 为什么线程不安全?
分三点回答(由主到次):
1. JDK 1.7 的环形链表问题
- 1.7 扩容用头插法,多线程同时迁移同一个链表时会形成环
- 导致 get 查找不存在的 key 时陷入死循环,CPU 100%
- 1.8 改成尾插法解决了这个问题
2. 数据覆盖/丢失
- 多线程 put 时,判断数组位置为空后被另一个线程抢先插入
- 后来的值会覆盖先来的值
- 1.7 和 1.8 都有这个问题
3. size 计数不准确
- size++ 不是原子操作
- 多线程同时加可能少加
回答 2:怎么解决 HashMap 线程安全问题?
先说结论(推荐 ConcurrentHashMap),再逐个对比:
1. 推荐:ConcurrentHashMap
- 1.7 用分段锁,1.8 用 CAS + synchronized 锁头节点
- 锁粒度小,读操作无锁,性能最好
2. Collections.synchronizedMap
- 包装一层,所有方法加 synchronized,锁整个 map
- 优点:简单
- 缺点:性能差,多线程串行
3. Hashtable
- 古老实现,全方法加 synchronized
- 不允许 key/value 为 null
- 基本不用了
评论
匿名评论隐私政策






