Java HashMap原理

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;
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章