HashMap在多线程并发情况下不安全的原因

本文深入探讨了HashMap在多线程环境下数据丢失的原因,以及在JDK1.7和1.8中并发操作可能引发的数据丢失和死循环问题。

为什么HashMap不是线程安全的

源码阅读详见 https://blog.youkuaiyun.com/leonliu06/article/details/107274223
  • 线程T1和T2同时对一个HashMap进行put操作,如产生hash碰撞,正常情况下,会形成链表,并发情况下,有可能T2线程会覆盖T1线程put的元素。
  • 线程T1和T2同时对一个HashMap进行resize操作,因jdk1.7中,扩容时,移动元素生成新链表是按头插法进行的,可能出现循环链表,使得get一个不存在的元素,且该元素索引位置在循环链表位置时,造成对环形链表的死循环遍历,在jdk1.8中不会。
  • 所以,HashMap的线程不安全主要体现如下:
    • 在JDK1.7中,当并发执行put操作时,会造成数据丢失,并发扩容操作时会造成死循环的情况。
    • 在JDK1.8中,当并发执行put操作时,也会造成数据丢失,但不会形成环形链表,所以不会出现死循环情况。
<think>嗯,用户想深入了解Java多线程环境下HashMap线程安全原因和替代方案。这个问题在并发编程中很关键,尤其是HashMap多线程环境中的行为。从系统指令来看,所有技术术语和代码都需要严格用LaTeX标记,并且必须引用站内资料。用户引用的几篇材料详细分析了线程安全的原理,特别是引用[4]和[5]提到JDK7时代的循环链表问题,引用[3]则指向ConcurrentHashMap解决方案。具体思考点有:JDK7的transfer方法在多线程扩容时会产生循环链表,引用[5]的代码段清楚展示了在e.next操作时可能出现环链。这时候get操作就会CPU飙升。而在JDK8虽然优化为尾插法避免死循环,但仍有数据覆盖问题,如两个线程同时执行put时可能丢失数据。引用[1][2]都指出同步锁方案可行,因为内置锁会让性能退化成Hashtable级别。替代方案方面需要分层推荐:基础场景用Collections.synchronizedMap,如引用[2]所示;高性能场景必须用ConcurrentHashMap,引用[3]提到它采用分段锁降低冲突。特别要强调Java8的ConcurrentHashMap改用CAS+synchronized,连putVal这样的操作都实现精细化同步。最后要生成延展问题:为什么ConcurrentHashMap放弃分段锁?如何理解Java内存模型对此的影响?并发容器选型标准也应该说明。</think> Java多线程环境下HashMap线程安全的主要原因在于其非同步设计,以下是具体原因及解决方案: ### 一、线程安全原因 1. **数据覆盖** 当多个线程同时执行`put()`操作时,若计算出的桶下标相同,会导致后写入的键值对覆盖前一个线程写入的数据。例: ```java // 线程A和B同时执行 map.put(key, valueA); // 线程A map.put(key, valueB); // 线程B // 最终可能只保留valueB ``` 2. **循环链表(JDK7)** 在扩容时执行`transfer()`方法,多线程操作可能导致链表形成闭环(引用[5]): ```java void transfer(Entry[] newTable) { for (Entry<K,V> e : table) { // 多线程操作此循环 while(null != e) { Entry<K,V> next = e.next; // 线程A在此挂起 e.next = newTable[i]; // 线程B先完成扩容 newTable[i] = e; // 导致线程A恢复后形成闭环 }}} ``` *触发get操作时可能引起CPU 100%* 3. **大小计算错误** 多个线程同时修改`size`字段时,由于非原子操作导致最终值小于实际数量: ```java // 实际执行过程: int tmp = size; // 线程A读取size=5 tmp = tmp + 1; // 线程B同时读取size=5 size = tmp; // 最终size=6而非7 ``` 4. **可见性问题** 未使用`volatile`修饰内部数组,线程修改后其他线程可能可见(引用[4])。 ### 二、替代方案 1. **Collections.synchronizedMap** ```java Map<String, String> safeMap = Collections.synchronizedMap(new HashMap<>()); ``` *原理:通过mutex对象锁实现同步(引用[2])* ```java public V put(K key, V value) { synchronized (mutex) { return m.put(key, value); } } ``` 2. **ConcurrentHashMap(推荐)** ```java Map<String, String> concurrentMap = new ConcurrentHashMap<>(); ``` - JDK7:分段锁(Segment),默认16个锁(引用[3]) - JDK8+:CAS+synchronized优化,锁粒度细化到桶头节点 ```java final V putVal(K key, V value) { if ((f = tabAt(tab, i)) == null) { if (casTabAt(tab, i, null, newNode)) break; // CAS插入 } else { synchronized (f) { // 锁住桶头节点 // 链表/红黑树操作 }}} ``` 3. **HashTable(推荐)** ```java Map<String, String> table = new Hashtable<>(); ``` *问题:全表锁导致并发性能差* ### 三、选择建议 | 方案 | 锁粒度 | 适用场景 | |---------------------|------------|-----------------------| | `ConcurrentHashMap` | 桶级锁 | 高并发更新场景(推荐) | | `Collections.synchronizedMap` | 全表锁 | 低并发或读多写少场景 | | `HashTable` | 全表锁 | 废弃方案,推荐使用 | > **关键结论**:HashMap的线程安全本质源于设计目标(单线程性能优先),需根据并发场景选择替代方案: > - 读多写少:`Collections.synchronizedMap` > - 高并发写:`ConcurrentHashMap`(JDK8+性能最优) > - 完全避免:`Hashtable`等全局锁方案(引用[1][2][3]) ---
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值