jdk1.7HashMap与jdk1.8concurrenthashmap出现的死循环问题

HashMap

假设HashMap初始化大小为4,插入个3节点,不巧的是,这3个节点都hash到同一个位置,如果按照默认的负载因子的话,插入第3个节点就会扩容,为了验证效果,假设负载因子是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;

       }

   }

}

以上是节点移动的相关逻辑。

插入第4个节点时,发生rehash,假设现在有两个线程同时进行,线程1和线程2,两个线程都会新建新的数组。

假设 线程2 在执行到 Entry<K,V>next=e.next;之后,cpu时间片用完了,这时变量e指向节点a,变量next指向节点b。

线程1继续执行,很不巧,a、b、c节点rehash之后又是在同一个位置7,开始移动节点

第一步,移动节点a

第二步,移动节点b

注意,这里的顺序是反过来的,继续移动节点c

这个时候 线程1 的时间片用完,内部的table还没有设置成新的newTable, 线程2 开始执行,这时内部的引用关系如下:

这时,在 线程2 中,变量e指向节点a,变量next指向节点b,开始执行循环体的剩余逻辑。

Entry<K,V> next = e.next;

int i = indexFor(e.hash, newCapacity);

e.next = newTable[i];

newTable[i] = e;

e = next;

执行之后的引用关系如下图

执行后,变量e指向节点b,因为e不是null,则继续执行循环体,执行后的引用关系

变量e又重新指回节点a,只能继续执行循环体,这里仔细分析下: 1、执行完 Entry<K,V>next=e.next;,目前节点a没有next,所以变量next指向null; 2、 e.next=newTable[i]; 其中 newTable[i] 指向节点b,那就是把a的next指向了节点b,这样a和b就相互引用了,形成了一个环; 3、 newTable[i]=e 把节点a放到了数组i位置; 4、 e=next; 把变量e赋值为null,因为第一步中变量next就是指向null;

所以最终的引用关系是这样的:

节点a和b互相引用,形成了一个环,当在数组该位置get寻找对应的key时,就发生了死循环。

另外,如果线程2把newTable设置成到内部的table,节点c的数据就丢了,看来还有数据遗失的问题。

总之,千万不要在多线程写时使用HashMap,单写多读是没有问题的。

 

ConcurrentHashMap

先看这么一段代码:

        Map map =new ConcurrentHashMap <>();

        map.computeIfAbsent("a",key -> {

            map.put("a","v2");

            return"v1";

        });

这段代码执行以后"a"对应的value到底是多少呢?

答案是执行这行代码的线程cpu占用会到100%,而且程序不退出。查看线程堆栈出现这样的情况:

"main" #1 prio=5 os_prio=31 tid=0x00007f804f002000 nid=0x1703 runnable [0x0000700000218000]

java.lang.Thread.State: RUNNABLE

at java.util.concurrent.ConcurrentHashMap.putVal(ConcurrentHashMap.java:1069)

at java.util.concurrent.ConcurrentHashMap.put(ConcurrentHashMap.java:1006)

at com.wangqun.HelloComputeIfAbsent.lambda$main$0(HelloComputeIfAbsent.java:14)

at com.wangqun.HelloComputeIfAbsent$$Lambda$1/796533847.apply(Unknown Source)

at java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1660)

- locked <0x000000076b448b10> (a java.util.concurrent.ConcurrentHashMap$ReservationNode)

at com.wangqun.HelloComputeIfAbsent.main(HelloComputeIfAbsent.java:13)

at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)

at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)

at java.lang.reflect.Method.invoke(Method.java:498)

at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)

可以看到put方法和computeIfAbsent方法同时卡在了一个ReservationNode对象上。查看ConcurrentHashMap的源码可以发现,这种情况在bucket没有初始化的时候会发生,简单来说computeIfAbsent会在bucket为null的时候初始化一个ReservationNode来占位,然后等待后面的计算结果出来,再替换当前的占位对象,而putVal会synchorized这个对象,并根据其hash值的正负来进行更新,遗憾的时ReservationNode的hash是-3,在putVal中没有处理过这种情况,然后就一直for循环处理了。

这其实是一种编程bug,computeIfAbsent在使用的时候,计算value的过程中一定不能出现对map的修改操作,否则如果修改的key和computeIfAbsent的key分到同一个桶,而且那个bucket没有被使用过,就会悲剧。

如果非要在计算新值的过程中修改map,可以换一种方法来实现computeIfAbsent的功能:

V value = map.get(k);

if (value == null) {

    V newValue = computeValue(k);  // 这里对computeValue(k)的重复调用不敏感

    value = map.putIfAbsent(k, newValue);

   if (value == null) {

        return newValue;

   }

   return value;

}

对于HashMap.computeIfAbsent,这么调用则没有这种问题出现。


原文地址:

jdk1.7:原文地址

jdk1.8:原文地址​​​​​​​

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值