HashMap在多线程下访问下导致死循环问题

HashMap在jdk1.8之前,会因为多线程put元素操作共享hashmap会出现,原因是向链表添加元素时采用的是头插法,多线程操作链表会发生环化,此时产生死循环

HashMap 的死循环问题 是多线程 扩容的时候有可能会产生,下面来看下扩容方法的源码:
resize() 函数

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

        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);  //transfer函数的调用
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

主要看 resize() 函数 里面 的 transfer 方法 的源码…

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);  //重新 元素在新table中的下标,
                e.next = newTable[i];  
                newTable[i] = e; //将旧table元素放到新table中
                e = next;
            }
        }
    }

在这里插入图片描述

这是在单线程的正常情况下,当HashMap<K,V>的容量不够之后的扩容操作,将旧表中的数据赋给新表中的数据.正常情况下,就是上面图片显示的那样.新表的数据就会很正常,并且还需要说的一点就是,进行扩容操作之后,在旧表中key值相同的数据块在新表中数据块的连接方式会逆向.就拿key = 3和key = 7的两个数据块来说,在旧表中是key = 3 的数据块指向key = 7的数据块的,但是在新表中,key = 7的数据块则是指向了key = 3的数据块key = 5 的数据块不和二者发生冲突,因此就保存到了 i = 1 的位置(这里的hash算法采用 k % hash.size() 的方式).这里采用了这样简单的算法无非是帮助我们理解这个过程,当然在正常情况下算法是不可能这么简单的.

这样在单线程的情况下就完成了扩容的操作.其中不会出现其他的问题…但是如果是在并发的情况下就不一样了.并发的情况出现问题会有很多种情况.这里我简单的说明俩种情况.我们来看图。
在这里插入图片描述

这张图说明了两种死循环的情况.第一种相对而严还是很容易理解的.第二种可能有点费劲…
第二种情况总结下:

  1. 已经循环一次了,key=3 已经放在了新数组的正确位置,此时 再次循环
  2. e 此时已经 来到了 key=7 的数据块位置 即 e = 7;
  3. next = e. next = null, 因为 上一次循环中 e.next= newTable[i] = null,
    但是有一点我们需要记住,图中t1和t2拿到的是同一个内存单元对应的数据块.而不是t1拿到了一个独立的数据块,t2拿到了一个独立的数据块…这是不对的…之所以发生系循环的原因就是因为拿到的数据块是同一个内存单元对应的数据块.这点我们需要注意…正是因为在高并发的情况下线程的工作方式是不确定的,我们无法预知线程的工作情况.因此在高并发的情况下,我们不要使用多线程对HashMap<K,V>进行操作,否则我们都不知道到底是哪里出了问题.

如果大家想在并发场景下使用HashMap,有两种解决方法:
1、使用ConcurrentHashMap。
2、使用Collections.synchronizedMap(Mao<K,V> m)方法把HashMap变成一个线程安全的Map。

JDK1.8 后的 HashMap也 会发生死循环

实验环境是jdk1.8.0_60,我们程序的含义是两个线程向同一个map添加元素,分别添加50000个不重复的元素,程序如下

public class HashMapMultiThread {

    static Map<String,String> map = new HashMap<>();

    public static class AddThread implements Runnable{

        int start;
        public AddThread(int start){
            this.start=start;
        }
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName());
            //添加元素
            for(int i = start ; i<10000000;i+=2){
                map.put(Integer.toString(i),Integer.toBinaryString(i));
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        //开启两个线程
        Thread t1 = new Thread(new AddThread(0));
        Thread t2 = new Thread(new AddThread(1));
        t1.start();
        t2.start();
        //主线程等待两个线程执行完
        t1.join();
        t2.join();
        System.out.println(map.size());
    }
}

该程序预测会产生三种结果
1.程序正常运行,得出结果为10万元素
2.结果小于10万,比如94509
3.产生死循环,程序永远无法结束

经过多次实验,没有出现第一种结果,第二种结果和第三种结果可以得到,这时就可以得出一个结论多线程并发操作共享hashmap是线程不安全的,多个线程操作hashmap同一个位置,由于hashmap没有线程可见性,此时后一个线程会将前一个线程添加的元素覆盖掉(第二种结果说明),有时会产生死循环(第三种结果)

验证死循环结果

我们使用jps和jstack拿到线程dump,观察该java进程下各个线程的运行状态

C:\Users\SJS>jps
30336 Main
21048 HashMapMultiThread

C:\Users\SJS>jstack 21048

打印出的堆栈信息如下

Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.60-b23 mixed mode):
       //重点看这里
"Thread-1" #15 prio=5 os_prio=0 tid=0x000000001d389000 nid=0x1340 runnable [0x000000001ddce000]
   java.lang.Thread.State: RUNNABLE
        at java.util.HashMap$TreeNode.balanceInsertion(HashMap.java:2221)
        at java.util.HashMap$TreeNode.treeify(HashMap.java:1930)
        at java.util.HashMap$TreeNode.split(HashMap.java:2153)
        at java.util.HashMap.resize(HashMap.java:713)
        at java.util.HashMap.putVal(HashMap.java:662)
        at java.util.HashMap.put(HashMap.java:611)
        at com.thinkcoder.concurrenterror.HashMapMultiThread$AddThread.run(HashMapMultiThread.java:38)
        at java.lang.Thread.run(Thread.java:745)

"Thread-0" #14 prio=5 os_prio=0 tid=0x000000001d38b000 nid=0x98c4 runnable [0x000000001dcce000]
   java.lang.Thread.State: RUNNABLE
        at java.util.HashMap$TreeNode.putTreeVal(HashMap.java:2002)
        at java.util.HashMap.putVal(HashMap.java:637)
        at java.util.HashMap.put(HashMap.java:611)
        at com.thinkcoder.concurrenterror.HashMapMultiThread$AddThread.run(HashMapMultiThread.java:38)
        at java.lang.Thread.run(Thread.java:745)

       //主线程在等待,是由于join的效果
"main" #1 prio=5 os_prio=0 tid=0x0000000003644000 nid=0x96c8 in Object.wait() [0x000000000343e000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x0000000702a2f420> (a java.lang.Thread)
        at java.lang.Thread.join(Thread.java:1245)
        - locked <0x0000000702a2f420> (a java.lang.Thread)
        at java.lang.Thread.join(Thread.java:1319)
        at com.thinkcoder.concurrenterror.HashMapMultiThread.main(HashMapMultiThread.java:49)

从线程堆栈信息中可以看出,Thread0和Thread1处于运行状态而main(主)线程处于等待状态,就是等着Thread0和Thread1执行完。但是无奈啊,这两个线程都在正常运行但是程序一直结束不了,这就是死循环的现象

我们按照Thread1的线程信息定位到balanceInsertion方法第2221行代码
在这里插入图片描述
断点调试该行代码,发现该方法中的for循环不会终止,确实发现了死循环现象
在这里插入图片描述为什么会发生死循环呢?
那咱们得研究下balanceInsertion方法,方法名的意思是平衡插入,方法的作用就是,当向已经树化的桶位添加元素时,为了保持红黑树的特性,需要对树进行重新结构化。

分析一下balanceInsertion方法源代码

 static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
                                             TreeNode<K,V> x) {
     //新插入的节点标为红色
     x.red = true;
     
     //无限for循环,定义xp、xpp、xppl、xppr变量,在循环体进行赋值,p就是parents
     //- root:当前根节点
     //- x   :新插入的节点
     //- xp  :新插入节点的父节点
     //- xpp :新插入节点的祖父节点
     //- xppl:新插入节点的左叔叔节点
     //- xppr:新插入节点的右叔叔节点
     for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
     	 
     	 //为定义的各个变量赋值的过程
         if ((xp = x.parent) == null) {
             x.red = false;
             return x;
         }
         else if (!xp.red || (xpp = xp.parent) == null)
             return root;
         //重点看这里
         //如果父节点是爷爷节点的左孩子
         if (xp == (xppl = xpp.left)) {
         	 //如果右叔叔不为空且为红色
             if ((xppr = xpp.right) != null && xppr.red) {
             	 //右叔叔变为黑色
                 xppr.red = false;
                 //父节点变为黑色
                 xp.red = false;
                 //爷爷节点变为黑色
                 xpp.red = true;
                 //将爷爷节点当作起始节点,再次循环,请注意再次循环!!!
                 x = xpp;
             }
		//省略其他代码
 }

总结一下上边的源码就是,新插入一个节点,该方法要保持红黑树的五个性质

性质1. 节点是红色或黑色。
性质2. 根节点是黑色。
性质3 每个叶节点(NIL节点,空节点)是黑色的。
性质4 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
性质5. 从任一节点到其每个叶子的路径上包含的黑色节点数量都相同。

发现一个问题,根节点、爷爷节点、父节点、左叔叔节点、右叔叔节点、新插入的节点都是一个元素667700,证明当前循环的树只有一个值,并且永远不会退出,因为它满足下面两个判断条件

//如果父节点是爷爷节点的左孩子
if (xp == (xppl = xpp.left)) {
 	 //如果右叔叔不为空且为红色
     if ((xppr = xpp.right) != null && xppr.red)

总结:

所以 不管事 jdk1.8以前还是以后的HashMap在多线程的情况下都会出现死循环的问题,1.8以前是在 多线程同时扩容的时候,可能会出现环形链表,导致死循环的发生。 1.8以后 是在链表转换树或者对树进行操作(重新平衡红黑树)的时候会出现死循环的问题。而且 多线程情况下使用hashMap 还会有 数据丢失的问题(因为各线程之前 的 map 是不可见的),所以 多线程情况下 建议使用 concurrentHahsMap 代替 hashMap

文章来源:https://blog.csdn.net/shang_0122/article/details/117423601
https://www.cnblogs.com/RGogoing/p/5285361.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值