HashMap在多线程下不安全问题(JDK1.7)

本文分析是基于JDK1.7的HashMapjava

多线程状况下HashMap所带来的问题

  1. 多线程put操做后,get操做致使死循环。
  2. 多线程put操做,致使元素丢失。

死循环场景重现

public class HashMapTest extends Thread {

    private static HashMap<Integer, Integer> map = new HashMap<>(2);
    private static AtomicInteger at = new AtomicInteger();

    @Override
    public void run() {
        while (at.get() < 1000000) {
            map.put(at.get(), at.get());
            at.incrementAndGet();
        }
    }


    public static void main(String[] args) {
        HashMapTest t0 = new HashMapTest();
        HashMapTest t1 = new HashMapTest();
        HashMapTest t2 = new HashMapTest();
        HashMapTest t3 = new HashMapTest();
        HashMapTest t4 = new HashMapTest();
        HashMapTest t5 = new HashMapTest();
        t0.start();
        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();

        for (int i = 0; i < 1000000; i++) {
            Integer integer = map.get(i);
            System.out.println(integer);
        }
    }
}
复制代码

反复执行几回,出现这种状况则表示死循环了:算法

由上可知,Thread-7因为HashMap的扩容致使了死循环。数组

HashMap分析

扩关键容源码

1    void transfer(Entry[] newTable, boolean rehash) {
2        int newCapacity = newTable.length;
3        for (Entry<K,V> e : table) {
4            while(null != e) {
5                Entry<K,V> next = e.next;
6                if (rehash) {
7                    e.hash = null == e.key ? 0 : hash(e.key);
8                }
9                int i = indexFor(e.hash, newCapacity);
10               e.next = newTable[i];
11               newTable[i] = e;
12               e = next;
13           }
14       }
15   }
复制代码

正常的扩容过程

咱们先来看下单线程状况下,正常的rehash过程:安全

  1. 假设咱们的hash算法是简单的key mod一下表的大小(即数组的长度)。
  2. 最上面是old hash表,其中HASH表的size=2,因此key=3,5,7在mod 2 之后都冲突在table[1]这个位置上了。
  3. 接下来HASH表扩容,resize=4,而后全部的<key,value>从新进行散列分布,过程以下:

在单线程状况下,一切看起来都很美妙,扩容过程也至关顺利。接下来看下并发状况下的扩容。多线程

并发状况下的扩容

  1. 有两个线程,分别用红色和蓝色标注了。并发

  2. 在线程1执行到第5行代码就被CPU调度挂起(执行完了,获取到next是7),去执行线程2,且线程2把上面代码都执行完毕。咱们来看看这个时候的状态ide

  1. 接着CPU切换到线程1上来,执行4-12行代码(已经执行完了第五行),首先安置健值为3这个Entry:

注意::线程二已经完成执行完成,如今table里面全部的Entry都是最新的,就是说7的next是3,3的next是null;如今第一次循环已经结束,3已经安置稳当。spa

  1. 看看接下来会发生什么事情:
    • e=next=7;
    • e!=null,循环继续
    • next=e.next=3
    • e.next 7的next指向3
    • 放置7这个Entry,如今如图所示:

  1. 放置7以后,接着运行代码:
    • e=next=3;
    • 判断不为空,继续循环
    • next= e.next 这里也就是3的next 为null
    • e.next=7,就3的next指向7.
    • 放置3这个Entry,此时的状态如图

这个时候其实就出现了死循环了,3移动节点头的位置,指向7这个Entry;在这以前7的next同时也指向了3这个Entry。线程

  1. 代码接着往下执行,e=next=null,此时条件判断会终止循环。此次扩容结束了。可是后续若是有查询(不管是查询的迭代仍是扩容),都会hang死在table[3]这个位置上。如今回过来看文章开头的那个Demo,就是挂死在扩容阶段的transfer这个方法上面。

出现问题的关键缘由:若是扩容前相邻的两个Entry在扩容后仍是分配到相同的table位置上,就会出现死循环的BUG。在复杂的生产环境中,这种状况尽管不常见,可是可能会碰到。code

多线程put操做,致使元素丢失

下面来介绍下元素丢失的问题。此次咱们选取三、五、7的顺序来演示:

  1. 若是在线程一执行到第5行代码就被CPU调度挂起:

  1. 线程二执行完成:

  1. 这个时候接着执行线程一,首先放置7这个Entry:

  1. 再放置5这个Entry:

  1. 因为5的next为null,此时扩容动做结束,致使3这个Entry丢失。

JDK 8 的改进

JDK 8 中采用的是位桶 + 链表/红黑树的方式,当某个位桶的链表的长度超过 8 的时候,这个链表就将转换成红黑树

HashMap 不会由于多线程 put 致使死循环(JDK 8 用 head 和 tail 来保证链表的顺序和以前同样;JDK 7 rehash 会倒置链表元素),可是还会有数据丢失等弊端(并发自己的问题)。所以多线程状况下仍是建议使用 ConcurrentHashMap

为何线程不安全

HashMap 在并发时可能出现的问题主要是两方面:

  1. 若是多个线程同时使用 put 方法添加元素,并且假设正好存在两个 put 的 key 发生了碰撞(根据 hash 值计算的 bucket 同样),那么根据 HashMap 的实现,这两个 key 会添加到数组的同一个位置,这样最终就会发生其中一个线程 put 的数据被覆盖

  2. 若是多个线程同时检测到元素个数超过数组大小 * loadFactor,这样就会发生多个线程同时对 Node 数组进行扩容,都在从新计算元素位置以及复制数据,可是最终只有一个线程扩容后的数组会赋给 table,也就是说其余线程的都会丢失,而且各自线程 put 的数据也丢失

相关文章
相关标签/搜索