多线程环境下操作HashMap的问题

本文探讨了并发环境下操作HashMap可能导致的循环链表及死循环问题,分析了rehash过程中的并发风险,并对比了不同线程安全集合类的性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

HashMap为什么不是线程安全,并发操作Hashmap会带来什么问题:
这个问题曾经有一个面试官问过我,当时我天真的以为是读写操作并发时存在脏数据的问题,当时面试官不置可否。我后面回来查资料,发现没有那么简单。并发操作HashMap,是有可能带来死循环以及数据丢失的问题的。

具体情况如下:(以下代码转自美团点评技术团队的文章Java8系列之重新认识HashMap)

情景如下代码:

    public class HashMapInfiniteLoop {    
       
        private static HashMap<Integer,String> map = new HashMap<Integer,String>(2,0.75f);    
        public static void main(String[] args) {    
            map.put(5, "C");    
       
            new Thread("Thread1") {    
                public void run() {    
                    map.put(7, "B");    
                    System.out.println(map);    
                };    
            }.start();    
            new Thread("Thread2") {    
                public void run() {    
                    map.put(3, "A);    
                    System.out.println(map);    
                };    
            }.start();          
        }    
    }  


其中,map初始化为一个长度为2的数组,loadFactor=0.75,threshold=2*0.75=1,也就是说当put第二个key的时候,map就需要进行扩容。

考虑这样一种情况:
先放出transfer的部分代码:

    do {  
        Entry<K,V> next = e.next; //假设线程一执行到这里就被调度挂起了  
        int i = indexFor(e.hash, newCapacity);  
        e.next = newTable[i];  
        newTable[i] = e;  
        e = next;  
    } while (e != null);  

线程1、线程2都添加了数据之后,线程1执行到transfer()方法的第一行就被调度挂起了,这时线程2被调度来执行扩容操作。线程2的扩容操作结束之后,线程1被调度回来继续执行,此时由于线程2的执行,e已经指向了线程2修改之后的反转链表,但是线程1并不知道线程2已经在它之前做过这些操作了,于是它继续往下走,此时next=key(7),

然后计算索引。索引计算完之后执行e.next=newTable[i],此时e.next=key(7)。继续往下走,newTable[i]=e,此时newTable[i]=key(3),再往下,e=next,此时e指向了key(7),本次循环结束。从线程二重组链表结束,到线程1第一轮循环结束的变化图如下:


一切看起来都还没有什么问题。然后新一轮循环开始

这一轮循环我们不需要走完,就能发现问题。

第一句,执行后为:next=null;

第二句,计算索引,还是i

第三句,在这里就出问题了,这句话执行的是e.next=newTable[i],我们看上图,newTable[i]指向的是key(3),因此出现链表末尾的元素的next指针指向了链表头,循环链表就出现了。(按道理,HashMap是不存在循环链表的。)

第四句话,将链表头的元素换成key(7),而循环链表依然存在。

第五句,e=null,执行到这循环结束,因为e=null了。

整个过程并不会发生明显的异常。看起来一切安好。顺利的完成了rehash,但是悲剧在后面:当我们调用get()这个链表中不存在的元素的时候,就会出现死循环。go die

一句话总结就是,并发环境下的rehash过程可能会带来循环链表,导致死循环致使线程挂掉。

因此并发环境下,建议使用Java.util.concurrent包中的ConcurrentHashMap以保证线程安全。

至于HashTable,它并未使用分段锁,而是锁住整个数组,高并发环境下效率非常的低,会导致大量线程等待。
同样的,Synchronized关键字、Lock性能都不如分段锁实现的ConcurrentHashMap。

### 多线程环境下的HashMap并发问题与同步技术 #### 使用外部同步机制保护HashMap 为了在多线程环境中安全地使用 `HashMap`,可以采用显式的锁来控制对共享资源的访问。通过引入 `ReentrantLock` 类,可以在更细粒度上实现锁定策略[^4]。 ```java import java.util.HashMap; import java.util.concurrent.locks.ReentrantLock; public class SafeHashMap<K, V> { private final HashMap<K, V> map = new HashMap<>(); private final ReentrantLock lock = new ReentrantLock(); public void put(K key, V value) { lock.lock(); try { map.put(key, value); } finally { lock.unlock(); } } public V get(Object key) { lock.lock(); try { return map.get(key); } finally { lock.unlock(); } } } ``` 这种方法允许开发者精确指定哪些操作应该被序列化执行,从而减少不必要的阻塞时间并提高程序的整体效率。 #### 利用Collections.synchronizedMap封装HashMap 另一种简单的方式是利用 `Collections.synchronizedMap()` 方法将普通的 `HashMap` 转换为线程安全版本: ```java import java.util.Collections; import java.util.Map; import java.util.HashMap; Map<String, String> syncMap = Collections.synchronizedMap(new HashMap<>()); ``` 此方法会在每次调用集合的操作时自动加锁,在大多数情况下能够满足需求;然而需要注意的是,对于复合动作(如迭代遍历),仍然需要额外的手动同步处理[^1]。 #### 运用ConcurrentHashMap替代传统HashMap 考虑到性能因素以及复杂场景的需求,推荐优先考虑使用 `ConcurrentHashMap` 来代替原始的 `HashMap` 。该类不仅提供了更高的吞吐量而且内置了许多实用功能,例如原子更新、批量检索等特性[^3]。 ```java import java.util.concurrent.ConcurrentHashMap; ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>(); concurrentMap.put("key", 1); // 线程安全写入 Integer value = concurrentMap.get("key"); // 线程安全读取 ``` 这种设计使得多个读取者几乎不受限于其他读者的存在而受到影响,并且只有当发生修改时才会涉及到争用情况的发生。
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值