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