大熱的《阿里巴巴Java開發規約》中有提到:html
【推薦】集合初始化時,指定集合初始值大小。java
說明:HashMap使用以下構造方法進行初始化,若是暫時沒法肯定集合大小,那麼指定默認值(16)便可:算法
public HashMap (int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}數組
看到代碼規約這一條的時候,我以爲是否是有點太 low 了,身爲開發,你們都知道 HashMap 的原理。性能優化
什麼?這個要經過插件監測?不必吧,哪一個開發不知道默認大小,什麼時候 resize 啊,而後我和孤盡打賭隨機諮詢幾位同窗如下幾個問題:數據結構
HashMap 默認bucket數組多大?多線程
若是new HashMap<>(19),bucket數組多大?併發
HashMap 何時開闢bucket數組佔用內存?app
HashMap 什麼時候擴容?dom
抽樣調查的結果出乎個人意料:
HashMap 默認bucket數組多大?(答案是16,大概一半的同窗答錯)
若是new HashMap<>(19),bucket數組多大?(答案是32,大多被諮詢同窗都不太瞭解這個點)
HashMap 何時開闢bucket數組佔用內存?(答案是第一次 put 時,一半同窗認爲是 new 的時候)
HashMap 什麼時候擴容?(答案是put的元素達到容量乘負載因子的時候,默認16*0.75,有1/4同窗中槍)
HashMap 是寫代碼時最經常使用的集合類之一,看來你們也不是全都很瞭解。孤盡乘勝追擊又拋出問題:JDK8中 HashMap 和以前 HashMap 有什麼不一樣?
我知道 JDK8 中 HashMap 引入了紅黑樹來處理哈希碰撞,具體細節和源代碼並無仔細翻過,看來是時候對比翻看下 JDK8 和 JDK7 的 HashMap 源碼了。
經過對比翻看源碼,先說下結論:
HashMap 在 new 後並不會當即分配bucket數組,而是第一次 put 時初始化,相似 ArrayList 在第一次 add 時分配空間。
HashMap 的 bucket 數組大小必定是2的冪,若是 new 的時候指定了容量且不是2的冪,實際容量會是最接近(大於)指定容量的2的冪,好比 new HashMap<>(19),比19大且最接近的2的冪是32,實際容量就是32。
HashMap 在 put 的元素數量大於 Capacity * LoadFactor(默認16 * 0.75) 以後會進行擴容。
JDK8在哈希碰撞的鏈表長度達到TREEIFY_THRESHOLD(默認8)後,會把該鏈表轉變成樹結構,提升了性能。
JDK8在 resize 的時候,經過巧妙的設計,減小了 rehash 的性能消耗。
存儲結構
JDK7 中的 HashMap 仍是採用你們所熟悉的數組+鏈表的結構來存儲數據。
JDK8 中的 HashMap 採用了數組+鏈表或樹的結構來存儲數據。
重要參數
HashMap中有兩個重要的參數,容量(Capacity) 和 負載因子(Load factor)
Initial capacity The capacity is the number of buckets in the hash table, The initial capacity is simply the capacity at the time the hash table is created.
Load factor The load factor is a measure of how full the hash table is allowed to get before its capacity is automatically increased.
Initial capacity 決定 bucket 的大小,Load factor 決定 bucket 內數據填充比例,基於這兩個參數的乘積,HashMap 內部由 threshold 這個變量來表示 HashMap 能放入的元素個數。
Capacity 就是 HashMap 中數組的 length
loadFactor 通常都是使用默認的0.75
threshold 決定能放入的數據量,通常狀況下等於 Capacity * LoadFactor
以上參數在 JDK7 和 JDK8中是一致的,接下來會根據實際代碼分析。
JDK8 中的 HashMap 實現
new
HashMap 的bucket數組並不會在new 的時候分配,而是在第一次 put 的時候經過 resize() 函數進行分配。
JDK8中 HashMap 的bucket數組大小確定是2的冪,對於2的冪大小的 bucket,計算下標只須要 hash 後按位與 n-1,比%模運算取餘要快。若是你經過 HashMap(int initialCapacity) 構造器傳入initialCapacity,會先計算出比initialCapacity大的 2的冪存入 threshold,在第一次 put 的 resize() 初始化中會按照這個2的冪初始化數組大小,此後 resize 擴容也都是每次乘2,這麼設計的緣由後面會詳細講。
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
// 比initialCapacity大的2的 N 次方,先存在threshold中,resize() 中會處理
this.threshold = tableSizeFor(initialCapacity);
}
/**
* 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;
}
hash
JKD8 中put 和 get 時,對 key 的 hashCode 先用 hash 函數散列下,再計算下標:
具體 hash 代碼以下:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
因爲 h>>>16,高16bit 補0,一個數和0異或不變,因此 hash 函數大概的做用就是:高16bit不變,低16bit和高16bit作了一個異或,目的是減小碰撞。
按照函數註釋,由於bucket數組大小是2的冪,計算下標index = (table.length - 1) & hash,若是不作 hash 處理,至關於散列生效的只有幾個低 bit 位,爲了減小散列的碰撞,設計者綜合考慮了速度、做用、質量以後,使用高16bit和低16bit異或來簡單處理減小碰撞,並且 JDK8中用了複雜度 O(logn)的樹結構來提高碰撞下的性能。具體性能提高能夠參考Java 8:HashMap的性能提高
put
put函數的思路大體分如下幾步:
對key的hashCode()進行hash後計算數組下標index;
若是當前數組table爲null,進行resize()初始化;
若是沒碰撞直接放到對應下標的bucket裏;
若是碰撞了,且節點已經存在,就替換掉 value;
若是碰撞後發現爲樹結構,掛載到樹上。
若是碰撞後爲鏈表,添加到鏈表尾,並判斷鏈表若是過長(大於等於TREEIFY_THRESHOLD,默認8),就把鏈表轉換成樹結構;
數據 put 後,若是數據量超過threshold,就要resize。
具體代碼以下:
public V put(K key, V value) {
// 對key的hashCode()作hash
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;
// 初始 tab 爲 null,resize 初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 計算下標index,沒碰撞,直接放
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;
}
}
// 節點已存在,替換value
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 超過threshold,進行resize擴容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// 數量大於 64 纔會發生轉換,避免初期,多個鍵值對剛好放入同一個鏈表中而致使沒必要要的轉化
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
resize
resize()用來第一次初始化,或者 put 以後數據超過了threshold後擴容,resize的註釋以下:
Initializes or doubles table size. If null, allocates in accord with initial capacity target held in field threshold. Otherwise, because we are using power-of-two expansion, the elements from each bin must either stay at same index, or move with a power of two offset in the new table.
數組下標計算: index = (table.length - 1) & hash ,因爲 table.length 也就是capacity 確定是2的N次方,使用 & 位運算意味着只是多了最高位,這樣就不用從新計算 index,元素要麼在原位置,要麼在原位置+ oldCapacity。
若是增長的高位爲0,resize 後 index 不變,如圖所示:
若是增長的高位爲1,resize 後 index 增長 oldCap,如圖所示:
這個設計的巧妙之處在於,節省了一部分從新計算hash的時間,同時新增的一位爲0或1的機率能夠認爲是均等的,因此在resize 的過程當中就將原來碰撞的節點又均勻分佈到了兩個bucket裏。
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
// oldCap > 0,是擴容而不是初始化
if (oldCap > 0) {
// 超過最大值就再也不擴充了,而且把閾值增大到Integer.MAX_VALUE
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 沒超過最大值,就擴充爲原來的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// oldCap 爲0,oldThr 不爲0,第一次初始化 table,通常是經過帶參數的構造器
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
// oldCap 和 oldThr 都爲0的初始化,通常是經過無參構造器生成,用默認值
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 若是到這裏 newThr 還沒計算,則計算新的閾值threshold
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) {
// 把每一個bucket都移動到新的buckets中
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;
// 原索引(e.hash 和相似 0010000 按位與,結果爲0,說明原高位爲0)
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
// 原索引+oldCap
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 原索引放到bucket裏
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 原索引+oldCap放到bucket裏
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
JDK7 中的 HashMap 實現
new
JDK7 裏 HashMap的bucket數組也不會在new 的時候分配,也是在第一次 put 的時候經過 inflateTable() 函數進行分配。
JDK7中 HashMap 的bucket數組大小也必定是2的冪,一樣有計算下標簡便的優勢。若是你經過 HashMap(int initialCapacity) 構造器傳入initialCapacity,會先存入 threshold,在第一次 put 時調用 inflateTable() 初始化,會計算出比initialCapacity大的2的冪做爲初始化數組的大小,此後 resize 擴容也都是每次乘2。
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
threshold = initialCapacity;
init();
}
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
// ···省略代碼
}
// 第一次 put 時,初始化 table
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
int capacity = roundUpToPowerOf2(toSize);
// threshold 在不超過限制最大值的前提下等於 capacity * loadFactor
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
// 接下來hash 部分分析
initHashSeedAsNeeded(capacity);
}
hash
JKD7 中,bucket數組下標也是按位與計算,可是 hash 函數與 JDK8稍有不一樣,代碼註釋以下:
Retrieve object hash code and applies a supplemental hash function to the result hash, which defends against poor quality hash functions. This is critical because HashMap uses power-of-two length hash tables, that otherwise encounter collisions for hashCodes that do not differ in lower bits. Note: Null keys always map to hash 0, thus index 0.
hash爲了防止只有 hashCode() 的低 bit 位參與散列容易碰撞,也採用了位移異或,只不過不是高低16bit,而是以下代碼中屢次位移異或。
JKD7的 hash 中存在一個開關:hashSeed。開關打開(hashSeed不爲0)的時候,對 String 類型的key 採用sun.misc.Hashing.stringHash32的 hash 算法;對非 String 類型的 key,多一次和hashSeed的異或,也能夠必定程度上減小碰撞的機率。
JDK 7u40之後,hashSeed 被移除,在 JDK8中也沒有再採用,由於stringHash32()的算法基於MurMur哈希,其中hashSeed的產生使用了Romdum.nextInt()實現。Rondom.nextInt()使用AtomicLong,它的操做是CAS的(Compare And Swap)。這個CAS操做當有多個CPU核心時,會存在許多性能問題。所以,這個替代函數在多核處理器中表現出了糟糕的性能。
具體hash 代碼以下所示:
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// 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);
}
/**
* 下標計算依然是使用按位與
*/
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}
hashSeed 默認值是0,也就是默認關閉,任何數字與0異或不變。hashSeed 會在capacity發生變化的時候,經過initHashSeedAsNeeded()函數進行計算。當capacity大於設置值Holder.ALTERNATIVE_HASHING_THRESHOLD後,會經過sun.misc.Hashing.randomHashSeed產生hashSeed 值,這個設定值是經過 JVM的jdk.map.althashing.threshold參數來設置的,具體代碼以下:
final boolean initHashSeedAsNeeded(int capacity) {
boolean currentAltHashing = hashSeed != 0;
boolean useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
boolean switching = currentAltHashing ^ useAltHashing;
if (switching) {
hashSeed = useAltHashing
? sun.misc.Hashing.randomHashSeed(this)
: 0;
}
return switching;
}
/**
* holds values which can't be initialized until after VM is booted.
*/
private static class Holder {
/**
* Table capacity above which to switch to use alternative hashing.
*/
static final int ALTERNATIVE_HASHING_THRESHOLD;
static {
String altThreshold = java.security.AccessController.doPrivileged(
new sun.security.action.GetPropertyAction(
"jdk.map.althashing.threshold"));
int threshold;
try {
threshold = (null != altThreshold)
? Integer.parseInt(altThreshold)
: ALTERNATIVE_HASHING_THRESHOLD_DEFAULT;// ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE
// disable alternative hashing if -1
if (threshold == -1) {
threshold = Integer.MAX_VALUE;
}
if (threshold < 0) {
throw new IllegalArgumentException("value must be positive integer.");
}
} catch(IllegalArgumentException failed) {
throw new Error("Illegal value for 'jdk.map.althashing.threshold'", failed);
}
ALTERNATIVE_HASHING_THRESHOLD = threshold;
}
}
put
JKD7 的put相比於 JDK8就要簡單一些,碰撞之後只有鏈表結構。具體代碼以下:
public V put(K key, V value) {
// 初始化
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
// 計算 hash 和下標
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
// 查到相同 key,替換 value
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;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
// 若是數據量達到threshold,須要 resize 擴容
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
//在鏈表頭部添加新元素
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
resize
JDK7的 resize() 也是擴容兩倍,不過擴容過程相對JDK8就要簡單許多,因爲默認initHashSeedAsNeeded內開關都是關閉狀態,因此通常狀況下transfer 不須要進行 rehash,能減小一部分開銷。代碼以下所示:
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];
// 擴容後轉移數據,根據initHashSeedAsNeeded判斷是否 rehash
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
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;
// 根據以前的分析,默認狀況是不須要 rehash 的
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;
}
}
}
總結
HashMap 在 new 後並不會當即分配bucket數組,而是第一次 put 時初始化,相似 ArrayList 在第一次 add 時分配空間。
HashMap 的 bucket 數組大小必定是2的冪,若是 new 的時候指定了容量且不是2的冪,實際容量會是最接近(大於)指定容量的2的冪,好比 new HashMap<>(19),比19大且最接近的2的冪是32,實際容量就是32。
HashMap 在 put 的元素數量大於 Capacity * LoadFactor(默認16 * 0.75) 以後會進行擴容。
JDK8處於提高性能的考慮,在哈希碰撞的鏈表長度達到TREEIFY_THRESHOLD(默認8)後,會把該鏈表轉變成樹結構。
JDK8在 resize 的時候,經過巧妙的設計,減小了 rehash 的性能消耗。
相對於 JDK7的1000餘行代碼,JDK8代碼量達到了2000餘行,對於這個你們最經常使用的數據結構增長了很多的性能優化。
仔細看完上面的分析和源碼,對 HashMap 內部的細節又多了些瞭解,有空的時候仍是多翻翻源碼,^_^
HashMap一般會用一個指針數組(假設爲table[])來作分散全部的key,當一個key被加入時,會經過Hash算法經過key算出這個數組的下標i,而後就把這個<key, value>插到table[i]中,若是有兩個不一樣的key被算在了同一個i,那麼就叫衝突,又叫碰撞,這樣會在table[i]上造成一個鏈表。
咱們知道,若是table[]的尺寸很小,好比只有2個,若是要放進10個keys的話,那麼碰撞很是頻繁,因而一個O(1)的查找算法,就變成了鏈表遍歷,性能變成了O(n),這是Hash表的缺陷(可參看《Hash Collision DoS 問題》)。
因此,Hash表的尺寸和容量很是的重要。通常來講,Hash表這個容器當有數據要插入時,都會檢查容量有沒有超過設定的thredhold,若是超過,須要增大Hash表的尺寸,可是這樣一來,整個Hash表裏的無素都須要被重算一遍。這叫rehash,這個成本至關的大。
一、多線程put操做後,get操做致使死循環。
二、多線程put非NULL元素後,get操做獲得NULL值。
三、多線程put操做,致使元素丟失。
爲什麼出現死循環?
你們都知道,HashMap採用鏈表解決Hash衝突,具體的HashMap的分析能夠參考一下Java集合—HashMap源碼剖析 的分析。由於是鏈表結構,那麼就很容易造成閉合的鏈路,這樣在循環的時候只要有線程對這個HashMap進行get操做就會產生死循環。可是,我好奇的是,這種閉合的鏈路是如何造成的呢。在單線程狀況下,只有一個線程對HashMap的數據結構進行操做,是不可能產生閉合的迴路的。那就只有在多線程併發的狀況下才會出現這種狀況,那就是在put操做的時候,若是size>initialCapacity*loadFactor,那麼這時候HashMap就會進行rehash操做,隨之HashMap的結構就會發生翻天覆地的變化。頗有可能就是在兩個線程在這個時候同時觸發了rehash操做,產生了閉合的迴路。
下面咱們從源碼中一步一步地分析這種迴路是如何產生的。先看一下put操做:
存儲數據put
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中put元素的時候,先根據key的hash值獲得這個元素在數組中的位置(即下標),而後就能夠把這個元素放到對應的位置中了。 若是這個元素所在的位置上已經存放有其餘元素了,那麼在同一個位子上的元素將以鏈表的形式存放,新加入的放在鏈頭,而先前加入的放在鏈尾。
檢查容量是否超標addEntry
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
能夠看到,若是如今size已經超過了threshold,那麼就要進行resize操做,新建一個更大尺寸的hash表,而後把數據從老的Hash表中遷移到新的Hash表中:
調整Hash表大小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);
}
當table[]數組容量較小,容易產生哈希碰撞,因此,Hash表的尺寸和容量很是的重要。通常來講,Hash表這個容器當有數據要插入時,都會檢查容量有沒有超過設定的thredhold,若是超過,須要增大Hash表的尺寸,這個過程稱爲resize。
多個線程同時往HashMap添加新元素時,屢次resize會有必定機率出現死循環,由於每次resize須要把舊的數據映射到新的哈希表,這一部分代碼在HashMap#transfer() 方法,以下:
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;
}
}
}
transfer部分代碼是致使多線程使用hashmap出現CUP使用率驟增,從而多個線程阻塞的罪魁禍首。
HashMap在併發執行put操做時會引發死循環, 是由於多線程會致使 HashMap的 Entry鏈表造成環形數據結構, 一旦造成環形數據結構, Entry 的 next 節點永遠不爲空, 就會產生死循環獲取 Entry。