外观
hashmap 多线程情况下使用会出现什么问题
⭐ 题目日期:
美团 - 2024/12/23
📝 题解:
在多线程环境下直接使用 HashMap
会导致一系列问题,主要原因是 HashMap
非线程安全,其内部实现未做同步控制。以下是具体问题和原因分析:
1. 数据不一致(脏读、丢失更新)
- 问题:多个线程同时执行
put()
或get()
时,可能导致读取到过期的数据或更新丢失。 - 原因:
put()
操作非原子性,涉及哈希计算、链表/红黑树插入等步骤,线程A的修改可能被线程B覆盖。get()
可能读到其他线程未完成修改的中间状态(如扩容时的临时不一致状态)。
示例:
// 线程A和线程B同时执行 put("key", 1) 和 put("key", 2)
// 最终结果可能是1或2,而非预期的递增操作。
2. 死循环(JDK 1.7及之前版本)
- 问题:多线程并发扩容时可能导致CPU占用100%(死循环)。
- 原因:
- JDK 1.7的
HashMap
使用头插法转移链表节点,并发扩容时可能形成环形链表(线程A和线程B互相指向对方的节点)。 - 后续调用
get()
或put()
遍历链表时陷入无限循环。
- JDK 1.7的
注意:JDK 1.8改为尾插法,解决了死循环问题,但仍存在数据不一致风险。
3. 扩容时节点丢失
- 问题:多线程同时触发扩容(
resize()
)可能导致部分键值对丢失。 - 原因:
- 扩容时需要重新计算桶位置并迁移节点,多个线程可能覆盖彼此的迁移结果。
示例:
// 线程A迁移了节点1,线程B迁移了节点2,但最终只有其中一个线程的迁移结果生效。
4. 并发修改异常(ConcurrentModificationException
)
- 问题:一个线程遍历
HashMap
(如iterator()
),另一个线程修改结构(put()
/remove()
),会抛出此异常。 - 原因:
HashMap
的迭代器通过modCount
字段检测结构性修改,多线程操作导致该字段不一致。
多线程下 HashMap
的问题总结
问题 | 原因 | JDK版本影响 |
---|---|---|
数据不一致(脏读/丢失) | put() /get() 非原子性,无同步控制 | 所有版本 |
死循环 | JDK 1.7 头插法扩容形成环形链表 | 仅JDK 1.7及之前 |
节点丢失 | 多线程扩容时迁移覆盖 | 所有版本 |
并发修改异常 | 迭代器遍历时检测到其他线程的结构修改(modCount 变化) | 所有版本 |
解决方案
1. 使用线程安全的替代类
ConcurrentHashMap
:- JDK提供的线程安全哈希表,采用分段锁(JDK 1.7)或CAS +
synchronized
(JDK 1.8+),性能高。 - 推荐场景:高并发读写。
- JDK提供的线程安全哈希表,采用分段锁(JDK 1.7)或CAS +
Collections.synchronizedMap()
:- 包装
HashMap
,所有方法用synchronized
加锁,但性能较差(全表锁)。 - 示例:
Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());
- 包装
2. 手动加锁(不推荐)
- 使用
synchronized
或ReentrantLock
控制所有HashMap
操作,但代码复杂且性能低。synchronized (map) { map.put("key", value); }
3. 避免共享状态
- 每个线程使用独立的
HashMap
(如ThreadLocal
封装),但仅适用于特定场景。
为什么 ConcurrentHashMap
更优?
- 分段锁(JDK 1.7):将数据分到多个段(Segment),不同段可并发操作。
- CAS +
synchronized
(JDK 1.8+):仅锁单个桶(Node),进一步降低锁粒度。 - 无死循环风险:设计上避免环形链表问题。
代码验证示例
public class HashMapConcurrencyDemo {
public static void main(String[] args) throws InterruptedException {
Map<String, Integer> unsafeMap = new HashMap<>();
// Map<String, Integer> safeMap = new ConcurrentHashMap<>();
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
unsafeMap.put(Thread.currentThread().getName() + i, i);
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Map size: " + unsafeMap.size());
// 可能输出小于2000(数据丢失)
}
}
输出结果:
使用 HashMap
时,size()
可能小于预期(如1735),而 ConcurrentHashMap
总能保证正确性。
总结
- 不要在多线程中直接使用
HashMap
,优先选择ConcurrentHashMap
。 - 若必须使用
HashMap
,需确保所有操作外部同步(但性能差)。 - JDK 1.8+的
HashMap
虽解决死循环,仍存在数据竞争问题。