Java基础:HashMap假死锁问题的测试、分析和总结

前言

  前两天在公司的内部博客看到一个同事分享的线上服务挂掉CPU100%的文章,让我联想到HashMap在不恰当使用情况下的死循环问题,这里做个整理和总结,也顺便复习下HashMap。

直接上测试代码

  由于机器配置和性能不同,测试出效果的线程数和put数量也各不相同

public class HashMapInfiniteLoopTest {
    /**
     * 基于JDK1.7测试HashMap在多线程环境下假死锁的情况
     * JDK1.8的HashMap实现跟1.7的比较有很大的变化,已不存在这样的问题
     * (这本来不是JDK的一个问题,HashMap本就不是线程安全的,多线程环境下共享一定要用线程安全的Map容器)
     */
    public static void main(String[] args) {
        String jdkVer = System.getProperty("java.version"); //JDK版本
        String jdkMod = System.getProperty("sun.arch.data.model"); //32位还是64位
        System.out.println(jdkVer +"#"+ jdkMod);

        final Map<String, String> map = new HashMap<>();
//        final Map<String, String> map = new ConcurrentHashMap<>();
        for(int i=0; i<30; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                    for(int j=0; j<1000; j++) {
                        map.put(""+j+"_"+System.currentTimeMillis(), ""+j+"_"+System.currentTimeMillis());
                    }
                }
            }, "myThread_"+i).start();
        }
    }
}

  通过jconsole查看Java进程情况:

  最后只能强制结束进程

 

分析

  HashMap使用hash表来作为其底层存储的数据结构(通过数组下标实现快速索引,链表实现元素碰撞处理),并且支持动态扩容,主要通过resize方法实现,也是从这个方法开始出问题的。(这里有两个面试官喜欢问的点:1.table的默认长度以及扩容前后大小?2.为什么要求table的长度必须是2的N次方?)

  因为整个HashMap都不是线程安全的,所以resize也未做同步,如果错误的在多线程环境下共享了HashMap就有可能引起我前面提到的假死锁问题。动态扩容的时候需要把旧的链表迁移到新的hash表中,如果是在多线程环境下,可能会形成循环链表,然而这个时候貌似一切正常,只有在再次put并遍历每个链表检查是否存在相同key,死循环就出现了(如果是get也会有同样的情况)。

下面是我整理转载自https://coolshell.cn/articles/9606.html的部分内容(写得太好了):


1

2

3

4

5

6

7

8

9

10

11

12

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);

}

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


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

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);

        }

    }

}

  • 假设我们的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(7)时,悲剧就出现了——Infinite Loop。

总结

  多线程并发环境下访问共享的map时一定要用线程安全的Map容器,如ConcurrentHashMap,HashTable等。

原文地址:https://www.cnblogs.com/ocean234/p/9063379.html

时间: 2024-11-05 17:22:36

Java基础:HashMap假死锁问题的测试、分析和总结的相关文章

java 关于 hashmap 的实现原理的测试

网上关于HashMap的工作原理的文章多了去了,所以我也不打算再重复别人的文章.我就是有点好奇,我怎么样能更好的理解他的原理,或者说使用他的特性呢?最好的开发就是测试~ 虽说不详讲hashmap的工作原理,但是起码的常识还是要提一下的. 一句话:hashmap最直观的表现是一维数组或者说一维字典,但是每个每个值又可以指向另一个数组或都字典! 一张图: 其实说实话,给我个人的感觉是,说链表只是显得高大上些罢了,当然这片面的理解,不过也是令人误解的地方. 我曾经就以为,链表真是个高大上的东西,那我怎

不惑JAVA之JAVA基础 - HashMap

HashMap应该是平时应用开发中或是框架设计中最为常用高效的容器.在介绍HashMap之前,先介绍两个常见的区别.后期会专门介绍CurrentHashMap. hashmap 和 hashtable 区别 HashMap和HashTable有什么区别,一个比较简单的回答是: HashMap是非线程安全的,HashTable是线程安全的. HashMap的键和值都允许有null值存在,而HashTable则不行. 因为线程安全的问题,HashMap效率比HashTable的要高. hashmap

Java基础--HashMap面试题

import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * HashMap问题 * @author 15735400536 * 使用HashMap,如果key是自定义的类,就必须重写hashcode()和equals() * 使用ArrayList,如果元素是自定义的类,要做相等判断,就必须重写hashCode()和equals() */ publ

[Java基础]HashMap的那些事

提到HashMap,使用Java语言的人来说,是再熟悉不过了.今天就简单聊聊我们认识的HashMap; 首先我们看一下Java中的HashMap类 public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { private static final long serialVersionUID = 362498820763181265L;

java 基础 ---HashMap、HashTable

HashMap.HashTable区别. 1.HashMap线程不安全,HashTable线程安全: 2.HashMap的键和值都允许null值存在,而HashTable不允许: 3.HashMap的效率高于Hashtable * Hash table based implementation of the <tt>Map</tt> interface.  This * implementation provides all of the optional map operatio

Java基础——HashMap源码分析

本篇介绍的HashMap综合了ArrayList和LinkedList这两个集合的优势,它的底层是基于哈希表实现的,如果不考虑哈希冲突的话,HashMap在增删改查操作上的时间复杂度都能够达到惊人的O(1). 对于HashMap类源码中开头注释翻译: HashMap基于哈希表的 Map 接口的实现.此实现提供所有可选的映射操作,并允许使用 null 值和 null 键.(除了不同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同.)此类不保证映射的顺序,特别是它不保

java基础---HashMap和HashTable的异同之处

1:连个都实现了Map的接口,两者的底层数据结构相同,都是transient修饰的entry数组,transient修饰的变量不会序列化即不会持久化,序列化的过程中不会包含这个变量,这个变量的生命周期仅仅是在用户的调用过程中,不能持久化到内存磁盘中.这样便于一些敏感信息的保存 2:HashMap是不安全的,不能同步,不支持多线程并发,HashTable是安全的,有同步锁,但效率低. 3:HashMap从AbstractMap继承而来,HashTable是从Dictionary继承而来 4:Has

java基础-HashMap

jdk7的HashMap实现的思路比较简单,就是一个Entry数组,数组中每个Entry都是一个链表的起点(表头). 1 public V put(K key, V value) { 2 if (table == EMPTY_TABLE) { 3 inflateTable(threshold); 4 } 5 //如果key为null,则将该entry放在第0位 6 if (key == null) 7 return putForNullKey(value); 8 int hash = hash(

java基础----&gt;hashMap的简单分析(一)

HashMap是一种十分常用的数据结构对象,可以保存键值对.它在项目中用的比较多,今天我们就来学习一下关于它的知识. HashMap的简单使用 一.hashMap的put和get方法 Map<String, String> map = new HashMap<>(); map.put("username", "huhx"); map.put("password", "1234"); map.put(nu