簡介
HashMap是Java程序員使用頻率最高的用于映射(鍵犬辰、值對)處理的數(shù)據(jù)類型浑吟,它根據(jù)鍵的hashCode值存儲數(shù)據(jù),大多數(shù)情況下可以直接定位到它的值赶盔,因而具有很快的訪問速度历恐,但遍歷順序卻是不確定的寸癌。 HashMap最多只允許一條記錄的鍵為null,允許多條記錄的值為null弱贼,且HashMap不是線程安全的類,可以使用ConcurrentHashMap和Collections的SynchronizedMap方法使HashMap具有線程安全的能力磷蛹。在JDK1.8中對HashMap底層的實現(xiàn)進(jìn)行了優(yōu)化吮旅,引入紅黑樹、擴(kuò)容優(yōu)化等味咳。那就重新認(rèn)識一下JDK1.8的HashMap庇勃,來看看對其做了什么優(yōu)化。
存儲結(jié)構(gòu)
要搞清楚HashMap槽驶,首先需要知道HashMap是什么责嚷,即它的存儲結(jié)構(gòu);其次弄明白它能干什么掂铐,即它的功能如何實現(xiàn)罕拂。我們都知道HashMap使用哈希表來存儲的數(shù)據(jù)的揍异,根據(jù)key的哈希值進(jìn)行存儲的,但是不同的key之間可能存在相同的哈希值爆班,這樣就會產(chǎn)生沖突衷掷;哈希表為解決沖突,可以采用開放地址法和鏈地址法等來解決問題柿菩,Java中的HashMap采用了鏈地址法來解決哈希沖突戚嗅,簡單來說就是數(shù)組加鏈表的結(jié)合。在每個數(shù)組元素上都一個鏈表結(jié)構(gòu)枢舶,當(dāng)存放數(shù)據(jù)的時候如果產(chǎn)生了哈希沖突懦胞,先得到數(shù)組下標(biāo),把數(shù)據(jù)放在對應(yīng)下標(biāo)元素的鏈表上凉泄。這里我們思考一個問題医瘫,即使哈希算法設(shè)計的再合理,也免不了會出現(xiàn)拉鏈過長的情況旧困,一旦出現(xiàn)拉鏈過長醇份,則會嚴(yán)重影響HashMap的性能,在JDK1.8版本中吼具,對數(shù)據(jù)結(jié)構(gòu)做了進(jìn)一步的優(yōu)化僚纷,引入了紅黑樹;當(dāng)鏈表長度太長(默認(rèn)超過7)時拗盒,鏈表就轉(zhuǎn)換為紅黑樹怖竭,如下圖所示。
重要字段
HashMap是根據(jù)key的哈希值進(jìn)行存取的陡蝇,那HashMap的性能和哈希算法的好壞有著直接的關(guān)系痊臭,哈希算法計算結(jié)果越分散均勻,哈希碰撞的概率就越小登夫,map的存取效率就會越高广匙。當(dāng)然,也和哈希數(shù)組的大小有關(guān)系恼策,如果哈希數(shù)組很大鸦致,即使較差的哈希算法也會比較分散,如果哈希數(shù)組較小涣楷,即使較好的哈希算法也會出現(xiàn)較多的碰撞分唾,所以就需要權(quán)衡空間和時間成本,找到比較平衡的值狮斗。
JDK1.8版本也是權(quán)衡了時間绽乔、空間成本以及效率,對之前的版本做出了很多優(yōu)化碳褒;不僅對數(shù)據(jù)結(jié)構(gòu)進(jìn)行了優(yōu)化折砸,除此之外還對擴(kuò)容進(jìn)行的優(yōu)化看疗,大大的提高的HashMap的性能。下面我們通過源碼來一起看一下具體的實現(xiàn)鞍爱。
我們來看一下HashMap中比較重要的幾個屬性鹃觉。
//默認(rèn)的初始容量,必須是2的冪次方.
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//所能容納 key-value 個數(shù)的極限睹逃,當(dāng)Map 的size > threshold 會進(jìn)行擴(kuò)容 盗扇。容量 * 擴(kuò)容因子
int threshold;
//hashMap最大的容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//HashMap 默認(rèn)的桶數(shù)組的大小
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 16
//默認(rèn)的加載因子.當(dāng)容量超過 0.75*table.length 擴(kuò)容
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//HashMap的加載因子,在構(gòu)造器中指定的.
final float loadFactor;
//鏈表節(jié)點(diǎn)數(shù)大于8個鏈表轉(zhuǎn)紅黑樹
static final int TREEIFY_THRESHOLD = 8;
//紅黑樹節(jié)點(diǎn)轉(zhuǎn)換鏈表節(jié)點(diǎn)的閾值, 6個節(jié)點(diǎn)轉(zhuǎn)
static final int UNTREEIFY_THRESHOLD = 6;
//以Node數(shù)組存儲元素沉填,長度為2的次冪疗隶。
transient Node<K,V>[] table;
// 轉(zhuǎn)紅黑樹, table的最小長度
static final int MIN_TREEIFY_CAPACITY = 64;
// 鏈表節(jié)點(diǎn), 繼承自Entry
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
// ... ...
}
// 紅黑樹節(jié)點(diǎn)
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;
// ...
}
HashMap中的屬性還是比較好理解的。其實到這里會有一個疑問翼闹,為什么默認(rèn)的哈希桶數(shù)組table的長度為16斑鼻,而且長度必須為2的n次方呢?
這里我們先說一下為什么哈希數(shù)組的長度是2的n次方猎荠。
其實不管是在JDK1.7還是JDK1.8中坚弱,計算key索引位置都是通過hash & (length-1)計算得來的。
我們應(yīng)該知道 hash % length 等價于 hash & (length - 1)关摇。
假如有一個key的哈希值二進(jìn)制如下:這里我們就只看低位荒叶。
hahsCode 0010 0011 ———————轉(zhuǎn)成十進(jìn)制—————————> 35
& %
(length-1)=15: 0000 1111 length = 16
-----------------------------------------------------------------------------------------------
(二進(jìn)制) 0011 = (十進(jìn)制)3 3
為什么不用 hash % length 計算索引位,要使用 hash & (length -1)來計算呢输虱?計算機(jī)底層是二進(jìn)制數(shù)進(jìn)行計算和存儲些楣,&是接近計算機(jī)底層運(yùn)算,相比于% 運(yùn)算效率上應(yīng)該會快宪睹。
那為什么length必須是2的n次方呢愁茁?
hahsCode 0010 0011 0010 1111
&
(length-1)=15: 0000 1111 (length-1) = 13: 0000 1111
------------------------------------------------------------
0011 1111
hahsCode 0010 1110 1110 1100
&
(length-1)=13: 0000 0101 (length-1) = 13: 0000 0101
-----------------------------------------------------------
0100 0100
其實我們可以發(fā)現(xiàn),當(dāng)哈希數(shù)組的長度為2的n次方時亭病,length - 1的二進(jìn)制碼全都是1鹅很,這樣的話索引的位置,完全依賴于hash值的低位值命贴,而且產(chǎn)生沖突的幾率要比容量不是2的n次方的概率要低道宅,索引位就完全依賴于哈希值的低位,所以只要哈希值分布均勻胸蛛,那產(chǎn)生沖突的概率就會低很多,故而length是2的n次方更優(yōu)樱报。
其次葬项,當(dāng)length為2的n次方時,也方便做擴(kuò)容迹蛤,JDK1.8在擴(kuò)容算法上也進(jìn)行了優(yōu)化民珍,使用的方法也非常巧妙襟士。會在擴(kuò)容方法的時候講到。
功能實現(xiàn)
確定索引位置
不管是增加嚷量、刪除陋桂、查找,都需要定位到哈希桶的數(shù)組位置蝶溶,前面也說過HashMap的數(shù)據(jù)結(jié)構(gòu)是數(shù)組和鏈表的結(jié)合嗜历,所以我們當(dāng)然希望這個HashMap里面的元素位置盡量分布均勻些压状,盡量使得每個位置上的元素數(shù)量只有一個痛阻,那么當(dāng)我們用hash算法求得這個位置的時候芒篷,馬上就可以知道對應(yīng)位置的元素就是我們要的膏蚓,不用再遍歷鏈表查詢趣竣,大大優(yōu)化了查詢效率推沸。
tableSizeFor()這個方法蔚携,就是保證在HashMap()進(jìn)行初始化的時候房维,哈希桶數(shù)組的大小永遠(yuǎn)是2^n傻粘。
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;
/**
假如現(xiàn)在傳入的參數(shù)cap = 3
那 n = 2 對應(yīng)的二進(jìn)制 為 10
n = n | n>>>1 10|01 得到 11
....
....
n = 11(二進(jìn)制) = (10進(jìn)制) 3
最后return 返回的是4
*/
}
//JDK1.8的Hash算法
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// JDK 1.7的Hash算法
static final int hash(int h) {
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
//索引位置
index = hash & (length-1);
//JDK1.7 使用hashCode() + 4次位運(yùn)算 + 5次異或運(yùn)算(9次擾動)
//JDK 1.8 簡化了hash函數(shù) = 只做了2次擾動 = 1次位運(yùn)算 + 1次異或運(yùn)算每窖。
在JDK1.8的實現(xiàn)中,優(yōu)化了高位運(yùn)算的算法弦悉,通過hashCode()的高16位異或低16位實現(xiàn)的:(h = k.hashCode()) ^ (h >>> 16)窒典,主要是從速度、功效警绩、質(zhì)量來考慮的崇败,相比于JDK1.7來說,JDK1.8降低了哈希函數(shù)擾動的次數(shù)肩祥,也算是優(yōu)化了hash算法后室。這么做可以在HashMap容量較小的時候,也能保證考慮到高低Bit都參與到Hash的計算中混狠,同時不會有太大的開銷岸霹。
假如有一個key的哈希值二進(jìn)制如下
hahsCode 0000 0000 0011 0011 0111 1010 1000 1011
hahsCode>>>16 0000 0000 0000 0000 0000 0000 0011 0011
———————————————————————————————————————————————————————————————
位或^運(yùn)算 0000 0000 0011 0011 0111 1010 1011 1000
&
HashMap.size()-1 0000 0000 0000 0000 0000 0000 0000 1111
———————————————————————————————————————————————————————————————
0000 0000 0000 0000 0000 0000 0000 1000 轉(zhuǎn)成十進(jìn)制是 8
put方法
從網(wǎng)上找到了一個流程圖,感覺很不錯将饺,就直接拿過來用了贡避,嘿嘿....畫的也是比較清楚的∮杌。看著流程圖刮吧,再結(jié)合源碼一看就明白。
<img src="https://user-gold-cdn.xitu.io/2019/12/30/16f547c76710e6d7?w=1716&h=1360&f=png&s=314628" width = "550" height = "450" alt="圖片名稱" align="center">
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//判斷哈希桶數(shù)組是否為空掖蛤。
if ((tab = table) == null || (n = tab.length) == 0)
//如果哈希桶數(shù)組為空杀捻,對其進(jìn)行初始化。默認(rèn)的桶數(shù)組大小為16
n = (tab = resize()).length;
//如果桶數(shù)組不為空蚓庭,得到計算key的索引位置致讥,判斷此索引所在位置是否已經(jīng)被占用了仅仆。
if ((p = tab[i = (n - 1) & hash]) == null)
//如果沒有被占用,那就封裝成Node節(jié)點(diǎn)垢袱,放入哈希桶數(shù)組中墓拜。
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//如果要插入的Node節(jié)點(diǎn)已經(jīng)存在,那就將舊的Node替換请契。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//如果不存在咳榜,那就插入,判斷插入的節(jié)點(diǎn)是不是樹節(jié)點(diǎn)姚糊。
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//如果是普通節(jié)點(diǎn)贿衍,循環(huán)哈希桶對應(yīng)的鏈表,將節(jié)點(diǎn)插入到鏈表末尾
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {d
p.next = newNode(hash, key, value, null);
//如果鏈表的長度大于7,就把節(jié)點(diǎn)轉(zhuǎn)成樹節(jié)點(diǎn)
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//如果鏈表中節(jié)點(diǎn)已經(jīng)存在救恨,那就將舊的節(jié)點(diǎn)替換贸辈。
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)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//如果超過了擴(kuò)容的臨界點(diǎn),就進(jìn)行擴(kuò)容肠槽。
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
get方法
get方法相對于put方法可能簡單一點(diǎn)擎淤,通過源碼一看就能明白。廢話不多說秸仙,直接上代碼看一下吧嘴拢。
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//哈希桶數(shù)組不為空,且根據(jù)傳入的key計算出索引位置的Node不為空寂纪。
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//如果計算出來的第一個哈希桶位置的Node就是要找的Node節(jié)點(diǎn)席吴,直接返回。
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
//如果是樹節(jié)點(diǎn)捞蛋,直接通過樹節(jié)點(diǎn)的方式查找孝冒。
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
//循環(huán)遍歷哈希桶所在的鏈表
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
擴(kuò)容機(jī)制(resize)
我個人認(rèn)為HashMap的擴(kuò)容算法是整個HashMap中的精髓部分,使用的方法也十分巧妙拟杉,不得不佩服庄涡,大佬還是大佬。
final Node<K,V>[] resize() {
//將當(dāng)前的table 暫存的 oldTab中進(jìn)行操作
Node<K,V>[] oldTab = table;
//當(dāng)前的哈希桶數(shù)組大小
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//當(dāng)前的擴(kuò)容臨界值
int oldThr = threshold;
int newCap, newThr = 0;
//如果當(dāng)前哈希桶數(shù)組不為空
if (oldCap > 0) {
//如果容量超過最大值
if (oldCap >= MAXIMUM_CAPACITY) {
//修改閾值為2^31-1
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 沒超過最大值搬设,就擴(kuò)充為原來的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0)
// 當(dāng)使用 new HashMap(int initialCapacity) 初始化后穴店,第一次 put 的時候
newCap = oldThr;
else {
//如果代碼到這里,說明hashMap還沒有進(jìn)行初始化, 對應(yīng) new HashMap()拿穴;
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 計算新的resize上限
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 將當(dāng)前閾值設(shè)置為剛計算出來的新的閾值泣洞,定義新表,容量為剛計算出來的新容量默色。將舊Hash桶中的元素斜棚,移動到新的Hash數(shù)組中。
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
/**
如果只是對HashMap的初始化來說该窗,到這里已經(jīng)結(jié)束了
下面代碼是將老的數(shù)組中的數(shù)據(jù)弟蚀,移動到擴(kuò)容以后的哈希桶數(shù)組中。
**/
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
// 將老表的節(jié)點(diǎn)設(shè)置為空, 以便垃圾收集器回收
oldTab[j] = null;
//判斷哈希桶位置是否只有一個節(jié)點(diǎn)酗失。如果這個位置只有一個節(jié)點(diǎn)义钉,那就將這個節(jié)點(diǎn)在擴(kuò)容以后的哈希桶數(shù)組中重新計算位置
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
//如果是紅黑樹節(jié)點(diǎn),則進(jìn)行紅黑樹的重hash分布
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
//如果是普通的鏈表節(jié)點(diǎn)规肴,則需要將這個位置上的所有節(jié)點(diǎn)重新在新的哈希桶數(shù)組中計算位置捶闸。
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
//如果要移動節(jié)點(diǎn)的hash值與老的容量進(jìn)行 "&" 運(yùn)算,如果結(jié)果為0拖刃,那在擴(kuò)容以后的數(shù)組中删壮,還是同樣的位置。
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
//如果運(yùn)算不為0,則擴(kuò)容后的索引位置為:老表的索引位置+擴(kuò)容之前的
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;
}
在源碼中有這么一段(e.hash & oldCap) == 0,怎么理解這個呢兑牡,我們通過下面的來看一下央碟。
假設(shè)擴(kuò)容之前 數(shù)組大小為16
假如有兩個key:
key1(hash&hash>>>16) 0000 0000 0011 0011 0111 1010 1011 1000
key2(hash&hash>>>16) 0000 0000 0011 0011 0111 1010 1010 1000
&
length-1 = 15 0000 0000 0000 0000 0000 0000 0000 1111
——————————————————————————————————————————————————————————————————
key1: 1000 轉(zhuǎn)成十進(jìn)制 8
key2: 1000 轉(zhuǎn)成十進(jìn)制 8
哈希沖突的兩個key,在擴(kuò)容到32之后
key1(key的hash&hash>>>16) 0000 0000 0011 0011 0111 1010 1011 1000
key2(key的hash&hash>>>16) 0000 0000 0011 0011 0111 1010 1010 1000
&
length-1 = 31 0000 0000 0000 0000 0000 0000 0001 1111
——————————————————————————————————————————————————————————————————
key1: 1 1000 轉(zhuǎn)乘二進(jìn)制 24=16+8
key2: 0 1000 轉(zhuǎn)乘二進(jìn)制 8
我們可以發(fā)現(xiàn)均函,代碼中利用的是 hash & oldCap(key的哈希值 & 擴(kuò)容之前的哈希桶長度)亿虽,而不是利用hash & newCap- 1 ,為什么要這樣做呢苞也?
通過上面的情況我們應(yīng)該可以看出洛勉,原本沖突的兩個key,在擴(kuò)容以后的位置是由新增的那一個bit位來決定的如迟。所以直接用 hash & 擴(kuò)容之前的容量來判斷新增的那個bit位是0 還是 1 收毫,就可以知道是在原來的位置上,還是在 原來位置+oldCap 位置上殷勘。
哈希沖突的兩個key此再,在擴(kuò)容到32之后
key1(key的hash&hash>>>16) 0000 0000 0011 0011 0111 1010 1011 1000
key2(key的hash&hash>>>16) 0000 0000 0011 0011 0111 1010 1010 1000
&
oldCap=16 0000 0000 0000 0000 0000 0000 0001 0000
--------------------------------------------------------------------
key1: 1 0000
key2: 0 0000
通過以上,我們可以看到劳吠,原來在同一個位置上的兩個key引润,通過擴(kuò)容之后的位置要不在原來的位置上,要不在oldCap+原位置上痒玩。這樣不需要像JDK1.7的實現(xiàn)那樣重新計算hash淳附,只需要看看原來的hash值新增的那個bit是1還是0就好了,是0的話索引沒變蠢古,是1的話索引變成“原索引+oldCap”奴曙。同時也是更加充分的說明了,為什么HashMap的容量必須是2的n次方了草讶。
知道HashTable的同學(xué)應(yīng)該也知道洽糟,HashTable默認(rèn)的容量是11,每次擴(kuò)容之后的容量是length*2+1。HashTable在設(shè)計上坤溃,想通過保證哈希桶數(shù)組為質(zhì)數(shù)拍霜,來盡可能保證哈希散列均勻。這里也就不對HashTable的容量為什么是質(zhì)數(shù)進(jìn)行展開薪介,感興趣的同學(xué)可以查一下資料祠饺,為什么對質(zhì)數(shù)求模會比對合數(shù)求模的哈希散列效果要好,可以參考http://blog.csdn.net/liuqiyao_01/article/details/14475159汁政。HashMap雖然也是想通過類似HashTable中的哈希算法那樣(通過對質(zhì)數(shù)求模來)降低哈希沖突道偷,顯然HashMap中的哈希算法要比HashTable中哈希算法要優(yōu)秀很多。
HashMap保證哈希數(shù)組的大小為2的n次方记劈,這個設(shè)計確實非常的巧妙勺鸦,利用2的n次方 - 1 二進(jìn)制碼全為1這個特性,在擴(kuò)容的時候目木,既省去了重新計算hash值的時間换途,而且同時,由于新增的1bit是0還是1可以認(rèn)為是隨機(jī)的嘶窄,因此resize的過程怀跛,均勻的把之前的沖突的節(jié)點(diǎn)分散到新的bucket了。
總結(jié)
- 擴(kuò)容是一個特別耗性能的操作柄冲,所以當(dāng)我們在使用HashMap的時候吻谋,估算map的大小,初始化的時候給一個大致的數(shù)值现横,避免map進(jìn)行頻繁的擴(kuò)容漓拾。
- HashMap是線程不安全的,不要在并發(fā)的環(huán)境中使用HashMap戒祠,使用ConcurrentHashMap或者SynchronizedMap
- 負(fù)載因子是可以修改的骇两,也可以大于1,但是建議不要輕易修改姜盈。