java.util.HashMap是很常見的類,前段時(shí)間公司系統(tǒng)由于對(duì)HashMap使用不當(dāng),導(dǎo)致cpu百分之百,在并發(fā)環(huán)境下使用HashMap而沒有做同步,可能會(huì)引起死循環(huán),關(guān)于這一點(diǎn),sun的官方網(wǎng)站上已有闡述,這并非是bug。
HashMap的數(shù)據(jù)結(jié)構(gòu)
HashMap主要是用數(shù)組來存儲(chǔ)數(shù)據(jù)的,我們都知道它會(huì)對(duì)key進(jìn)行哈希運(yùn)算,哈系運(yùn)算會(huì)有重復(fù)的哈希值,對(duì)于哈希值的沖突,HashMap采用鏈表來解決的。在HashMap里有這樣的一句屬性聲明:
transient Entry[] table;
Entry就是HashMap存儲(chǔ)的數(shù)據(jù),它擁有的屬性如下
final K key;
V value;
final int hash;
Entry<K,V> next;
看到next了嗎?next就是為了哈希沖突而存在的。比如通過哈希運(yùn)算,一個(gè)新元素應(yīng)該在數(shù)組的第10個(gè)位置,但是第10個(gè)位置已經(jīng)有Entry,那么好吧,將新加的元素也放到第10個(gè)位置,將第10個(gè)位置的原有Entry賦值給當(dāng)前新加的Entry的next屬性。數(shù)組存儲(chǔ)的鏈表,鏈表是為了解決哈希沖突的,這一點(diǎn)要注意。
幾個(gè)關(guān)鍵的屬性
存儲(chǔ)數(shù)據(jù)的數(shù)組
transient Entry[] table; 這個(gè)上面已經(jīng)講到了
默認(rèn)容量
static final int DEFAULT_INITIAL_CAPACITY = 16;
最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
默認(rèn)加載因子,加載因子是一個(gè)比例,當(dāng)HashMap的數(shù)據(jù)大小>=容量*加載因子時(shí),HashMap會(huì)將容量擴(kuò)容
static final float DEFAULT_LOAD_FACTOR = 0.75f;
當(dāng)實(shí)際數(shù)據(jù)大小超過threshold時(shí),HashMap會(huì)將容量擴(kuò)容,threshold=容量*加載因子
int threshold;
加載因子
final float loadFactor;
HashMap的初始過程
構(gòu)造函數(shù)1
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);
// Find a power of 2 >= initialCapacity
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
table = new Entry[capacity];
init();
}
重點(diǎn)注意這里
while (capacity < initialCapacity)
capacity <<= 1;
capacity才是初始容量,而不是initialCapacity,這個(gè)要特別注意,如果執(zhí)行new HashMap(9,0.75);那么HashMap的初始容量是16,而不是9,想想為什么吧。
構(gòu)造函數(shù)2
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
構(gòu)造函數(shù)3,全部都是默認(rèn)值
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
table = new Entry[DEFAULT_INITIAL_CAPACITY];
init();
}
構(gòu)造函數(shù)4
public HashMap(Map<? extends K, ? extends V> m) {
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
putAllForCreate(m);
}
如何哈希
HashMap并不是直接將對(duì)象的hashcode作為哈希值的,而是要把key的hashcode作一些運(yùn)算以得到最終的哈希值,并且得到的哈希值也不是在數(shù)組中的位置哦,無論是get還是put還是別的方法,計(jì)算哈希值都是這一句:
int hash = hash(key.hashCode());
hash函數(shù)如下:
static int hash(int h) {
return useNewHash ? newHash(h) : oldHash(h);
}
useNewHash聲明如下:
private static final boolean useNewHash;
static { useNewHash = false; }
這說明useNewHash其實(shí)一直為false且不可改變的,hash函數(shù)里對(duì)useNewHash的判斷真是多余的。
private static int oldHash(int h) {
h += ~(h << 9);
h ^= (h >>> 14);
h += (h << 4);
h ^= (h >>> 10);
return h;
}
private static int newHash(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);
}
其實(shí)HashMap的哈希函數(shù)會(huì)一直都是oldHash。
如果確定數(shù)據(jù)的位置
看下面兩行
int hash = hash(k.hashCode());
int i = indexFor(hash, table.length);
第一行,上面講過了,是得到哈希值,第二行,則是根據(jù)哈希指計(jì)算元素在數(shù)組中的位置了,位置的計(jì)算是將哈希值和數(shù)組長(zhǎng)度按位與運(yùn)算。
static int indexFor(int h, int length) {
return h & (length-1);
}
put方法到底作了什么?
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
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;
}
如果key為NULL,則是單獨(dú)處理的,看看putForNullKey方法:
private V putForNullKey(V value) {
int hash = hash(NULL_KEY.hashCode());
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
if (e.key == NULL_KEY) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, (K) NULL_KEY, value, i);
return null;
}
NULL_KEY的聲明:static final Object NULL_KEY = new Object();
這一段代碼是處理哈希沖突的,就是說,在數(shù)組某個(gè)位置的對(duì)象可能并不是唯一的,它是一個(gè)鏈表結(jié)構(gòu),根據(jù)哈希值找到鏈表后,還要對(duì)鏈表遍歷,找出key相等的對(duì)象,替換它,并且返回舊的值。
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
if (e.key == NULL_KEY) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
如果遍歷完了該位置的鏈表都沒有找到有key相等的,那么將當(dāng)前對(duì)象增加到鏈表里面去
modCount++;
addEntry(hash, (K) NULL_KEY, value, i);
return null;
且看看addEntry方法
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
if (size++ >= threshold)
resize(2 * table.length);
}
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);新建一個(gè)Entry對(duì)象,并放在當(dāng)前位置的Entry鏈表的頭部,看看下面的 Entry構(gòu)造函數(shù)就知道了,注意紅色部分。
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
如何擴(kuò)容?
當(dāng)put一個(gè)元素時(shí),如果達(dá)到了容量限制,HashMap就會(huì)擴(kuò)容,新的容量永遠(yuǎn)是原來的2倍。
上面的put方法里有這樣的一段:
if (size++ >= threshold)
resize(2 * table.length);
這是擴(kuò)容判斷,要注意,并不是數(shù)據(jù)尺寸達(dá)到HashMap的最大容量時(shí)才擴(kuò)容,而是達(dá)到 threshold指定的值時(shí)就開始擴(kuò)容,threshold=最大容量*加載因子。 看看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);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
重點(diǎn)看看紅色部分的 transfer方法
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
tranfer方法將所有的元素重新哈希,因?yàn)樾碌娜萘孔兇?,所以每個(gè)元素的哈希值和位置都是不一樣的。
正確的使用HashMap
1:不要在并發(fā)場(chǎng)景中使用HashMap
HashMap是線程不安全的,如果被多個(gè)線程共享的操作,將會(huì)引發(fā)不可預(yù)知的問題,據(jù)sun的說法,在擴(kuò)容時(shí),會(huì)引起鏈表的閉環(huán),在get元素時(shí),就會(huì)無限循環(huán),后果是cpu100%。
看看get方法的紅色部分
public V get(Object key) {
if (key == null)
return getForNullKey();
int hash = hash(key.hashCode());
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}
2:如果數(shù)據(jù)大小是固定的,那么最好給HashMap設(shè)定一個(gè)合理的容量值
根據(jù)上面的分析,HashMap的初始默認(rèn)容量是16,默認(rèn)加載因子是0.75,也就是說,如果采用HashMap的默認(rèn)構(gòu)造函數(shù),當(dāng)增加數(shù)據(jù)時(shí),數(shù)據(jù)實(shí)際容量超過10*0.75=12時(shí),HashMap就擴(kuò)容,擴(kuò)容帶來一系列的運(yùn)算,新建一個(gè)是原來容量2倍的數(shù)組,對(duì)原有元素全部重新哈希,如果你的數(shù)據(jù)有幾千幾萬個(gè),而用默認(rèn)的HashMap構(gòu)造函數(shù),那結(jié)果是非常悲劇的,因?yàn)镠ashMap不斷擴(kuò)容,不斷哈希,在使用HashMap的場(chǎng)景里,不會(huì)是多個(gè)線程共享一個(gè)HashMap,除非對(duì)HashMap包裝并同步,由此產(chǎn)生的內(nèi)存開銷和cpu開銷在某些情況下可能是致命的。
聯(lián)系客服