HashMap就像一个没有锁的储物柜,当多人(多线程)同时存取物品时,容易引发混乱。以下是其线程不安全的核心原因:
1. 数据覆盖(丢失更新)
-
场景:两个线程同时向同一个位置存数据。
-
原理:
- 线程A和线程B同时计算键的哈希值,定位到同一个桶(数组位置)。
- 若该位置为空,两线程都会尝试插入新节点。
- 结果:后插入的线程覆盖前一个线程的数据,导致数据丢失。
-
示例:
// 线程A和线程B同时执行: map.put("key", "A"); map.put("key", "B"); // 最终可能只存入了"B",而"A"被覆盖。
2. 扩容死循环(JDK 1.7及之前)
-
场景:多线程同时触发扩容(如元素数量超过阈值)。
-
原理:
- JDK 1.7使用头插法转移链表节点,导致链表顺序反转。
- 并发扩容时,线程A和线程B可能互相修改节点的
next指针。 - 结果:链表成环,后续查询陷入死循环,CPU飙升。
-
示意图:
原链表:A → B → null 线程A转移后:B → A → null 线程B转移时修改指针,导致:A → B → A(循环) -
JDK 1.8改进:改用尾插法,避免反转链表,但数据覆盖问题仍存在。
3. 大小(size)不准确
-
场景:多线程同时插入或删除元素。
-
原理:
size变量记录当前元素数量,但更新操作非原子性。- 线程A和线程B同时执行
put(),各自读取size=10,然后都改为11。 - 结果:实际新增两个元素,但
size只显示增加1。
-
代码示例:
// 线程A和线程B同时执行: map.put(key, value); // 各自认为size应+1 // 最终size可能少计1。
4. 红黑树结构破坏(JDK 1.8+)
-
场景:多线程同时操作红黑树(当链表长度≥8时转换)。
-
原理:
- 红黑树的平衡操作涉及复杂的指针修改。
- 并发插入或删除时,可能导致树结构不一致。
-
结果:查询结果错误或抛出异常。
如何解决?
-
使用线程安全的替代类:
ConcurrentHashMap:分段锁或CAS机制,高并发下性能更好。Collections.synchronizedMap():给整个Map加锁,性能较差。
-
示例:
Map<String, String> safeMap = new ConcurrentHashMap<>(); safeMap.put("key", "value"); // 线程安全
总结
HashMap线程不安全的本质:缺乏同步机制,导致多线程操作共享数据时出现竞态条件。
核心问题:数据覆盖、链表成环(旧版)、size不准、结构破坏。
解决方案:换用ConcurrentHashMap或手动加锁。
口诀:
「HashMap 像没锁的柜,多线程用会崩溃
数据覆盖死循环,大小不准让人累
若要安全又高效,ConcurrentHashMap 才对味!」