《Java困惑》:多并发情况下HashMap是否还会产生死循环

《Java困惑》:多并发情况下HashMap是否还会产生死循环

ConcurrentHashMap是Java 5中支持高并发、高吞吐量的线程安全HashMap实现,

在看很多博客在介绍ConcurrentHashMap之前,都说HashMap适用于单线程访问,这是因为HashMap的所有方法都没有进行锁同步,因此是线程不安全的,不仅如此,当多线程访问的时候还容易产生死循环。

虽然自己在前几天的时候看过HashMap的源码,感觉思路啥啥的都还清楚,对于多线程访问只知道HashMap是线程不安全的,但是不知道HashMap在多线程并发的情况下会产生死循环呢,为什么会产生,何种情况下才会产生死循环呢???

正由于有这个问题,于是先将分析ConcurrentHashMap的源码的事情给放了一放,开始在网上查找这个问题的原因。

声明: 
由于上面博客中所分析的HashMap的源码和我目前所看到的源码不一致,发现改动挺大的。目前我的JDK的版本是:jdk1.8.0_45.

这篇博文想讨论的问题是:在目前jdk1.8.0_45源码下,还存不存在上面列出的博文中所将到的这种死循环的问题,个人的答案是:不存在了。若有错,请批评指正

原HashMap的源码产生死循环的过程

下面贴出原HashMap的源码,这是原博客分析的基础,因此,截图如下:

第一篇博客中所贴出的HashMap的源码如下:

个人小结:根据原HashMap的源码,当我们想往HashMap中添加某个元素<K,V>时,假如根据k的hash值找到的存储位置是在table中的index,且刚好此位置已经有了元素,即发生了碰撞,碰撞的处理方式为:将此元素加在此位置的链表的头部,注意,是加在链表头部。

在addEntry方法的代码中很容易的看出这一点。而addEntry方法是在put方法中检测该key在该位置不存在时调用的。

addEntry方法代码如下:


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

就因为是将结点加在链表的头部,所以就出现了原博文中介绍的死循环问题。

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


死循环的产生具体过程描述如下(图解可以看原博文): 
第一步:假设线程1刚记录下来e=3,next=7 就切换到线程2执行 
第二步:线程2执行完resize的全过程,结果如下:table[3]—>7——>3——>null. 
第三步:现在又切换到线程1继续执行,会进行如下三轮循环 
1)第一轮循环,e = 3,next = 7,由于线程1和线程2的newTab是独立的,因此,此轮循环的结果为:newTab[3]—->3—->null; 
2)第二轮循环,e = 7,next = 3(此next就是节点e=7的下一个节点,在线程2中改变的),因此,结果为:newTab[3]—–>7—->3—->null. 
3)第三轮循环,e = 3,next = null(此next就是此时节点e=3的下一个节点,为null,在第一轮循环中改变的),因此,结果为: 
newTab[3]—->3<———>7(这里的双箭头表示节点3的下一个节点为节点7,节点7的下一个节点为3)。就这样循环就产生了。 
当下次我们用get方法来获取get(key)方法来获取此可以与节点3和7相同的hash值的value时就进行了死循环中。 

现在HashMap源码中不会产生上面介绍的死循环

为避免误会,特此声明,JDK版本为:jdk1.8.0_45

HashMap源码中的put方法的源码如下:


    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;//重新开辟一个Node<K,V>的数组
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
    //------------将新节点加在链表之后---------------------------
                        p.next = newNode(hash, key, value, null);
    //-----------------------------------------
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49

resize方法的代码如下:

       final Node<K,V>[] resize() {
            Node<K,V>[] oldTab = table;
            int oldCap = (oldTab == null) ? 0 : oldTab.length;
            int oldThr = threshold;
            int newCap, newThr = 0;
            if (oldCap > 0) {
                if (oldCap >= MAXIMUM_CAPACITY) {
                    threshold = Integer.MAX_VALUE;
                    return oldTab;
                }
                else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                         oldCap >= DEFAULT_INITIAL_CAPACITY)
                    newThr = oldThr << 1; // double threshold
            }
            else if (oldThr > 0) // initial capacity was placed in threshold
                newCap = oldThr;
            else {               // zero initial threshold signifies using defaults
                newCap = DEFAULT_INITIAL_CAPACITY;
                newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
            }
            if (newThr == 0) {
                float ft = (float)newCap * loadFactor;
                newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                          (int)ft : Integer.MAX_VALUE);
            }
            threshold = newThr;
            @SuppressWarnings({"rawtypes","unchecked"})
                Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
            table = newTab;
            if (oldTab != null) {
                for (int j = 0; j < oldCap; ++j) {
                    Node<K,V> e;
                    if ((e = oldTab[j]) != null) {//第j个位置如果有节点
                        oldTab[j] = null;
                        if (e.next == null)//第j个位置只有一个节点情况
                            newTab[e.hash & (newCap - 1)] = e;
                        else if (e instanceof TreeNode)
                            ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                        else { // preserve order
                            Node<K,V> loHead = null, loTail = null;
                            Node<K,V> hiHead = null, hiTail = null;
    //-----------------保持着原来的顺序,并没有倒序-------------------------//
                            Node<K,V> next;
                            do {
                                next = e.next;//保存next
                                //一个接一个,和原来的顺序一致
                                if ((e.hash & oldCap) == 0) {
                                    if (loTail == null)
                                        loHead = e;
                                    else
                                        loTail.next = e;
                                    loTail = e;
                                }
                                else {
                                    if (hiTail == null)
                                        hiHead = e;
                                    else
                                        hiTail.next = e;
                                    hiTail = e;
                                }
                            } while ((e = next) != null);
                            if (loTail != null) {
                                loTail.next = null;
                                newTab[j] = loHead;
                            }
                            if (hiTail != null) {
                                hiTail.next = null;
                                newTab[j + oldCap] = hiHead;
                            }
                        }
                    }
                }
            }
            return newTab;
        }
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75

在上面的代码中(//——-//位置),我们可以看到现在的HashMap是将新节点加在了链表的最后面,而不是最前面。

因此,如果按照现在HashMap的源码的思路,则正常的Rehash过程如下(只需看第三步,第二步也应该改下顺序):


现在我们把上面产生死循环的例子搬移到这里来看是否还会产生死循环呢??


过程描述如下: 
第一步:假设线程1刚记录下来e=3,next=7 就切换到线程2执行 
第二步:线程2执行完resize的全过程,结果如下:table[3]—>3——>7——>null. 
第三步:现在又切换到线程1继续执行,会进行如下2轮循环结束 
1)第一轮循环,e = 3,next = 7,由于线程1和线程2的newTab是独立的,因此,此轮循环的结果为:newTab[3]—->3—->null; 
2)第二轮循环,e = 7,next = null(此next为null,在线程2中改变的),因此,结果为:newTab[3]—–>3—->7—->null. 
就这样结束了,正常运行,不会产生死循环。

Hashtable

最后要说的一点是:Hashtable的put方法的加入节点的方式是加入到链表的头结点。但是,Hashtable是线程安全的,更不会有这个问题

Hashtable的addEntry方法代码如下:

    private void addEntry(int hash, K key, V value, int index) {
        modCount++;

        Entry<?,?> tab[] = table;
        if (count >= threshold) {
            // Rehash the table if the threshold is exceeded
            rehash();

            tab = table;
            hash = key.hashCode();
            index = (hash & 0x7FFFFFFF) % tab.length;
        }

        // Creates the new entry.
        @SuppressWarnings("unchecked")
        Entry<K,V> e = (Entry<K,V>) tab[index];
        tab[index] = new Entry<>(hash, key, value, e);
        count++;
    }

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

并发下的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。


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值