MST

星途 面试题库

面试题:Java HashMap扩容机制在高并发场景下可能引发的问题及解决方案

当Java HashMap在高并发场景下进行扩容时,可能会出现哪些问题?针对这些问题,有哪些有效的解决方案?请从原理和实现等多角度进行分析。
50.4万 热度难度
编程语言Java

知识考点

AI 面试

面试题答案

一键面试

高并发场景下HashMap扩容可能出现的问题

  1. 死循环
    • 原理:在JDK 1.7及之前版本,HashMap采用头插法扩容。高并发下,多个线程同时进行扩容操作时,新的Entry链表顺序会与旧链表顺序相反。当一个线程已经完成部分链表迁移,另一个线程继续迁移时,就可能出现链表成环的情况,进而导致在获取元素时陷入死循环。例如,线程A将节点a迁移到新表,线程B在迁移节点b时,因为头插法会改变链表顺序,使得b成为新链表头,然后线程A继续迁移,可能会使a又成为新链表头,最终形成环。
    • 实现角度:在transfer方法(JDK 1.7中扩容时调用)中,通过e.next = newTable[i]; newTable[i] = e;这样的头插法代码实现链表迁移,高并发下容易出现上述问题。
  2. 数据丢失
    • 原理:在高并发场景下,多个线程同时进行put操作,并且在put过程中触发扩容。假设线程A和线程B都要put新元素,在判断是否需要扩容时都通过了检查,然后都开始进行扩容操作。线程A先完成扩容并将元素插入新表,线程B完成扩容后可能会覆盖线程A插入的元素,导致数据丢失。
    • 实现角度:例如在put方法中,在判断容量是否足够后,没有进行适当的同步,多个线程可能同时执行扩容相关操作,如resize方法中的transfer操作,导致数据覆盖。

有效的解决方案

  1. 使用ConcurrentHashMap
    • 原理:在JDK 1.7中,ConcurrentHashMap采用分段锁机制,将整个哈希表分成多个段(Segment),每个段独立加锁,不同段的操作可以并发执行,减少了锁竞争。在JDK 1.8中,采用Node数组加链表或红黑树的数据结构,并且利用CAS(Compare - And - Swap)和synchronized关键字来实现线程安全。在扩容时,它采用了一种更加细粒度的控制方式,允许并发扩容。例如,在扩容时,会将数据迁移到新的数组,并且在迁移过程中,通过设置一些标志位来控制并发操作,避免死循环和数据丢失。
    • 实现角度:JDK 1.8中ConcurrentHashMap的put方法,在链表或红黑树节点操作时,使用synchronized关键字对节点加锁,在数组扩容等操作时,使用CAS操作来保证数据一致性。扩容时,采用多线程分段迁移数据,不会出现死循环和数据丢失问题。
  2. 使用Collections.synchronizedMap包装HashMap
    • 原理Collections.synchronizedMap返回的是一个线程安全的Map,它通过对整个Map的方法进行同步,使用synchronized关键字,在调用put、get等方法时,会对整个Map对象加锁,这样在同一时间只有一个线程可以操作Map,从而避免了高并发下的问题。
    • 实现角度:例如put方法实现如下:
public V put(K key, V value) {
    synchronized (mutex) {return m.put(key, value);}
}

这里mutex是一个对象锁,m是被包装的HashMap。通过对put方法加锁,保证了高并发下操作的原子性,避免了死循环和数据丢失,但由于锁粒度大,并发性能相对较低。