摘要:哈希碰撞的概率取決于計(jì)算方式和空間容量的大小。超過(guò)后執(zhí)行擴(kuò)容操作。當(dāng)一個(gè)哈希桶存儲(chǔ)的鏈表長(zhǎng)度大于會(huì)將鏈表轉(zhuǎn)換成紅黑樹(shù),小于時(shí)則從紅黑樹(shù)轉(zhuǎn)換成鏈表。換句話(huà)來(lái)說(shuō),就是為了減少哈希碰撞。紅黑樹(shù)相關(guān)的操作雖然代碼不同,但是實(shí)際上要干的事情是一樣的。
前言
學(xué)習(xí)情況記錄
學(xué)習(xí)情況記錄
時(shí)間:week 3
SMART子目標(biāo) :Java 容器
記錄在學(xué)習(xí)Java容器 知識(shí)點(diǎn)中,關(guān)于HashMap的需要重點(diǎn)記錄的知識(shí)點(diǎn)。
知識(shí)點(diǎn)概覽:
一、hashCode()
二、HashMap 底層實(shí)現(xiàn)
簡(jiǎn)介
存儲(chǔ)結(jié)構(gòu)
重要屬性
增加元素操作
Q: HashMap 的長(zhǎng)度為什么默認(rèn)初始長(zhǎng)度是16,并且每次resize()的時(shí)候,長(zhǎng)度必須是2的冪次方?
HashMap 擴(kuò)容
Q: HashMap 死鏈問(wèn)題
Java 8 與 Java 7對(duì)比
為什么要使用紅黑樹(shù)?
三、結(jié)語(yǔ)
一、hashCode()在Object 類(lèi)中,hashCode()方法是一個(gè)被native修飾的類(lèi),JavaDoc中描述的是返回該對(duì)象的哈希值。
那么哈希值這個(gè)返回值是有什么作用呢?
主要是保證基于散列的集合,如HashSet、HashMap以及HashTable等,在插入元素時(shí)保證元素不可重復(fù),同時(shí)為了提高元素的插入刪除便利效率而設(shè)計(jì);主要是為了查找的便捷性而存在。
拿Set進(jìn)行舉例,
眾所周知,Set集合是不能重復(fù),如果每次添加數(shù)據(jù)都拿新元素去和集合內(nèi)部元素進(jìn)行逐一地equal()比較,那么插入十萬(wàn)條數(shù)據(jù)的效率可以說(shuō)是非常低的。
所以在添加數(shù)據(jù)的時(shí)候就出現(xiàn)了哈希表的應(yīng)用,哈希算法也稱(chēng)之為散列算法,當(dāng)添加一個(gè)值的時(shí)候,先去計(jì)算出它的哈希值,根據(jù)算出的哈希值將數(shù)據(jù)插入指定位置。這樣的話(huà)就避免了一直去使用equal()比較的效率問(wèn)題。
具體表現(xiàn)在:
如果指定位置為空,則直接添加
如果指定位置不為空,調(diào)用equal() 判斷兩個(gè)元素是否相同,如果相同則不存儲(chǔ)
上述第二種情況中,如果兩個(gè)元素不相同,但是hashCode()相同,那就是發(fā)生了我們所謂的哈希碰撞。
哈希碰撞的概率取決于hashCode()計(jì)算方式和空間容量的大小。
這種情況下,會(huì)在相同的位置,創(chuàng)建一個(gè)鏈表,把key值相同的元素存放到鏈表中。
在HashMap中就是使用拉鏈法來(lái)解決hashCode沖突。
總結(jié)hashCode是一個(gè)對(duì)象的標(biāo)識(shí),Java中對(duì)象的hashCode是一個(gè)int類(lèi)型值。通過(guò)hashCode來(lái)指定數(shù)組的索引可以快速定位到要找的對(duì)象在數(shù)組中的位置,之后再遍歷鏈表找到對(duì)應(yīng)值,理想情況下時(shí)間復(fù)雜度為O(1),并且不同對(duì)象可以擁有相同的hashCode。
二、HashMap 底層實(shí)現(xiàn) 0. 簡(jiǎn)介HashMap 基于哈希表的Map接口實(shí)現(xiàn)的,是以Key-Value存儲(chǔ)形式存在;
非線(xiàn)程安全;
key value都可以為null;
HashMap中的映射不是有序的;
在 JDK1.8 中,HashMap 是由 數(shù)組+鏈表+紅黑樹(shù)構(gòu)成,新增了紅黑樹(shù)作為底層數(shù)據(jù)結(jié)構(gòu);
當(dāng)一個(gè)哈希桶存儲(chǔ)的鏈表長(zhǎng)度大于8 會(huì)將鏈表轉(zhuǎn)換成紅黑樹(shù),小于6時(shí)則從紅黑樹(shù)轉(zhuǎn)換成鏈表;
1.8之前和1.8及以后的源碼,差別較大
1. 存儲(chǔ)結(jié)構(gòu)在 JDK1.8 中,HashMap 是由 數(shù)組+鏈表+紅黑樹(shù)構(gòu)成,新增了紅黑樹(shù)作為底層數(shù)據(jù)結(jié)構(gòu)。
通過(guò)哈希來(lái)確認(rèn)到數(shù)組的位置,如果發(fā)生哈希碰撞就以鏈表的形式存儲(chǔ) ,但是這樣如果鏈表過(guò)長(zhǎng)來(lái)的話(huà),HashMap會(huì)把這個(gè)鏈表轉(zhuǎn)換成紅黑樹(shù)來(lái)存儲(chǔ),閾值為8。
下面是HashMap的結(jié)構(gòu)圖:
2. 重要屬性 2.1 table/** * The table, initialized on first use, and resized as * necessary. When allocated, length is always a power of two. * (We also tolerate length zero in some operations to allow * bootstrapping mechanics that are currently not needed.) */ transient Node[] table;
在JDK1.8中我們了解到HashMap是由數(shù)組加鏈表加紅黑樹(shù)來(lái)組成的結(jié)構(gòu)其中table就是HashMap中的數(shù)組。
2.2 size/** * The number of key-value mappings contained in this map. */ transient int size;
HashMap中 鍵值對(duì)存儲(chǔ)數(shù)量。
2.3 loadFactor/** * The load factor for the hash table. * * @serial */ final float loadFactor;
負(fù)載因子。負(fù)載因子是權(quán)衡資源利用率與分配空間的系數(shù)。當(dāng)元素總量 > 數(shù)組長(zhǎng)度 * 負(fù)載因子 時(shí)會(huì)進(jìn)行擴(kuò)容操作。
2.4 threshold/** * The next size value at which to resize (capacity * load factor). * * @serial */ // (The javadoc description is true upon serialization. // Additionally, if the table array has not been allocated, this // field holds the initial array capacity, or zero signifying // DEFAULT_INITIAL_CAPACITY.) int threshold;
擴(kuò)容閾值。threshold = 數(shù)組長(zhǎng)度 * 負(fù)載因子。超過(guò)后執(zhí)行擴(kuò)容操作。
2.5 TREEIFY_THRESHOLD/UNTREEIFY_THRESHOLD/** * The bin count threshold for using a tree rather than list for a * bin. Bins are converted to trees when adding an element to a * bin with at least this many nodes. The value must be greater * than 2 and should be at least 8 to mesh with assumptions in * tree removal about conversion back to plain bins upon * shrinkage. */ static final int TREEIFY_THRESHOLD = 8; /** * The bin count threshold for untreeifying a (split) bin during a * resize operation. Should be less than TREEIFY_THRESHOLD, and at * most 6 to mesh with shrinkage detection under removal. */ static final int UNTREEIFY_THRESHOLD = 6;
樹(shù)形化閾值。當(dāng)一個(gè)哈希桶存儲(chǔ)的鏈表長(zhǎng)度大于8 會(huì)將鏈表轉(zhuǎn)換成紅黑樹(shù),小于6時(shí)則從紅黑樹(shù)轉(zhuǎn)換成鏈表。
3. 增加元素public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }3.1 hash()
可以看到實(shí)際執(zhí)行添加元素的是putVal()操作,在執(zhí)行putVal()之前,先是對(duì)key執(zhí)行了hash()方法,讓我們看下里面做了什么
static final int hash(Object key) { int h; // key.hashCode():返回散列值也就是hashcode // ^ :按位異或 // >>>:無(wú)符號(hào)右移,忽略符號(hào)位,空位都以0補(bǔ)齊 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
key==null說(shuō)明,HashMap中是支持key為null的情況的。
同樣的方法在Hashstable中是直接用key來(lái)獲取hashCode,沒(méi)有key==null的判斷,所以Hashstable是不支持key為null的。
再回來(lái)說(shuō)這個(gè)hash()方法。這個(gè)方法用專(zhuān)業(yè)術(shù)語(yǔ)來(lái)稱(chēng)呼就叫做擾動(dòng)函數(shù)。
使用hash()也就是擾動(dòng)函數(shù),是為了防止一些實(shí)現(xiàn)比較差的hashCode()方法。換句話(huà)來(lái)說(shuō),就是為了減少哈希碰撞。
JDK 1.8 的 hash方法 相比于 JDK 1.7 hash 方法更加簡(jiǎn)化,但是原理不變。我們?cè)倏聪翵DK1.7中是怎么做的。
// code in JDK1.7 static int hash(int h) { // This function ensures that hashCodes that differ only by // constant multiples at each bit position have a bounded // number of collisions (approximately 8 at default load factor). h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }
相比于 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能會(huì)稍差一點(diǎn)點(diǎn),因?yàn)楫吘箶_動(dòng)了 4 次。
3.2 putVal()再來(lái)看真正執(zhí)行增加元素操作的putVal()方法,
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node3.3 HashMap 的長(zhǎng)度為什么默認(rèn)初始長(zhǎng)度是16,并且每次resize()的時(shí)候,長(zhǎng)度必須是2的冪次方?[] tab; Node p; int n, i; // 當(dāng)數(shù)組為空或長(zhǎng)度為0,初始化數(shù)組容量(resize() 方法是初始化或者擴(kuò)容用的) if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 計(jì)算數(shù)組下標(biāo) i = (n-1) & hash // 如果這個(gè)位置沒(méi)有元素,則直接創(chuàng)建Node并存值 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { // 這個(gè)位置已有元素 Node e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) // hash值、key值相等,用e變量獲取到當(dāng)前位置這個(gè)元素的引用,后面用于替換已有的值 e = p; else if (p instanceof TreeNode) // 當(dāng)前是以紅黑樹(shù)方式存儲(chǔ),執(zhí)行其特有的putVal方法 -- putTreeVal e = ((TreeNode )p).putTreeVal(this, tab, hash, key, value); else { // 當(dāng)前是以鏈表方式存儲(chǔ),開(kāi)始遍歷鏈表 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 // 超過(guò)閾值,存儲(chǔ)方式轉(zhuǎn)化成紅黑樹(shù) 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) // onlyIfAbsent 如果為true - 不覆蓋已存在的值 // 把新值賦值進(jìn)去 e.value = value; afterNodeAccess(e); return oldValue; } } // 記錄修改次數(shù) ++modCount; // 判斷元素?cái)?shù)量是否超過(guò)閾值 超過(guò)則擴(kuò)容 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
這是一個(gè)常見(jiàn)的面試題。這個(gè)問(wèn)題描述的設(shè)計(jì),實(shí)際上為了服務(wù)于從Key映射到數(shù)組下標(biāo)index的Hash算法。
前面提到了,我們?yōu)榱俗孒ashMap存儲(chǔ)高效,應(yīng)該盡量減少哈希碰撞,也就是說(shuō),應(yīng)該讓元素分配得盡可能均勻。
Hash 值的范圍值-2147483648到2147483647,前后加起來(lái)大概40億的映射空間,只要哈希函數(shù)映射得比較均勻松散,一般應(yīng)用是很難出現(xiàn)碰撞的。但問(wèn)題是一個(gè)40億長(zhǎng)度的數(shù)組,內(nèi)存是放不下的。所以這個(gè)散列值是不能直接拿來(lái)用的。
所以才需要一個(gè)映射的算法。這個(gè)計(jì)算方式就是3.2中有出現(xiàn)的(n - 1) & hash。
我們來(lái)進(jìn)一步演示一下這個(gè)算法:
假設(shè)有一個(gè)key="book"
計(jì)算book的hashCode值,結(jié)果為十進(jìn)制的3029737,二進(jìn)制的101110001110101110 1001。
假定HashMap長(zhǎng)度是默認(rèn)的16,計(jì)算Length-1的結(jié)果為十進(jìn)制的15,二進(jìn)制的1111。
把以上兩個(gè)結(jié)果做與運(yùn)算,101110001110101110 1001 & 1111 = 1001,十進(jìn)制是9,所以 index=9。
通過(guò)這種與運(yùn)算的方式,能夠和取模運(yùn)算一樣的效果hashCode % length,在上述例子中就是3029737 % 16=9。
并且通過(guò)位運(yùn)算的方式大大提高了性能。
可能到這里,你還是不知道為什么長(zhǎng)度必須是2的冪次方,也是因?yàn)檫@種位運(yùn)算的方法。
長(zhǎng)度16或者其他2的冪,Length-1的值是所有二進(jìn)制位全為1,這種情況下,index的結(jié)果等同于HashCode后幾位的值。只要輸入的HashCode本身分布均勻,Hash算法的結(jié)果就是均勻的。如果HashMap的長(zhǎng)度不是2的冪次方,會(huì)出現(xiàn)某些index永遠(yuǎn)不會(huì)出現(xiàn)的情況,這個(gè)顯然不符合均勻分布的原則和期望。所以在源碼里面一直都在強(qiáng)調(diào)power-of-two expansion和size must be power of two。
另外,HashMap 構(gòu)造函數(shù)允許用戶(hù)傳入的容量不是 2 的 n 次方,因?yàn)樗梢宰詣?dòng)地將傳入的容量轉(zhuǎn)換為 2 的 n 次方。
/** * Returns a power of two size for the given target capacity. */ static final int tableSizeFor(int cap) { int n = cap - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }4. HashMap 擴(kuò)容
接下來(lái)我們來(lái)講講HashMap擴(kuò)容相關(guān)的知識(shí)。
4.1 擴(kuò)容HashMap的初始長(zhǎng)度是16,假設(shè)HashMap中的鍵值對(duì)一直在增加,但是table數(shù)組容量一直不變,那么就會(huì)發(fā)生哈希碰撞,查找的效率肯定會(huì)越來(lái)越低。所以當(dāng)鍵值對(duì)數(shù)量超過(guò)某個(gè)閾值的時(shí)候,HashMap就會(huì)執(zhí)行擴(kuò)容操作。
那么擴(kuò)容的閾值是怎么計(jì)算的呢?
閾值 = 數(shù)組長(zhǎng)度 * 負(fù)載因子threshold = capacity * loadFactor
每次擴(kuò)容后,threshold 加倍
上述計(jì)算就出現(xiàn)在resize()方法中。下面會(huì)詳細(xì)解析這個(gè)方法。我們先繼續(xù)往下講。
loadFactor這個(gè)參數(shù),我們之前提到過(guò),負(fù)載因子是權(quán)衡資源利用率與分配空間的系數(shù)。至于為什么是0.75呢?這個(gè)實(shí)際上就是一個(gè)作者認(rèn)為比較好的權(quán)衡,當(dāng)然你也可以通過(guò)構(gòu)造方法手動(dòng)設(shè)置負(fù)載因子 。public HashMap(int initialCapacity, float loadFactor) {...)。
接下去再來(lái)到這里的主角resize()方法
final Node[] resize() { // 舊數(shù)組引用 Node [] oldTab = table; // 舊數(shù)組長(zhǎng)度 int oldCap = (oldTab == null) ? 0 : oldTab.length; // 舊閾值 int oldThr = threshold; // 新數(shù)組長(zhǎng)度、新閾值 int newCap, newThr = 0; if (oldCap > 0) { if (oldCap >= MAXIMUM_CAPACITY) { // 舊數(shù)組已經(jīng)超過(guò)了數(shù)組的最大容量 // 閾值改成最大,直接返回舊數(shù)組,不操作了 threshold = Integer.MAX_VALUE; return oldTab; } // newCap 變成原來(lái)的 兩倍 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) // 執(zhí)行擴(kuò)容操作,新閾值 = 舊閾值 * 2 newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // initial capacity was placed in threshold // 初始閾值被手動(dòng)設(shè)置過(guò) // 數(shù)組容量 = 初始閾值 newCap = oldThr; else { // zero initial threshold signifies using defaults // 初始化操作 // 數(shù)組容量 = 默認(rèn)初始容量 newCap = DEFAULT_INITIAL_CAPACITY; // 初始閾值 = 容量 * 默認(rèn)負(fù)載因子 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { // 如果在前面閾值都沒(méi)有被設(shè)置過(guò) float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } // 更新閾值 threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) // 創(chuàng)建數(shù)組 Node [] newTab = (Node [])new Node[newCap]; // 更新table引用的數(shù)組 table = newTab; if (oldTab != null) { // 擴(kuò)容 for (int j = 0; j < oldCap; ++j) { // 遍歷舊數(shù)組 Node e; if ((e = oldTab[j]) != null) { // 取出這個(gè)位置的頭節(jié)點(diǎn) // 把舊引用取消,方便垃圾回收 oldTab[j] = null; if (e.next == null) newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) // 紅黑樹(shù)的處理 ((TreeNode )e).split(this, newTab, j, oldCap); else { // preserve order // 鏈表的處理 這個(gè)鏈表處理實(shí)際上非常的巧妙 // 定義了兩條鏈 Node loHead = null, loTail = null; Node hiHead = null, hiTail = null; Node next; do { next = e.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; }
上述代碼紅黑樹(shù)和鏈表的處理不知道大家看懂了沒(méi)有,我反正在第一次看的時(shí)候有點(diǎn)暈乎。但是理解了之后有感覺(jué)非常的巧妙。
拿鏈表處理打比方,它干的就是把在遍歷舊的table數(shù)組的時(shí)候,把該位置的鏈表分成high鏈表和low鏈表。具體是什么意思呢?看下下面的舉例。
有一個(gè)size為16的HashMap。有A/B/C/D/E/F六個(gè)元素,其中A/B/C的Hash值為5,D/E/F的Hash值為21,我們知道計(jì)算數(shù)組下標(biāo)的方法是與運(yùn)算(效果相當(dāng)于取模運(yùn)算),這樣計(jì)算出來(lái),A/B/C/D/E/F的index = 5,都會(huì)被存在index=5的位置上中。
假設(shè)它們是依次插入,那么在index為5的位置上,就會(huì)有A->B->C->D->E->F這樣一個(gè)鏈表。
當(dāng)這個(gè)HashMap要進(jìn)行擴(kuò)容的時(shí)候,此時(shí)我們有舊數(shù)組oldTable[],容量為16,新數(shù)組newTable[],容量為32(擴(kuò)容數(shù)組容量加倍)。
當(dāng)遍歷到舊數(shù)組index=5的位置的時(shí)候,進(jìn)入到上面提到的鏈表處理的代碼段中,對(duì)鏈表上的元素進(jìn)行Hash & oldCapacity的操作,Hash值為5的A/B/C計(jì)算之后為0,被分到了low鏈表,Hash為21的D/E/F被分到了high鏈表。
然后把low鏈表放入新數(shù)組的index=5的位置,把high鏈表放入到新數(shù)組的index=5+16=21的位置。
紅黑樹(shù)相關(guān)的操作雖然代碼不同,但是實(shí)際上要干的事情是一樣的。就是把相同位置的不同Hash大小的鏈表元素在新table數(shù)組中進(jìn)行分離。希望講到這里你能聽(tīng)懂。
4.2 HashMap 死鏈問(wèn)題Java7的HashMap會(huì)存在死循環(huán)的問(wèn)題,主要原因就在于,Java7中,HashMap擴(kuò)容轉(zhuǎn)移后,前后鏈表順序倒置,在轉(zhuǎn)移過(guò)程中其他線(xiàn)程修改了原來(lái)鏈表中節(jié)點(diǎn)的引用關(guān)系,導(dǎo)致在某Hash桶位置形成了環(huán)形鏈表,此時(shí)get(key),如果key不存在于這個(gè)HashMap且key的Hash結(jié)果等于那個(gè)形成了循環(huán)鏈表的Hash位置,那么程序就會(huì)進(jìn)入死循環(huán);
Java8在同樣的前提下并不會(huì)引起死循環(huán),原因是Java8擴(kuò)容轉(zhuǎn)移后前后鏈表順序不變,保持之前節(jié)點(diǎn)的引用關(guān)系。
具體的圖像演示請(qǐng)看這篇。漫畫(huà):高并發(fā)下的HashMap
5. Java 8 與 Java 7對(duì)比發(fā)生hash沖突時(shí),Java7會(huì)在鏈表頭部插入,Java8會(huì)在鏈表尾部插入
擴(kuò)容后轉(zhuǎn)移數(shù)據(jù),Java7轉(zhuǎn)移前后鏈表順序會(huì)倒置,Java8還是保持原來(lái)的順序
引入紅黑樹(shù)的Java8極大程度地優(yōu)化了HashMap的性能‘
put 操作達(dá)到閾值時(shí),Java7中是先擴(kuò)容再新增元素,Java8是先新增元素再擴(kuò)容;
6. HashMap 遍歷方式HashMap 有兩種遍歷方式,
// 遍歷方式1 Iterator> entryIterator = map.entrySet().iterator(); while (entryIterator.hasNext()) { Map.Entry next = entryIterator.next(); System.out.println("key=" + next.getKey() + " value=" + next.getValue()); } // 遍歷方式2 Iterator iterator = map.keySet().iterator(); while (iterator.hasNext()){ String key = iterator.next(); System.out.println("key=" + key + " value=" + map.get(key)); }
這里建議使用,第一種方式進(jìn)行遍歷。
第一種可以把 key value 同時(shí)取出,第二種還得需要通過(guò) key 取一次 value,效率較低。
7. 為什么要使用紅黑樹(shù)?很多人可能都會(huì)答上一句,為了提高查找性能,但更確切地來(lái)說(shuō)的話(huà),采用紅黑樹(shù)的方法是為了提高在極端哈希沖突的情況下提高HashMap的性能。
下面是一段測(cè)試HashMap性能的基準(zhǔn)測(cè)試代碼:
import com.google.caliper.Param; import com.google.caliper.Runner; import com.google.caliper.SimpleBenchmark; public class MapBenchmark extends SimpleBenchmark { private HashMap < Key, Integer > map; @Param private int mapSize; @Override protected void setUp() throws Exception { map = new HashMap < > (mapSize); for (int i = 0; i < mapSize; ++i) { map.put(Keys.of(i), i); } } public void timeMapGet(int reps) { for (int i = 0; i < reps; i++) { map.get(Keys.of(i % mapSize)); } } }
class Key implements Comparable < Key > { private final int value; Key(int value) { this.value = value; } @Override public int compareTo(Key o) { return Integer.compare(this.value, o.value); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Key key = (Key) o; return value == key.value; } // @Override // public int hashCode() { // return value; // } @Override public int hashCode() { // key 返回的hash值都相同 return 0; } }
public class Keys { public static final int MAX_KEY = 10 _000_000; private static final Key[] KEYS_CACHE = new Key[MAX_KEY]; static { for (int i = 0; i < MAX_KEY; ++i) { KEYS_CACHE[i] = new Key(i); } } public static Key of (int value) { return KEYS_CACHE[value]; } }
可以看到,Key對(duì)象返回的hash值被修改成了,相同,也就是我們說(shuō)的極端哈希沖突的情況下,此時(shí),去測(cè)量Java7和Java8版本的HashMap的查詢(xún)性能差距。
Java 7的結(jié)果是可以預(yù)期的。 HashMap.get()的性能損耗與HashMap本身的大小成比例增長(zhǎng)。 由于所有鍵值對(duì)都在一個(gè)巨大的鏈表中的同一個(gè)桶中,查找一個(gè)條目需要平均遍歷一半這樣的列表(大小為n)。 因此O(n)復(fù)雜性在圖上可視化。
與此相對(duì)的是Java8,性能提高了很多,發(fā)生災(zāi)難性哈希沖突的情況下,在JDK 8上執(zhí)行的相同基準(zhǔn)測(cè)試會(huì)產(chǎn)生O(logn)最差情況下的性能。
關(guān)于此處的算法優(yōu)化實(shí)際上在JEP-180中有描述到,
另外如果Key對(duì)象如果不是Comparable的話(huà),那么發(fā)生重大哈希沖突時(shí),插入和刪除元素的效率會(huì)大大降低。(因?yàn)榈讓訉?shí)現(xiàn)時(shí)紅黑樹(shù),需要通過(guò)compare方法去確定順序)
又可能會(huì)有人說(shuō)了,哪有這么極端的哈希沖突?
這個(gè)實(shí)際上是一個(gè)安全性的考慮,雖然在正常情況下很少有可能發(fā)生很多沖突。但是想象一下,如果Key來(lái)自不受信任的來(lái)源(例如從客戶(hù)端收到的HTTP頭名稱(chēng)),那么就有可能收到偽造key值,并且這種做法不難,因?yàn)楣K惴ㄊ谴蠹叶贾赖?,假設(shè)有人有心去偽造相同的哈希值的key值,那么你的HashMap中就會(huì)出現(xiàn)上述這種極端哈希沖突的情況。 現(xiàn)在,如果你去對(duì)這個(gè)HashMap執(zhí)行多次的查詢(xún)請(qǐng)求,就會(huì)發(fā)現(xiàn)程序執(zhí)行查詢(xún)的效率會(huì)變得很慢,cpu占用率很高,程序甚至?xí)芙^對(duì)外提供服務(wù)。
三、結(jié)語(yǔ)HashMap的相關(guān)知識(shí)就介紹到這里,篇幅非常的長(zhǎng),本人也寫(xiě)了很久。
整個(gè)學(xué)習(xí)過(guò)程下來(lái)的感覺(jué)就是,知識(shí)的學(xué)習(xí)應(yīng)該是相互穿插并且印證,HashMap實(shí)際上和其他的Map有很多交叉的實(shí)現(xiàn)原理,比如ConcurrentHashMap大致原理和HashMap相同,只是前者使用分段鎖確保線(xiàn)程安全,Hashstable和HashMap底層原理也很相似,只是Hashstable使用synchronized做同步,并且官方在Hashstable出來(lái)之后就沒(méi)有再去更新過(guò),屬于過(guò)時(shí)的類(lèi);HashMap和TreeMap底層都涉及到了紅黑樹(shù)。當(dāng)你互相對(duì)照地去學(xué)習(xí)時(shí),你會(huì)發(fā)現(xiàn)學(xué)懂了其中一個(gè),另外幾個(gè)就懂了差不多了,因?yàn)楹芏嘣矶际谴┎鍛?yīng)用的。
下一章會(huì)涉及到其他的Map類(lèi)型,并且對(duì)他們進(jìn)行對(duì)比。
參考《碼出高效》
https://dzone.com/articles/ha...
https://stackoverflow.com/que...
漫畫(huà):高并發(fā)下的HashMap
https://mp.weixin.qq.com/s/RI...
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/75657.html
摘要:作為面試官,我是如何甄別應(yīng)聘者的包裝程度語(yǔ)言和等其他語(yǔ)言的對(duì)比分析和主從復(fù)制的原理詳解和持久化的原理是什么面試中經(jīng)常被問(wèn)到的持久化與恢復(fù)實(shí)現(xiàn)故障恢復(fù)自動(dòng)化詳解哨兵技術(shù)查漏補(bǔ)缺最易錯(cuò)過(guò)的技術(shù)要點(diǎn)大掃盲意外宕機(jī)不難解決,但你真的懂?dāng)?shù)據(jù)恢復(fù)嗎每秒 作為面試官,我是如何甄別應(yīng)聘者的包裝程度Go語(yǔ)言和Java、python等其他語(yǔ)言的對(duì)比分析 Redis和MySQL Redis:主從復(fù)制的原理詳...
摘要:作為面試官,我是如何甄別應(yīng)聘者的包裝程度語(yǔ)言和等其他語(yǔ)言的對(duì)比分析和主從復(fù)制的原理詳解和持久化的原理是什么面試中經(jīng)常被問(wèn)到的持久化與恢復(fù)實(shí)現(xiàn)故障恢復(fù)自動(dòng)化詳解哨兵技術(shù)查漏補(bǔ)缺最易錯(cuò)過(guò)的技術(shù)要點(diǎn)大掃盲意外宕機(jī)不難解決,但你真的懂?dāng)?shù)據(jù)恢復(fù)嗎每秒 作為面試官,我是如何甄別應(yīng)聘者的包裝程度Go語(yǔ)言和Java、python等其他語(yǔ)言的對(duì)比分析 Redis和MySQL Redis:主從復(fù)制的原理詳...
摘要:基礎(chǔ)問(wèn)題的的性能及原理之區(qū)別詳解備忘筆記深入理解流水線(xiàn)抽象關(guān)鍵字修飾符知識(shí)點(diǎn)總結(jié)必看篇中的關(guān)鍵字解析回調(diào)機(jī)制解讀抽象類(lèi)與三大特征時(shí)間和時(shí)間戳的相互轉(zhuǎn)換為什么要使用內(nèi)部類(lèi)對(duì)象鎖和類(lèi)鎖的區(qū)別,,優(yōu)缺點(diǎn)及比較提高篇八詳解內(nèi)部類(lèi)單例模式和 Java基礎(chǔ)問(wèn)題 String的+的性能及原理 java之yield(),sleep(),wait()區(qū)別詳解-備忘筆記 深入理解Java Stream流水...
摘要:基礎(chǔ)問(wèn)題的的性能及原理之區(qū)別詳解備忘筆記深入理解流水線(xiàn)抽象關(guān)鍵字修飾符知識(shí)點(diǎn)總結(jié)必看篇中的關(guān)鍵字解析回調(diào)機(jī)制解讀抽象類(lèi)與三大特征時(shí)間和時(shí)間戳的相互轉(zhuǎn)換為什么要使用內(nèi)部類(lèi)對(duì)象鎖和類(lèi)鎖的區(qū)別,,優(yōu)缺點(diǎn)及比較提高篇八詳解內(nèi)部類(lèi)單例模式和 Java基礎(chǔ)問(wèn)題 String的+的性能及原理 java之yield(),sleep(),wait()區(qū)別詳解-備忘筆記 深入理解Java Stream流水...
閱讀 2837·2021-09-02 15:11
閱讀 968·2019-08-26 18:18
閱讀 1935·2019-08-26 11:57
閱讀 3398·2019-08-23 16:59
閱讀 2062·2019-08-23 16:51
閱讀 2371·2019-08-23 16:11
閱讀 3215·2019-08-23 14:58
閱讀 1166·2019-08-23 11:34