java.util.HashMap是很常見的類,前段時間公司系統因爲對HashMap使用不當,致使cpu百分之百,在併發環境下使用HashMap而沒有作同步,可能會引發死循環,關於這一點,sun的官方網站上已有闡述,這並不是是bug。java
HashMap的數據結構
HashMap主要是用數組來存儲數據的,咱們都知道它會對key進行哈希運算,哈系運算會有重複的哈希值,對於哈希值的衝突,HashMap採用鏈表來解決的。在HashMap裏有這樣的一句屬性聲明:
transient Entry[] table;
Entry就是HashMap存儲的數據,它擁有的屬性以下web
1 2 3 4 |
final K key; V value; final int hash; Entry<K,V> next; |
看到next了嗎?next就是爲了哈希衝突而存在的。好比經過哈希運算,一個新元素應該在數組的第10個位置,可是第10個位置已經有Entry,那麼好吧,將新加的元素也放到第10個位置,將第10個位置的原有Entry賦值給當前新加的Entry的next屬性。數組存儲的鏈表,鏈表是爲了解決哈希衝突的,這一點要注意。
幾個關鍵的屬性
存儲數據的數組
transient Entry[] table; 這個上面已經講到了
默認容量數組
1 |
static final int DEFAULT_INITIAL_CAPACITY = 16; |
最大容量安全
1 |
static final int MAXIMUM_CAPACITY = 1 << 30; |
默認加載因子,加載因子是一個比例,當HashMap的數據大小>=容量*加載因子時,HashMap會將容量擴容數據結構
1 |
static final float DEFAULT_LOAD_FACTOR = 0.75f; |
當實際數據大小超過threshold時,HashMap會將容量擴容,threshold=容量*加載因子併發
1 |
int threshold; |
加載因子app
1 |
final float loadFactor; |
HashMap的初始過程
構造函數1函數
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
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(); } |
重點注意這裏網站
1 2 |
while (capacity < initialCapacity) capacity <<= 1; |
capacity纔是初始容量,而不是initialCapacity,這個要特別注意,若是執行new HashMap(9,0.75);那麼HashMap的初始容量是16,而不是9,想一想爲何吧。this
構造函數2
1 2 3 |
public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } |
構造函數3,所有都是默認值
1 2 3 4 5 6 |
public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR); table = new Entry[DEFAULT_INITIAL_CAPACITY]; init(); } |
構造函數4
1 2 3 4 5 |
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做一些運算以獲得最終的哈希值,而且獲得的哈希值也不是在數組中的位置哦,不管是get仍是put仍是別的方法,計算哈希值都是這一句:
1 |
int hash = hash(key.hashCode()); |
hash函數以下:
1 2 3 |
static int hash(int h) { return useNewHash ? newHash(h) : oldHash(h); } |
useNewHash聲明以下:
1 2 |
private static final boolean useNewHash; static { useNewHash = false; } |
這說明useNewHash其實一直爲false且不可改變的,hash函數裏對useNewHash的判斷真是多餘的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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); } |
其實HashMap的哈希函數會一直都是oldHash。
若是肯定數據的位置
看下面兩行
1 2 |
int hash = hash(k.hashCode());int i = indexFor(hash, table.length); |
第一行,上面講過了,是獲得哈希值,第二行,則是根據哈希指計算元素在數組中的位置了,位置的計算是將哈希值和數組長度按位與運算。
1 2 3 |
static int indexFor(int h, int length) { return h & (length-1); } |
put方法到底做了什麼?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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的聲明:
1 |
static final Object NULL_KEY = new Object(); |
這一段代碼是處理哈希衝突的,就是說,在數組某個位置的對象可能並非惟一的,它是一個鏈表結構,根據哈希值找到鏈表後,還要對鏈表遍歷,找出key相等的對象,替換它,而且返回舊的值。
1 2 3 4 5 6 7 8 |
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相等的,那麼將當前對象增長到鏈表裏面去
1 2 3 |
modCount++; addEntry(hash, (K) NULL_KEY, value, i); return null; |
且看看addEntry方法
1 2 3 4 5 6 |
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(hash, key, value, e);新建一個Entry對象,並放在當前位置的Entry鏈表的頭部,看看下面的 Entry構造函數就知道了,注意紅色部分。
1 2 3 4 5 6 |
Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; hash = h; } |
如何擴容?
當put一個元素時,若是達到了容量限制,HashMap就會擴容,新的容量永遠是原來的2倍。
上面的put方法裏有這樣的一段:
1 2 |
if (size++ >= threshold) resize(2 * table.length); |
這是擴容判斷,要注意,並非數據尺寸達到HashMap的最大容量時才擴容,而是達到 threshold指定的值時就開始擴容, threshold=最大容量*加載因子。 看看resize方法
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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); } |
重點看看紅色部分的 transfer方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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方法將全部的元素從新哈希,由於新的容量變大,因此每一個元素的哈希值和位置都是不同的。
正確的使用HashMap
1:不要在併發場景中使用HashMap
HashMap是線程不安全的,若是被多個線程共享的操做,將會引起不可預知的問題,據sun的說法,在擴容時,會引發鏈表的閉環,在get元素時,就會無限循環,後果是cpu100%。
看看get方法的紅色部分
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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設定一個合理的容量值
根據上面的分析,HashMap的初始默認容量是16,默認加載因子是0.75,也就是說,若是採用HashMap的默認構造函數,當增長數據時,數據實際容量超過10*0.75=12時,HashMap就擴容,擴容帶來一系列的運算,新建一個是原來容量2倍的數組,對原有元素所有從新哈希,若是你的數據有幾千幾萬個,而用默認的HashMap構造函數,那結果是很是悲劇的,由於HashMap不斷擴容,不斷哈希,在使用HashMap的場景裏,不會是多個線程共享一個HashMap,除非對HashMap包裝並同步,由此產生的內存開銷和cpu開銷在某些狀況下多是致命的。