jdk7和jdk8的實現是不一樣的,jdk7採用數組+鏈表實現,jdk8採用數組+鏈表+紅黑樹實現。
HashMap線程不安全,有線程安全需求的要用ConcurrentHashMap替代。
HashMap允許key爲null,不允許key重複。
HashMap併發下put()會導致死鏈,導致cpu打滿的原因不是死鏈的形成,而是查詢時死鏈會導致無限循環。
jdk7版
主要成員變量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默認初始化大小 16
static final float DEFAULT_LOAD_FACTOR = 0.75f; // 負載因子0.75
static final Entry<?,?>[] EMPTY_TABLE = {}; // 初始化的默認數組
transient int size; // HashMap中元素的數量
int threshold; // 域值,每次擴容要重新計算下,用來判斷是否需要調整HashMap的容量
Entry類型定義
// 在HashMap裏的靜態內部類 ,Entry用來存儲鍵值對,HashMap中的Entry[]用來存儲entry
static class Entry<K,V> implements Map.Entry<K,V> {
final K key; //鍵
V value; //值
Entry<K,V> next; //採用鏈表存儲HashCode相同的鍵值對,next指向下一個entry
int hash; //entry的hash值
//構造方法, 負責初始化entry
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
public final K getKey() {
return key;
}
public final V getValue() {
return value;
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
Object k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
}
public final int hashCode() {
return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
}
public final String toString() {
return getKey() + "=" + getValue();
}
//當使用相同的key的value被覆蓋時調用
void recordAccess(HashMap<K,V> m) {
}
//每移除一個entry就被調用一次
void recordRemoval(HashMap<K,V> m) {
}
}
到這裏就可以給出HashMap的抽象結構圖了:
關於這種結構,我們還要了解的是存儲對象是如何分佈到數組和鏈表上的,數組是如何擴容的,搞完這兩點,基本就沒啥問題了。
以上這兩個關鍵點,我們應該能從put()函數中得到解答
// 向map中添加key-value 鍵值對,如果可以包含了key的映射,則舊的value將被替換
public V put(K key, V value) {
if (table == EMPTY_TABLE) { // table如果爲空,進行初始化操作
inflateTable(threshold);
}
if (key == null) // key 爲null ,放入數組的0號索引位置
return putForNullKey(value);
int hash = hash(key); // 計算key的hash值
int i = indexFor(hash, table.length); // 計算key在entry數組中存儲的位置
// 判斷該位置是否已經有元素存在
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
// 判斷key是否已經在map中存在,若存在用新的value替換掉舊的value,並返回舊的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++; // 修改次數加1
addEntry(hash, key, value, i); // 將key-value轉化爲Entry,添加到Map中,擴容操作在裏面
return null;
}
// 擴充表,HashMap初始化時是一個空數組,此方法創建一個新的Entry[]
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize,power是冪的意思
int capacity = roundUpToPowerOf2(toSize); // capacity爲2的冪數,大於等於toSize
// 計算閾值,用來確定需不需要擴容
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity]; // 新建數組,並重新賦值
initHashSeedAsNeeded(capacity); // 修改hashSeed
}
// 根據hashcode,和表的長度,返回存放的索引,按位與的效果
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);
}
// 添加實體,bucketIndex是數組下標,關鍵點是擴容
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);
}
// 擴容操作,併發下可能形成死鏈,打滿cpu
void resize(int newCapacity) {
Entry[] oldTable = table; // 將table賦值給新的引用
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
// 創建一個長度爲newCapacity的數組
Entry[] newTable = new Entry[newCapacity];
// 將table中的元素複製到newTable中
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
// 更改閾值
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
// 將table中的數據複製到newTable中,死鍊形成
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
// 遍歷原table
for (Entry<K,V> e : table) {
while(null != e) {
// 遍歷桶(鏈表)
Entry<K,V> next = e.next; // 這個和下面的e = next配合形成一個循環
if (rehash) { // 是否需要重新計算Hash值
e.hash = null == e.key ? 0 : hash(e.key);
}
// 重新確定所屬數組角標
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i]; // 讓被遷移的e指向頭,首次爲null
newTable[i] = e; // 替換頭,e是共享變量
e = next; // 迭代下一個
}
}
}
遷移鏈表時併發下是如何產生死鏈的?如果多個線程同時進行擴容,肯定會形成多個新的數組+鏈表結構,但是遷移的數據還是原來的,在堆上並沒有變化,只是被多個新的Hash結構引用了,假設線程1剛執行了next=e.next,就被掛起,線程2開始遷移並完成一個桶的遷移,然後線程1被喚醒,
此時線程1看到的結構如下,其實已經被遷移了,但是它還是會操作
這裏我們看到,經過3輪循環後,遷移結束了,但是環形鏈表形成了,下次查找時就會形成死循環,導致cpu某一個核心被打滿,其中也能看到,併發resize()也可能會導致數據丟失,總之就是結果完全無法預知。
jdk8版本
jdk8中HashMap引入了紅黑樹,原因是碰撞頻繁使,鏈表長度過長導致查詢變慢,所以jdk8中,當鏈表的長度達到一定值(默認是8)時,將鏈表轉換成紅黑樹(時間複雜度爲O(lg n)),極大的提高了查詢效率。
主要成員變量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
static final int TREEIFY_THRESHOLD = 8;
transient Node<K,V>[] table;
transient int size;
可以看到,數組結構的類型已經變成了Node<>類型。Node<>結構定義如下:
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
看看jdk8中HashMap大致結構:
再來看看擴容是怎麼做的:
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
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) {
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;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
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;
}