HashMap能夠說是面試中最常出現的名詞,此次頭條的一面,第一個問的問題就是HashMap。因此就讓咱們來探討下HashMap吧。html
實驗環境:JDK1.8java
首先先說一下,和JDK1.7相比,對HashMap作了一些優化,使得HashMap的性能更加的優化。面試
HashMap的儲存結構數組
HashMap中的Hash安全
HashMap是怎麼保存數據的數據結構
HashMap的擴容操做多線程
HashMap的線程安全問題併發
只有當咱們知道HashMap的儲存結構時,咱們纔可以明白HashMap的工做原理。app
在JDK1.7中,HashMap採用的是數組【位桶】+單鏈表
的數據結構性能
圖片來自這裏
在JDK1.8中,與JDK1.7最不相同的地方就是,採用了紅黑樹進行儲存,採用的是數組【位桶】+鏈表+紅黑樹
,當鏈表的長度超過某一閥值時,就會將鏈表轉換爲紅黑樹,這個閥值能夠本身設置,默認是8。
圖片來自這裏
首先先說HashMap中的hash。當咱們使用HashMap中的put(k,v)
時,
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
首先咱們要根據key
算出key的hash值。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
這個hash值不只僅是經過Object中的hashCode的獲得的,還須要進行右移和^位異或。
總所周知,HashMap默認的容量大小是16,那麼當咱們儲存一個值時,是怎麼判斷儲存的位置呢?
首先咱們須要明白幾個參數。在使用HashMap的時候咱們極可能會使用如下的構造參數:
public HashMap(int initialCapacity, float loadFactor) ;
未產生hash衝突
// n是HashMap的大小,Hash爲key的hash值,tab爲以下圖中的table,i表明儲存的位置
int i;
// 爲null表明此位置爲空的
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
產生hash衝突
可是若是當咱們獲得的hash值
同樣或者說相與
的結果的table位置已經存在一個值了,那麼咱們應該怎麼去儲存呢?
當key與table[i]的全部key進行equals比較,若是相同則直接更新覆蓋value。
假如key進行equals比較不相同,則進行元素的插入操做(在jdk1.7中是鏈表的插入,在jdk1.8中既有鏈表的插入操做也有紅黑樹的操做)。
HashMap保存數據的JKD1.8源代碼看源代碼可以更好的理解HashMap的put操做
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 假如table是空的或者說長度爲0,則進行擴容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 假如桶中的元素是空的,則直接將元素放在桶中【使用(n - 1) & hash]判斷放的位置】
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 假如桶中已經存在這個元素
else {
Node<K,V> e; K k;
// 假如桶中的第一個元素p的hash值,key與要存的值相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;// 使用e來記錄p
// TreeNode 表明紅黑樹節點
// 假如key不相等,則將元素放入紅黑樹節點中
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 假如p爲鏈表節點
else {
// 進行鏈表查找
for (int binCount = 0; ; ++binCount) {
// 假如next爲空【表明達到鏈表末尾】
if ((e = p.next) == null) {
// 在末尾插入新的節點
p.next = newNode(hash, key, value, null);
// 若是鏈表長度達到閥值,則轉化爲紅黑樹
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
// 插入元素後跳出循環
break;
}
// 在鏈表中也會遇到key同樣的元素,則時候就跳出循環
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 此時e爲鏈表中key相等的元素
break;
p = e;
}
}
// e不爲nul,表明要相同的元素
if (e != null) { // existing mapping for key
V oldValue = e.value;
// 若是onlyIfAbsent爲false或者舊值爲空,則進行更新
// 在源碼中onlyIfAbsent默認是false
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// 回調以容許LinkedHashMap過後操做
afterNodeAccess(e);
// 返回舊值
return oldValue;
}
}
// modeCount表明HashMap在結構上面被修改的次數
++modCount;
// 加入大小大於閥值則進行擴容
if (++size > threshold)
resize();
// 回調以容許LinkedHashMap過後操做
afterNodeInsertion(evict);
return null;
}
在HashMap中進行擴容操做是特別耗費時間的,由於隨着擴容,會從新進行一次hash分配,遍歷hash表中的全部元素,由於桶的大小【也就是數組長度n】變了,那麼(n - 1) & hash
的值也會發生改變,因此咱們在編寫程序時應該儘可能避免resize,儘可能在新建HashMap對象的時候指令桶的長度【阿里巴巴開發手冊也是這樣推薦使用】。
HashMap進行擴容時,會徹底新建一個桶,咱們從上面瞭解到桶就是數組,而數組是沒辦法自動擴容的,因此咱們須要用一個新的數組來代替前面的桶。而當HashMap進行擴容是,閥值
會變成原來的兩倍
,容量
也會變成原來的兩倍
首先咱們先講講JDK1.7中的resize(),JDK1.8有紅黑樹,仍是有點麻煩。
void resize(int newCapacity) { //傳入新的容量
//table爲擴容前的Entry數組
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
// 若是擴容前的數組大小若是已經達到最大(2^30)
if (oldCapacity == MAXIMUM_CAPACITY) {
//修改閾值爲int的最大值(2^31-1),這樣之後就不會擴容了
threshold = Integer.MAX_VALUE;
return;
}
// 新建一個Entry數組
Entry[] newTable = new Entry[newCapacity];
//將數據轉移到新的Entry數組裏
transfer(newTable);
// 修改table的指向對象
table = newTable;
threshold = (int) (newCapacity * loadFactor);//修改閾值
}
void transfer(Entry[] newTable) {
Entry[] src = table; //src引用了舊的Entry數組
int newCapacity = newTable.length;
// 遍歷舊的Entry數組
for (int j = 0; j < src.length; j++) {
Entry<K, V> e = src[j];
// 若是此位置存在元素
if (e != null) {
// for循環事後,舊的Entry數組就再也不引用任何對象
src[j] = null;
// 遍歷鏈表
do {
// 得到鏈表中的下一個元素
Entry<K, V> next = e.next;
// 從新計算數據保存位置
int i = indexFor(e.hash, newCapacity);
// 在jdk1.7中是頭部插入,此時e.next指向新的數組位置newTable[i]
e.next = newTable[i];
// 將newTable指向e
newTable[i] = e;
// 訪問下一個Entry鏈上的元素
e = next;
} while (e != null);
}
}
}
static int indexFor(int h, int length) {
return h & (length - 1);
}
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
// 得到table的大小,並將其長度賦值給oldCap
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 閥值賦值
int oldThr = threshold;
int newCap, newThr = 0;
// 若是table不爲空
if (oldCap > 0) {
// 數組大小大於(2^30)
if (oldCap >= MAXIMUM_CAPACITY) {
// 修改閾值爲int的最大值(2^31-1),這樣之後就不會擴容了
threshold = Integer.MAX_VALUE;
return oldTab;
}
// newCap = oldCap << 1新的容量爲之前的兩倍
// 當新的table長度沒有超過最致使,且之前的table長度大於16,則進行閥值更新
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 閥值擴大成兩倍
newThr = oldThr << 1; // double threshold
}
// 若是table爲空,且閥值大於0
else if (oldThr > 0) // initial capacity was placed in threshold
// 則新的容量大小爲閥值
newCap = oldThr;
// 假如table爲空切閥值小於等於0,則初始化閥值,和table
else { // zero initial threshold signifies using defaults
// 新的table長度爲16
newCap = DEFAULT_INITIAL_CAPACITY;
// 新的閥值爲負載因子【0.75】*16
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;
/* *以上都是進行初始化操做,目的是擴大容量,或則初始化HashMap *下面即是從新存放元素操做 */
@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;
// 假如oldTab[j]中含有元素
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
// 假如沒有下一個元素,也就是oldTab[j]中只有e一個元素
if (e.next == null)
// 從新選擇空間
newTab[e.hash & (newCap - 1)] = e;
// 假若有下一個元素,且該節點爲紅黑樹節點
else if (e instanceof TreeNode)
// 將該節點進行rehash後,放到新的地方
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
/** * 在JDK1.8中不像JDK1.7同樣從新進行hash值計算,而是利用了一個規律: * 假如e.hash & oldCap爲0,那麼該元素的引索位置沒有變 * 假如e.hash & oldCap爲1,那麼該元素的引索位置爲原引索+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
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;
}
相信不少人都據說過HashMap線程不安全,可是HashMap爲何會產生線程安全問題呢?
設想一個場景,A線程正在進行put操做,它通過hash計算,以及鏈表查找,已經肯定了put的位置X
,可是這時候cpu時間片到了,A線程不得不退出put操做的執行,這時候B線程得到了cpu時間片,在X
的位置進行插入值,若是A線程再執行put操做就會覆蓋之前的值,此時數據就不一致了。
當多個線程進行resize()操做時,假如table已經變成新數組,那麼下一個線程會使用已經被賦值過得的table作爲初始值進行操做。這樣可能就會出現死循環的操做。
至於怎麼避免HashMap的多線程安全問題,ConcurrentHashMap是一個好東西,至於它是怎麼解決併發的問題,咱們下次再聊。
HashMap其實並非很難,咱們主要是要理解它儲存元素的思想與方法。而經過源代碼,咱們可以更好的理解設計的理念