`
leifengshan
  • 浏览: 6145 次
  • 性别: Icon_minigender_1
  • 来自: 北京
文章分类
社区版块
存档分类
最新评论

[转载]疫苗:Java HashMap的死循环

阅读更多

在淘宝内网里看到同事发了贴说了一个CPU被100%的线上故障,并且这个事发生了很多次,原因是在Java语言在并发情况下使用HashMap造 成Race Condition,从而导致死循环。这个事情我4、5年前也经历过,本来觉得没什么好写的,因为Java的HashMap是非线程安全的,所以在并发下 必然出现问题。但是,我发现近几年,很多人都经历过这个事(在网上查“HashMap Infinite Loop”可以看到很多人都在说这个事)所以,觉得这个是个普遍问题,需要写篇疫苗文章说一下这个事,并且给大家看看一个完美的“Race Condition”是怎么形成的。

 

问题的症状

 

从前我们的Java代码因为一些原因使用了HashMap这个东西,但是当时的程序是单线程的,一切都没有问题。后来,我们的程序性能有问题,所以 需要变成多线程的,于是,变成多线程后到了线上,发现程序经常占了100%的CPU,查看堆栈,你会发现程序都Hang在了HashMap.get()这 个方法上了,重启程序后问题消失。但是过段时间又会来。而且,这个问题在测试环境里可能很难重现。

 

我们简单的看一下我们自己的代码,我们就知道HashMap被多个线程操作。而Java的文档说HashMap是非线程安全的,应该用ConcurrentHashMap。

 

但是在这里我们可以来研究一下原因。

 

Hash表数据结构

 

我需要简单地说一下HashMap这个经典的数据结构。

 

HashMap通常会用一个指针数组(假设为table[])来做分散所有的key,当一个key被加入时,会通过Hash算法通过key算出这个 数组的下标i,然后就把这个<key, value>插到table[i]中,如果有两个不同的key被算在了同一个i,那么就叫冲突,又叫碰撞,这样会在table[i]上形成一个链 表。

 

我们知道,如果table[]的尺寸很小,比如只有2个,如果要放进10个keys的话,那么碰撞非常频繁,于是一个O(1)的查找算法,就变成了链表遍历,性能变成了O(n),这是Hash表的缺陷(可参看《Hash Collision DoS 问题》)。

 

所以,Hash表的尺寸和容量非常的重要。一般来说,Hash表这个容器当有数据要插入时,都会检查容量有没有超过设定的thredhold,如果 超过,需要增大Hash表的尺寸,但是这样一来,整个Hash表里的无素都需要被重算一遍。这叫rehash,这个成本相当的大。

 

相信大家对这个基础知识已经很熟悉了。

 

HashMap的rehash源代码

 

下面,我们来看一下Java的HashMap的源代码。

 

Put一个Key,Value对到Hash表中:

 

public V put(K key, V value)
{
    ......
    //算Hash值
    int hash = hash(key.hashCode());
    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)
{
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
    //查看当前的size是否超过了我们设定的阈值threshold,如果超过,需要resize
    if (size++ >= threshold)
        resize(2 * table.length);
}
 
 

 

新建一个更大尺寸的hash表,然后把数据从老的Hash表中迁移到新的Hash表中。

 

void resize(int newCapacity)
{
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    ......
    //创建一个新的Hash Table
    Entry[] newTable = new Entry[newCapacity];
    //将Old Hash Table上的数据迁移到New Hash Table上
    transfer(newTable);
    table = newTable;
    threshold = (int)(newCapacity * loadFactor);
}
 
 

 

迁移的源代码,注意高亮处:

 

 
void transfer(Entry[] newTable)
{
    Entry[] src = table;
    int newCapacity = newTable.length;
    //下面这段代码的意思是:
    //  从OldTable里摘一个元素出来,然后放到NewTable中
    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 = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            } while (e != null);
        }
    }
}
 

 

好了,这个代码算是比较正常的。而且没有什么问题。

 

正常的ReHash的过程

 

画了个图做了个演示。

 

  • 我假设了我们的hash算法就是简单的用key mod 一下表的大小(也就是数组的长度)。
  • 最上面的是old hash 表,其中的Hash表的size=2, 所以key = 3, 7, 5,在mod 2以后都冲突在table[1]这里了。
  • 接下来的三个步骤是Hash表 resize成4,然后所有的<key,value> 重新rehash的过程

 

 

并发下的Rehash

 

1)假设我们有两个线程。我用红色和浅蓝色标注了一下。

 

我们再回头看一下我们的 transfer代码中的这个细节:

 

1
2
3
4
5
6
7
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);

 

而我们的线程二执行完成了。于是我们有下面的这个样子。

 

 

注意,因为Thread1的 e 指向了key(3),而next指向了key(7),其在线程二rehash后,指向了线程二重组后的链表。我们可以看到链表的顺序被反转后。

 

2)线程一被调度回来执行。

 

  • 先是执行 newTalbe[i] = e;
  • 然后是e = next,导致了e指向了key(7),
  • 而下一次循环的next = e.next导致了next指向了key(3)

 

 

3)一切安好。

 

线程一接着工作。把key(7)摘下来,放到newTable[i]的第一个,然后把e和next往下移

 

 

4)环形链接出现。

 

e.next = newTable[i] 导致  key(3).next 指向了 key(7)

 

注意:此时的key(7).next 已经指向了key(3), 环形链表就这样出现了。

 

 

于是,当我们的线程一调用到,HashTable.get(11)时,悲剧就出现了——Infinite Loop。

 

其它

 

有人把这个问题报给了Sun,不过Sun不认为这个是一个问题。因为HashMap本来就不支持并发。要并发就用ConcurrentHashmap

http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6423457

 

我在这里把这个事情记录下来,只是为了让大家了解并体会一下并发环境下的危险。

参考:http://mailinator.blogspot.com/2009/06/beautiful-race-condition.html

 

转自:http://coolshell.cn/articles/9606.html

分享到:
评论

相关推荐

    疫苗:Java HashMap的死循环

    Java HashMap的死循环原因分析 HashMap是Java中一种常用的数据结构,它提供了快速的查找、插入和删除操作。然而,在多线程环境中使用HashMap可能会导致死循环的问题。下面我们来分析HashMap的死循环原因。 首先,...

    java-hashmap:Java HashMap的插图

    Java HashMap的插图 Java HashMap HashMap类使用哈希表来实现Map接口。 这样,即使对于大型集合,诸如get()和put()之类的基本操作的执行时间也可以保持恒定。 目录 插图1:使用put()方法在HashMap中创建和...

    Java HashMap类详解

    也就是说:Java 集合实际上是多个引用变量所组成的集合,这些引用变量指向实际的 Java 对象。 4. HashMap 的存储实现 HashMap 的存储实现可以通过查看其 put(K key, V value) 方法的源代码来了解。该方法首先判断 ...

    Java魔法解密:揭秘HashMap底层机制.pptx.pptx

    HashMap是Java编程语言中的一种重要数据结构,它是`java.util.HashMap`类的实例,实现了`Map`接口。HashMap主要用于存储键值对,其中键是唯一的,且键和值之间通过哈希函数建立关联,使得在理想情况下,查询、插入和...

    js 版 java hashmap

    JavaScript中的HashMap并不是内置的数据结构,但在许多开发场景中,我们需要实现类似Java中HashMap的功能,用于存储键值对数据。在JavaScript中,我们通常使用对象(Object)来模拟HashMap的行为,因为对象的属性名...

    java HashMap原理分析

    Java HashMap原理分析 Java HashMap是一种基于哈希表的数据结构,它的存储原理是通过将Key-Value对存储在一个数组中,每个数组元素是一个链表,链表中的每个元素是一个Entry对象,Entry对象包含了Key、Value和指向...

    深入了解JAVA HASHMAP的死循环

    然而,由于HashMap不是线程安全的,因此在多线程环境下直接使用可能会引发一系列问题,其中之一就是所谓的"死循环"。本文将深入探讨这一主题。 首先,死循环问题通常发生在并发环境中,当多个线程同时修改HashMap时...

    Java-HashMap.rar_hashmap_java hashmap

    在Java编程语言中,`HashMap`是`java.util`包中的一个核心类,它属于集合框架的一部分,主要用于存储键值对的数据结构。`HashMap`基于哈希表(散列表)实现,提供了快速的插入、删除和查找操作,平均时间复杂度为O(1...

    自定义map实现java的hashmap

    - 线程安全:Java中的HashMap不是线程安全的,如果在多线程环境下使用,需要考虑同步机制,如使用`Collections.synchronizedMap()`或者使用`ConcurrentHashMap`。 - 空值处理:键或值为null的情况需要特殊处理,...

    深入Java集合学习系列:HashMap的实现原理

    在Java编程语言中,集合框架是开发者日常工作中不可或缺的一部分,HashMap作为其中的重要成员,它的实现原理对于理解Java性能优化和数据结构有深远的意义。HashMap是一个基于哈希表的数据结构,它实现了Map接口,...

    StringToHashMap:Java HashMap的扩展,它允许用户从键值对的字符串创建HashMap

    StringToHashMap Java HashMap的扩展,它允许用户从键/值对的String创建HashMap。 输入的String需要2个不同的定界符:1)分隔每个键/值对的定界符,以及2)分隔每个键和值的定界符。

    Java HashMap高难度面试题集锦解析Java HashMap面试题及答案解析-高难度

    Java HashMap 是一个非常重要的数据结构,它在面试中经常被问到,因为它涉及到许多底层实现细节和并发问题。以下是对给定的Java HashMap面试题的详细解析: 1. **HashMap的内部实现原理**: HashMap基于哈希表,...

    JAVA hashmap 负载因子为什么是0.75,官方解释

    java hashmap 扩容因子为什么是0.75,官方给出的解释

    hashmap-thread-test:测试 Java HashMap 是否是线程安全的

    使用Maven的`exec:java`命令执行测试,这将编译并运行项目的主要Java类。 测试代码可能包括以下几个步骤: 1. 创建一个`HashMap`实例。 2. 在多个线程中执行插入、删除或查找操作。 3. 使用同步机制(如`...

    Java中HashMap的工作机制

    在Java中,HashMap是一种广泛使用的数据结构,它基于哈希表的Map接口实现。哈希表是一种通过哈希过程将键映射到特定位置的数据结构,该位置存储了键对应的值。在详细探讨Java中HashMap的工作机制之前,首先需要理解...

    java软件技术文档-深入java8的集合3:HashMap的实现原理.pdf

    HashMap 是 Java 中最常用的集合类之一,它是基于哈希表数据结构实现的,提供快速的存取操作。在深入理解 HashMap 的实现原理之前,我们先要明白哈希表的基本概念。哈希表是一种通过哈希函数将键(Key)映射到数组...

    Java SE程序 HashMap类

    Java SE程序 HashMap类Java SE程序 HashMap类Java SE程序 HashMap类Java SE程序 HashMap类Java SE程序 HashMap类Java SE程序 HashMap类Java SE程序 HashMap类Java SE程序 HashMap类Java SE程序 HashMap类Java SE程序...

Global site tag (gtag.js) - Google Analytics