HashMap內部結構深刻剖析

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開銷在某些狀況下多是致命的。

相關文章
相關標籤/搜索