多线程下HashMap是怎么死循环的?

开发 前端
竞态条件可能会导致程序崩溃、数据损坏、死锁等问题。为了避免竞争条件,可以使用同步机制(如锁、信号量等)来保证只有一个进程或线程可以访问共享资源。

这是一个老问题了,现在都Java21了,又翻出来Java7的问题,算是对历史的总结把。

背过面试八股文的都知道,HashMap是非线程安全的,多线程下要用ConcurrentHashMap之类的。但是实际工作中,还是会碰到在多线程中使用HashMap。可能是写的时候迷糊了,也可能是代码升级时没有注意,比如原本是单线程的,后来性能不行改成多线程了。多线程下使用HashMap,偶尔会出现服务hang死的情况,重启就好,测试环境还复现不了,纯偶发,即Race Condition(竞态条件)。

竞态条件(Race Condition)是指在多线程或者多进程的程序中,由于多个线程或进程之间执行顺序的不确定性,导致程序出现意料之外的结果或者行为。这种情况通常发生在多个线程或进程同时访问共享资源时,其中一个线程或进程修改了共享资源的状态,但其他线程或进程并没有意识到这个修改,导致它们基于过期的状态做出了错误的决策。竞争条件是一种常见的并发编程错误,需要在程序设计和实现时特别注意。

竞态条件可能会导致程序崩溃、数据损坏、死锁等问题。为了避免竞争条件,可以使用同步机制(如锁、信号量等)来保证只有一个进程或线程可以访问共享资源。

那我们来分析下,HashMap是怎么形成竞态条件的。

先梳理一下源码

put方法是入口:

public V put(K key, V value) {
    if (key == null)
        return putForNullKey(value);
    // 计算hash值
    int hash = hash(key);
    int i = indexFor(hash, table.length);
    // 如果该key已被插入,则替换旧的value
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }

    modCount++;
    // 如果该key不存在,增加一个节点
    addEntry(hash, key, value, i);
    return null;
}

检查容量是否超过阈值,超过了就扩容:

void addEntry(int hash, K key, V value, int bucketIndex) {
    // 判断当前的size是否超过阈值,如果超过了,重新resize,也就是扩容
    if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }
    // size未超过阈值或者扩容完成后,增加节点
    createEntry(hash, key, value, bucketIndex);
}

将新增元素插入链表中:

void createEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    size++;
}

扩容重建新的hash表,并将老表中的数据迁移到新表中:

void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }

    // 创建新的hash表
    Entry[] newTable = new Entry[newCapacity];
    boolean oldAltHashing = useAltHashing;
    useAltHashing |= sun.misc.VM.isBooted() &&
            (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
    boolean rehash = oldAltHashing ^ useAltHashing;
    // 将老表中的数据迁移到新表中
    transfer(newTable, rehash);
    table = newTable;
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

老表数据迁移到新表,就是循环遍历旧数据,然后插入的新表中:

void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {
        while(null != e) {
            Entry<K,V> next = e.next;
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity);
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}

上面的这个代码没有什么问题,有问题也不会坚持这么多版本了。

那问题出在哪呢?出在了多线程上,接下来我们画图看看。

看图说话

本节中的例子和图片思路来源于酷壳。

假设我们的hash算法就是key mode表格大小,hash表size是2,当前元素key是3、5、7,mod 2时hash值都是1。执行resize成4时,所有元素迁移到新表数据,过程如下(下图来源于酷壳):

示例示例

上面的过程是正常运行的结果,如果在迁移时有两个现成同时操作(能过到同时迁移,说明前置的都是同时执行的),就会出现问题,还是通过示意图的方式:

怎么解决呢?

想要成功的解决竞态条件问题,保证程序可以正确的按逻辑顺序运行,从理论上应该满足以下四个条件:

  • 不会有两个及以上进程同时出现在他们的critical section;
  • 不要做任何关于CPU速度和数量的假设;
  • 任何进程在运行到critical section之外时都不能阻塞其他进程;
  • 不会有进程永远等在critical section之前。

"Critical section" 是计算机科学中的一个概念,用于描述多个进程或线程访问共享资源时的同步问题。在一个程序中,当多个进程或线程需要同时访问共享资源(如共享内存或文件),为了避免数据竞争和其他同步问题,需要将访问共享资源的代码段限制在一个受保护的区域内,这个区域就称为 "critical section"。在这个区域内,只有一个进程或线程可以访问共享资源,其他进程或线程必须等待,直到进程或线程退出该区域。这样可以确保共享资源的一致性和正确性。

为了避免这种情况的发生,我们可以采用以下几种方法:

  1. 使用ConcurrentHashMap:ConcurrentHashMap是Java提供的一种线程安全的哈希表实现。它采用了分段锁的机制,在多线程环境下能够保证高效并发访问。如果我们需要在多线程环境下使用哈希表,建议使用ConcurrentHashMap来代替HashMap。
  2. 使用Collections.synchronizedMap:Collections.synchronizedMap是Java提供的一种线程安全的Map实现。它通过对Map的所有方法进行同步,来保证线程安全。但是,由于它采用了同步的机制,因此在高并发环境下可能会出现性能瓶颈。
  3. 使用Lock:我们也可以使用Lock来手动控制对HashMap的访问。具体来说,我们可以在对HashMap进行操作时,先获取一个锁,然后再进行操作。这种方式需要我们手动控制锁的释放,因此比较容易出现死锁的情况。
责任编辑:武晓燕 来源: 看山的小屋
相关推荐

2013-06-06 13:34:56

HashMap线程不安全

2020-12-17 07:39:30

HashMap死循环数据

2023-01-31 08:24:55

HashMap死循环

2022-01-24 07:01:20

安全多线程版本

2020-09-29 15:24:07

面试数据结构Hashmap

2022-01-20 08:44:25

HashMap死循环开放性

2022-01-18 06:59:50

HashMap循环底层

2020-05-27 12:45:52

HashMapJava加载因子

2013-06-06 13:10:44

HashMap无锁

2010-03-17 19:24:38

Java多线程循环

2021-06-11 11:28:22

多线程fork单线程

2018-10-10 20:20:14

2011-06-22 16:08:40

Qt 多线程 事件循环

2024-10-16 09:34:50

2020-11-13 07:16:09

线程互斥锁死循环

2024-03-22 12:29:03

HashMap线程

2023-10-19 08:30:58

线程源码thread

2011-10-31 15:59:56

SQLiteiPhoneiOS

2011-09-07 10:13:04

IPv6IPv4

2022-03-04 10:17:04

Redis数据
点赞
收藏

51CTO技术栈公众号