HashMap 为什么线程不安全?
面试频率:★★★★★
工作频率:★★★★★
⚡30 秒速记(复习只看这里)
🟢 一句话
HashMap 线程不安全,因为多个线程同时修改数组、链表、红黑树时,没有同步控制,会导致数据覆盖、数据丢失。
🟡 HashMap 底层结构
JDK8:
数组(table)
↓
链表
↓
红黑树(链表长度 >= 8 且数组长度 >= 64)
🟠 三个核心概念
1. Hash 决定桶位置
hash(key)
↓
计算 index
↓
决定放数组哪个位置
2. 链表解决 Hash 冲突
多个 key:
hash 一样
↓
进入同一个桶
↓
形成链表
例如:
1号桶
↓
张三
↓
李四
↓
王五
3. 扩容会重新计算位置
扩容:
数组长度变大
↓
重新计算 index
↓
重新搬迁元素
hash 不变。
但是数组位置可能变化。
📚 完整知识
一、HashMap 底层结构
HashMap 本质:
数组 + 链表 + 红黑树
结构:
table
0
1 ──> Node
|
↓
Node
2
3 ──> Node
数组中的每一个位置叫:
桶(bucket)
二、put("张三") 发生了什么?
执行:
map.put("张三", 1);
流程:
key
↓
hash()
↓
计算数组下标 index
↓
找到桶
↓
没有元素
↓
直接放入
例如:
数组
0
1 ──> 张三
2
3
三、为什么需要链表?
因为可能出现 Hash 冲突。
例如:
张三.hash()
↓
1号桶
李四.hash()
↓
也是1号桶
如果直接覆盖:
张三 ❌
李四
数据丢失。
所以:
使用链表。
变成:
1号桶
↓
张三
↓
李四
四、扩容(resize)是什么?
当 HashMap 元素太多:
数组容量不够。
例如:
扩容前:
数组长度 = 4
0
1 ──> 张三
|
↓
李四
2
3 ──> 王五
HashMap 创建更大的数组:
数组长度 = 8
0
1
2
3
4
5
6
7
然后:
把旧数组里的元素重新计算位置。
这个过程:
叫搬迁。
五、扩容后 hash 会变化吗?
不会。
例如:
张三.hashCode()
扩容前:
123456
扩容后:
123456
hash 不变。
变化的是:
数组下标。
例如:
扩容前:
index = hash % 4
123456 % 4
↓
1号桶
扩容后:
index = hash % 8
123456 % 8
↓
5号桶
所以:
hash 一样,但是位置可能不同。
六、头插法是什么?(JDK1.7)
注意:
头插法针对的是:
同一个桶里面的链表。
不是整个 HashMap。
例如:
原链表:
1号桶
张三
↓
李四
↓
王五
搬迁时:
先搬张三:
张三
再搬李四:
放链表头:
李四
↓
张三
再搬王五:
继续放头:
王五
↓
李四
↓
张三
特点:
新元素插入链表头部。
七、尾插法是什么?(JDK1.8)
JDK8 改成尾插法。
例如:
原:
张三
↓
李四
↓
王五
搬迁:
保持顺序:
张三
↓
李四
↓
王五
不会反转。
八、为什么 JDK1.7 扩容会死循环?
原因:
多线程同时扩容。
JDK1.7:
使用头插法。
两个线程:
同时修改链表 next 指针。
可能形成:
张三
↓
李四
↓
王五
↑ |
|______|
形成环。
之后:
执行:
map.get(key)
遍历链表:
张三
↓
李四
↓
王五
↓
张三
↓
李四
...
永远结束不了。
导致:
CPU 100%。
九、为什么 JDK1.8 还线程不安全?
虽然:
JDK8 修复了死循环。
但是:
仍然存在:
1. 数据覆盖
两个线程同时 put:
线程A 写入
↓
线程B覆盖
↓
A的数据丢失
2. 数据丢失
扩容过程中:
多个线程同时修改数组。
可能导致:
部分数据没有迁移成功。
所以:
HashMap:
❌ 多线程环境不能直接使用。
十、ConcurrentHashMap 为什么安全?
ConcurrentHashMap:
JDK8:
主要使用:
CAS
+
synchronized
保证并发修改安全。
相比:
Collections.synchronizedMap()
性能更好。
💼 工作场景
错误:
Map<String,Object> cache =
new HashMap<>();
多个线程:
put()
get()
高并发情况下:
可能:
数据异常
偶发Bug
数据丢失
正确:
单线程:
HashMap
多线程:
ConcurrentHashMap
🎤 面试回答(30秒)
HashMap 是线程不安全的,因为它内部使用数组加链表或者红黑树结构存储数据,在多线程环境下多个线程同时执行 put、resize 等操作时,没有同步机制保护,可能导致数据覆盖和数据丢失。JDK1.7 扩容时采用头插法,多线程情况下可能形成环形链表导致死循环。JDK1.8 改成尾插法解决了死循环问题,但是 HashMap 仍然不是线程安全的,所以并发场景一般使用 ConcurrentHashMap。
🎯 面试追问
Q1:Hash 决定什么?
答:
决定数组桶的位置。
不是决定链表顺序。
Q2:链表什么时候出现?
答:
多个 key 经过 hash 后进入同一个数组位置,也就是发生 Hash 冲突。
Q3:扩容后两个元素还在一起吗?
答:
不一定。
hash 不变。
但是数组长度变化,index 重新计算。
可能分开。
Q4:头插法插在哪里?
答:
插入同一个桶里的链表头部。
不是数组头。
Q5:为什么 JDK8 不会死循环?
答:
因为改成尾插法,避免扩容时链表顺序反转导致环形成。
⚠️ 工作踩坑
✅ 多线程缓存使用 HashMap
问题:
数据异常。
✅ 认为 JDK8 HashMap 线程安全
错误。
JDK8 只是解决扩容死循环。
✅ 不了解 Hash 冲突
误以为:
一个 hash 对应一个元素。
实际上:
多个元素可能进入同一个桶。
❓ 自测
-
HashMap 底层结构是什么?
-
为什么需要链表?
-
hash 一样,扩容后位置一定一样吗?
-
什么是搬迁?
-
头插法插的是哪里?
-
JDK1.7 为什么会死循环?
-
JDK1.8 为什么仍然线程不安全?
-
多线程环境为什么用 ConcurrentHashMap?