• 关于HashMap多线程下环形链表的总结


    1. 概述

    本文主要针对对网上关于HashMap在多线程环境下会形成循环链表的问题进行一次总结.

    2. 敲黑板的点

    1. 只会在低于jdk1.8的版本中发生(1.6, 1.7会有, 再古老的版本我就不知道了)
    2. jdk1.7的HashMap的数据结构使用的是数组+链表, 不存在红黑树.
    3. 这种情况发生在集合扩容的时候

    3. 为什么会出现循环链表的情况呢?(jdk1.7)

    多线程环境下, 多个线程同时对集合进行扩容时会发生.

    下面翻开jdk1.7的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];
    	transfer(newTable, initHashSeedAsNeeded(newCapacity));
    	table = newTable;
    	threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }
    
    /**
     * Transfers all entries from current table to newTable.
     * 这段代码主要是遍历原集合中的所有Entry, 然后依次将他们放入到新的集合中.
     */
    void transfer(Entry[] newTable, boolean rehash) {
    	int newCapacity = newTable.length;
    	
    	// 遍历所有Entry
    	for (Entry<K,V> e : table) {
    	
    		// 这里的while主要针对存在链表的情况
    		while(null != e) {
    		
    			// 获取下一个元素, 如果存在链表, next就不为null
    			Entry<K,V> next = e.next;
    			
    			// 是否需要重新hash
    			if (rehash) {
    				e.hash = null == e.key ? 0 : hash(e.key);
    			}
    			
    			// 获取新下标
    			int i = indexFor(e.hash, newCapacity);
    			
    			// 第一次while循环时, newTable[i]是null
    			// 第二次while循环时, newTable[i]是第一次循环时的元素
    			// 原先链表的顺序为: 1,3,5,7,9
    			// 正常情况下, 扩容完成之后, 链表中元素的顺序为: 9,7,5,3,1
    			e.next = newTable[i];
    			
    			// 覆盖上次循环的值, 因为上次循环时的值已经被链接到e.next上了
    			newTable[i] = e;
    			
    			// 继续循环链表上的下一个元素
    			e = next;
    		}
    	}
    }
    

    形成循环链表的代码就在transfer方法的while循环中, 正是因为扩容之后链表中元素的会发生逆转, 所以会产生循环链表.

    举例说明:

    /**
     * [ 1, 3, 5, 7, 9] // HashMap table
     *  11
     *  12
     *  13
     *  14
     *  15
     */
    

    可以发现, 集合中下标0处发生了hash冲突, 产生了链表: 1,11,12,13,14,15.

    现有两个线程: 线程A和线程B, 线程A进入while循环时, 执行到Entry<K,V> next = e.next;时被挂起了, 这时该线程A中的e=1, e.next=11. 此时线程B进入while循环, 这时线程B中的e=1, e.next=11, 然后线程B继续向下执行, 执行完第一次while循环之后, 链表的顺序就变为11,1,12,13,14,15. 这时元素11的下一个元素时1, 而此时线程A中元素1的下一个元素为11. 完美! 产生了循环链表!

    4. jdk1.8中改进了resize方法

    可以参考我的另一片博客HashMap源码分析

    改进之后的方法不再进行链表的逆转, 而是保持原有链表的顺序, 如果在多线程环境下, 顶多会在链表后边多追加几个元素而已, 不会出现环的情况.

    5. HashMap的线程安全问题

    毫无疑问, HashMap没有进行一点线程安全的控制, 甚至连volatile关键字都没有, 其实也不需要, 如果HashMap加入了一些线程安全控制的代码, 那么在单线程时, 这些线程安全的代码无疑时影响性能的相关要素. 所以没必要.

    既然没有线程安全代码, 那么HashMap在多线程环境中一定是线程不安全的. 最简单的就是多个线程put元素时, 获取得到的size时不一样的, 因为没有加volatile关键字, 可以这么理解.

    6. 总结

    1. jdk1.7 和 jdk1.8的HashMap实现不一样, 优化了存储结构和hash算法
    2. put元素时产生环形链表的问题已在jdk1.8中解决了.
  • 相关阅读:
    VS 2013 中如何自定义快捷键(图解)
    c# XML读取
    Java与.NET的WebServices相互调用
    .NET 的 WCF 和 WebService 有什么区别?(转载)
    2017年第六届数学中国数学建模国际赛(小美赛)比赛心得
    网络分析法(Analytic Network Process,ANP)
    图的简单应用(C/C++实现)
    【Android开发学习笔记之一】5大布局方式详解
    Android布局属性详解
    Android应用程序使用两个LinearLayout编排5个Button控件
  • 原文地址:https://www.cnblogs.com/wuqinglong/p/9646498.html
Copyright © 2020-2023  润新知