Java中HashMap 多线程操作导致死循环问题?

在 Java 的面试中,关于 HashMap 在多线程环境下的表现常常是一个热门话题。特别是 JDK1.7 及之前版本的 HashMap 在多线程扩容时可能会导致死循环的问题。接下来,我们将深入剖析这个问题,并通过源码解读和示例代码进行详细解释。

HashMap 的基本结构

首先,我们了解一下 HashMap 的基本结构。HashMap 是基于数组和链表实现的哈希表。每个哈希桶(bucket)中存储的是一个链表或红黑树(在 JDK1.8 以后),用来解决哈希冲突。

java

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;

    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }

    // other methods
}

扩容问题

HashMap 的扩容发生在容量超过阈值时(默认是容量的 0.75 倍)。在 JDK1.7 及之前的版本中,HashMap 扩容时会重新计算每个元素的哈希值,并将其放置在新的数组位置。

JDK1.7 中的链表头插法

在 JDK1.7 中,扩容过程使用了链表的头插法,这可能导致多线程操作下链表形成环形结构。

java

void transfer(Entry[] newTable) {
    Entry[] src = table;
    int newCapacity = newTable.length;
    for (int j = 0; j < src.length; j++) {
        Entry<K,V> e = src[j];
        if (e != null) {
            src[j] = null;
            do {
                Entry<K,V> next = e.next;
                int i = e.hash & (newCapacity - 1);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            } while (e != null);
        }
    }
}

问题分析

在多线程环境下,如果两个线程同时对同一个桶进行扩容操作,可能会导致链表形成环形结构。例如,假设两个线程同时对桶中的链表进行操作:

  1. 线程 A 读取链表中的元素,并准备插入新的哈希桶。
  2. 线程 B 也读取同一个链表中的元素,并准备插入新的哈希桶。
  3. 线程 A 插入元素到新的哈希桶,并更新链表的头节点。
  4. 线程 B 插入元素,但由于链表已经被线程 A 修改,导致链表形成环形结构。

这时,如果有线程试图遍历这个链表,由于链表已经形成环形结构,会导致死循环。

JDK1.8 中的改进

为了避免这个问题,JDK1.8 中的 HashMap 改用了尾插法,使得插入的节点总是放在链表的末尾,避免了链表中的环形结构。

java

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
        newCap = oldCap << 1;
        newThr = oldThr << 1;
    } else {
        newCap = (oldThr > 0) ? oldThr : DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(newCap * DEFAULT_LOAD_FACTOR);
    }
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    threshold = newThr;
    table = newTab;
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                else {
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        } else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loHead != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiHead != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

尾插法原理

尾插法在插入节点时,总是将新节点插入到链表的末尾。这避免了链表在多线程操作下倒置的问题,从而避免了环形链表的形成。

多线程环境下的解决方案

尽管 JDK1.8 中改进了 HashMap 的扩容机制,但在多线程环境下使用 HashMap 仍然不是安全的,因为仍然可能存在数据覆盖的问题。因此,在多线程环境下,推荐使用 ConcurrentHashMap

ConcurrentHashMap 通过分段锁(JDK1.7)和 CAS 操作(JDK1.8)来保证线程安全。

JDK1.7 中的分段锁

JDK1.7 中,ConcurrentHashMap 使用了分段锁的机制,每一个分段(Segment)相当于一个小的哈希表,并且拥有自己的锁。这样在多线程访问时,不同分段上的操作可以并行进行,从而提高了并发性能。

JDK1.8 中的 CAS 操作

在 JDK1.8 中,ConcurrentHashMap 摒弃了分段锁,转而使用 CAS 操作和内置锁来保证并发安全。具体实现中,使用了 Unsafe 类的 compareAndSwap 方法来实现无锁的线程安全操作。

java

private final boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v) {
    return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}

性能对比示例

为了更直观地理解 HashMap 和 ConcurrentHashMap 在多线程环境下的表现,我们可以编写一个简单的性能对比示例。

java

import java.util.HashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class MapPerformanceTest {

    private static final int THREAD_COUNT = 1000;
    private static final int OPERATIONS = 100000;

    public static void main(String[] args) throws InterruptedException {
        HashMap<Integer, Integer> hashMap = new HashMap<>();
        ConcurrentHashMap<Integer, Integer> concurrentHashMap = new ConcurrentHashMap<>();

        System.out.println("Testing HashMap...");
        long hashMapTime = testMapPerformance(hashMap);
        System.out.println("HashMap Time: " + hashMapTime + " ms");

        System.out.println("Testing ConcurrentHashMap...");
        long concurrentHashMapTime = testMapPerformance(concurrentHashMap);
        System.out.println("ConcurrentHashMap Time: " + concurrentHashMapTime + " ms");
    }

    private static long testMapPerformance(final java.util.Map<Integer, Integer> map) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(THREAD_COUNT);
        long startTime = System.currentTimeMillis();

        for (int i = 0; i < THREAD_COUNT; i++) {
            executorService.execute(() -> {
                for (int j = 0; j < OPERATIONS; j++) {
                    int key = (int) (Math.random() * OPERATIONS);
                    map.put(key, key);
                }
            });
        }

        executorService.shutdown();
        executorService.awaitTermination(1, TimeUnit.HOURS);

        long endTime = System.currentTimeMillis();
        return endTime - startTime;
    }
}

通过这个示例,我们可以直观地看到在多线程环境下,ConcurrentHashMap 的性能远优于 HashMap

结论

在多线程环境中,JDK1.7 及之前版本的 HashMap 的确存在死循环的问题,JDK1.8 通过改进扩容机制避免了这个问题。然而,为了保证线程安全,推荐在多线程环境下使用 ConcurrentHashMap,其通过分段锁和 CAS 操作提供了更高效的并发性能。

  • 17
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值