開心一刻
一天,樓主在路上碰到了一個很久沒見的朋友,找了個餐館,坐下聊了起來
樓主:在哪上班了 ?
朋友:火葬場啊
樓主:在那上班,一個月多少錢啊 ?
朋友:兩萬多啊
樓主(不可思議):多少 ?
朋友(非常淡定):兩萬多
樓主:你們那還要人嗎 ?
朋友:要啊,24小時都要
樓主:不是,我的意思是你們那還收人嗎
朋友:收,天天都收
樓主:我是說,我能進去不 ?
朋友:那200多斤的胖子都能進去,你進不去 ?
樓主:不是,你是非要把我給煉了是咋地 ? 我能進去不,我能自己進去不 ?
朋友:那有點懸,都是推進去的
樓主:我是說,你們那還招工嗎
朋友:招,不分公母,都招
樓主:老板,買單
老板:你還沒點菜了
樓主:不點了,再不走就要被煉了
數據結構
對 HashMap 的底層數據結構,相信大家都有所了解,不同的版本,底層數據結構會有所不同
1.7 的底層數據結構

/** * An empty table instance to share when the table is not inflated. */ static final Entry<?,?>[] EMPTY_TABLE = {}; /** * The table, resized as necessary. Length MUST Always be a power of two. */ transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; int hash; ... }
1.8 的底層數據結構

/** * 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<K,V>[] table; static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; ... } /** * Entry for Tree bins. Extends LinkedHashMap.Entry (which in turn * extends Node) so can be used as extension of either regular or * linked node. */ static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { TreeNode<K,V> parent; // red-black tree links TreeNode<K,V> left; TreeNode<K,V> right; TreeNode<K,V> prev; // needed to unlink next upon deletion boolean red; ... }
但基礎結構還是: 數組 + 鏈表 ,稱作 哈希表 或 散列表
只是 1.8 做了優化,引進了 紅黑樹 ,來提升鏈表中元素獲取的速度
JDK1.7 頭插
只有元素添加的時候,才會出現鏈表元素的插入,那么我們先來看看 put 方法
put - 添加元素
源碼如下

/** * Associates the specified value with the specified key in this map. * If the map previously contained a mapping for the key, the old * value is replaced. * * @param key key with which the specified value is to be associated * @param value value to be associated with the specified key * @return the previous value associated with <tt>key</tt>, or * <tt>null</tt> if there was no mapping for <tt>key</tt>. * (A <tt>null</tt> return can also indicate that the map * previously associated <tt>null</tt> with <tt>key</tt>.) */ public V put(K key, V value) { if (table == EMPTY_TABLE) { inflateTable(threshold); } if (key == null) return putForNullKey(value); int hash = hash(key); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null; }
直接看代碼可能不夠直觀,我們結合流程圖來看
什么? 還是不夠直觀? (樓主也這么覺得)
那我們就結合具體案例來看下這個流程
假設 HashMap 初始狀態
然后依次往里面添加元素:(2,b), (3,w), (5,e), (9,t), (16,p)
再利用斷點調試,我們來看看真實情況
一切都對得上,進展的也挺順利
resize - 數組擴容
上述提到了擴容,但是沒細講,我們來看看擴容的實現
關鍵代碼如下

/** * Rehashes the contents of this map into a new array with a * larger capacity. This method is called automatically when the * number of keys in this map reaches its threshold. * * If current capacity is MAXIMUM_CAPACITY, this method does not * resize the map, but sets threshold to Integer.MAX_VALUE. * This has the effect of preventing future calls. * * @param newCapacity the new capacity, MUST be a power of two; * must be greater than current capacity unless current * capacity is MAXIMUM_CAPACITY (in which case value * is irrelevant). */ 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. */ 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; } } }
主要做了兩件事:1、創建一個新的 Entry 空數組,長度是原數組的 2 倍,2、遍歷原數組,對每個元素重新計算新數組的索引值,然后放入到新數組的對應位置
有意思的是這個轉移方法:transfer,我們結合案例來仔細看看
假設擴容之前的狀態如下圖所示
擴容過程如下
利用斷點調試,我們來看看真實情況
鏈表元素的轉移,還是采用的頭插法
鏈表成環
不管是元素的添加,還是數組擴容,只要涉及到 hash 沖突,就會采用頭插法將元素添加到鏈表中
上面講了那么多,看似風平浪靜,實則暗流涌動;單線程下,確實不會有什么問題,那多線程下呢 ? 我們接着往下看
將設擴容之前的的狀態如下所示
然后,線程 1 添加 (1,a) ,線程 2 添加 (19,n),線程 1 會進行擴容,線程 2 也進行擴容,那么 transfer 的時候就可能出現如下情況
哦豁,鏈表成環了,這就會導致:Infinite Loop
JDK1.8 尾插
1.8就不講那么詳細了,我們主要來看看 resize 中的元素轉移部分

if (oldTab != null) { // 從索引 0 開始逐個遍歷舊 table for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; if (e.next == null) // 鏈表只有一個元素 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; if ((e.hash & oldCap) == 0) { // 索引不變的元素鏈表 if (loTail == null) loHead = e; else // 通過尾部去關聯 next,維持了元素原有順序 loTail.next = e; loTail = e; } else { // 相同偏移量的元素鏈表 if (hiTail == null) hiHead = e; else // 通過尾部去關聯 next,維持了元素原有順序 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; } } } } }
分高位鏈與低位鏈進行尾插,移動元素到新的數組中,具體細節可查看:Java 8系列之重新認識HashMap;不需要重新hash就能找到元素在新數組的位置
在擴容時,頭插法會改變鏈表中元素原本的順序,以至於在並發場景下導致鏈表成環的問題,而尾插法,在擴容時會保持鏈表元素原本的順序,就不會出現鏈表成環的問題
相關疑惑
1、JDK 1.7及之前,為什么采用頭插法
呃... 這個可能需要問頭插法的實現者了;
但有種說法,我覺得挺有道理:緩存的時間局部性原則,最近訪問過的數據下次大概率會再次訪問,把剛訪問過的元素放在鏈表最前面可以直接被查詢到,減少查找次數
2、既然頭插法有鏈表成環的問題,為什么直到 1.8 才采用尾插法來替代頭插法
只有在並發情況下,頭插法才會出現鏈表成環的問題,多線程情況下,HashMap 本就非線程安全,這就相當於你在它的規則之外出了問題,那能怪誰?
1.8 采用尾插,是對 1.7 的優化
3、既然 1.8 沒有鏈表成環的問題,那是不是說明可以把 1.8 中的 HashMap 用在多線程中
鏈表成環只是並發問題中的一種,1.8 雖然解決了此問題,但是還是會有很多其他的並發問題,比如:上秒 put 的值,下秒 get 的時候卻不是剛 put 的值;因為操作都沒有加鎖,不是線程安全的
總結
1、JDK 1.7 采用頭插法來添加鏈表元素,存在鏈表成環的問題,1.8 中做了優化,采用尾插法來添加鏈表元素
2、HashMap 不管在哪個版本都不是線程安全的,出了並發問題不要怪 HashMap,從自己身上找原因