Java高频面试之集合-18
hello啊,各位观众姥爷们!!!本baby今天来报道了!哈哈哈哈哈嗝🐶
面试官:HashMap 是线程安全的吗?多线程下会有什么问题?
HashMap 的线程安全性分析
HashMap 不是线程安全的,在多线程环境下使用可能导致数据不一致、死循环等问题。以下是详细分析:
一、多线程下的主要问题
-
数据覆盖(Lost Updates)
- 场景:两个线程同时执行
put
操作,且键的哈希值相同。 - 原因:线程 A 和 B 同时检测到桶为空,均尝试插入新节点,导致后插入的值覆盖前一个。
- 示例:
// 线程A和B同时执行 map.put(key1, value1); // 若两个线程的 key1 哈希到同一桶且桶为空 map.put(key1, value2); // 最终可能只有 value2 被保留
- 场景:两个线程同时执行
-
链表成环(Infinite Loop)
- JDK 1.7 问题:扩容时采用头插法,多线程并发扩容可能导致链表形成环形结构,后续
get
操作触发死循环。 - JDK 1.8 改进:改用尾插法,但并发扩容仍可能导致数据丢失或链表断裂。
- JDK 1.7 问题:扩容时采用头插法,多线程并发扩容可能导致链表形成环形结构,后续
-
Size 计算错误
- 原因:
size
字段的自增操作(size++
)非原子性,多线程并发修改可能导致最终值小于实际插入数。
- 原因:
-
哈希表状态不一致
- 场景:线程 A 在扩容过程中,线程 B 并发插入数据,导致部分数据未被正确迁移到新数组。
二、问题根源
- 非原子操作:
put
、resize
等操作涉及多个步骤(如哈希计算、链表遍历、节点插入),未加锁导致中间状态暴露。 - 可见性问题:线程本地缓存与主内存不同步,导致读取到过期数据。
三、验证示例(JDK 1.7 链表成环)
// 线程A和B并发执行以下代码
public void unsafePut() {
Map<Integer, Integer> map = new HashMap<>(2);
for (int i = 0; i < 10000; i++) {
new Thread(() -> map.put(ThreadLocalRandom.current().nextInt(), 1)).start();
}
}
- 结果:可能出现 CPU 占用 100%(死循环)或数据丢失。
四、解决方案
-
使用线程安全容器
ConcurrentHashMap
:分段锁(JDK 1.7)或 CAS + synchronized(JDK 1.8+),保证高并发下的安全性和性能。Collections.synchronizedMap
:通过同步方法包装 HashMap,但性能较低。
-
显式同步控制
Map<String, String> syncMap = new HashMap<>(); // 每次操作时加锁 synchronized (syncMap) { syncMap.put(key, value); }
-
避免共享状态
- 线程局部存储(ThreadLocal):每个线程使用独立的 HashMap 实例。
五、性能对比
方案 | 线程安全 | 性能 | 适用场景 |
---|---|---|---|
HashMap | 否 | 高 | 单线程或只读多线程环境 |
ConcurrentHashMap | 是 | 中高 | 高并发读写场景 |
synchronizedMap | 是 | 低 | 低并发场景,需兼容旧代码 |
🐮☺️
- HashMap 非线程安全:多线程下可能导致数据覆盖、死循环、size 错误等问题。
- 替代方案:优先选择
ConcurrentHashMap
,或在必要时使用显式同步。 - 设计建议:在并发编程中,始终使用线程安全的数据结构以避免潜在风险。