輕鬆理解 Java HashMap 和 ConcurrentHashMap

前言

Map 這樣的Key Value在軟件開發中是非常經典的結構,常用於在內存中存放數據。

本篇主要想討論 ConcurrentHashMap 這樣一個併發容器,在正式開始之前我覺得有必要談談 HashMap,沒有它就不會有後面的 ConcurrentHashMap。

HashMap

衆所周知 HashMap 底層是基於數組 + 鏈表組成的,不過在 jdk1.7 和 1.8 中具體實現稍有不同。

Base 1.7

1.7 中的數據結構圖:

先來看看 1.7 中的實現。

這是 HashMap 中比較核心的幾個成員變量;看看分別是什麼意思?

初始化桶大小,因爲底層是數組,所以這是數組默認的大小。

桶最大值。

默認的負載因子(0.75)

table真正存放數據的數組。

Map存放數量的大小。

桶大小,可在初始化時顯式指定。

負載因子,可在初始化時顯式指定。

重點解釋下負載因子:

由於給定的 HashMap 的容量大小是固定的,比如默認初始化:

public HashMap() {

    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);

}

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

    this.loadFactor = loadFactor;

    threshold = initialCapacity;

    init();

}

給定的默認容量爲 16,負載因子爲 0.75。Map 在使用過程中不斷的往裏面存放數據,當數量達到了16 * 0.75 = 12就需要將當前 16 的容量進行擴容,而擴容這個過程涉及到 rehash、複製數據等操作,所以非常消耗性能。

因此通常建議能提前預估 HashMap 的大小最好,儘量的減少擴容帶來的性能損耗。

根據代碼可以看到其實真正存放數據的是

transient Entry[] table = (Entry[]) EMPTY_TABLE;

這個數組,那麼它又是如何定義的呢?

Entry 是 HashMap 中的一個內部類,從他的成員變量很容易看出:

key 就是寫入時的鍵。

value 自然就是值。

開始的時候就提到 HashMap 是由數組和鏈表組成,所以這個 next 就是用於實現鏈表結構。

hash 存放的是當前 key 的 hashcode。

知曉了基本結構,那來看看其中重要的寫入、獲取函數:

put 方法

public V put(K key, V value) {

    if (table == EMPTY_TABLE) {

        inflateTable(threshold);

    }

    if (key == null)

        return putForNullKey(value);

    int hash = hash(key);

    int i = indexFor(hash, table.length);

    for (Entry 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 爲空,則 put 一個空值進去。

根據 key 計算出 hashcode。

根據計算出的 hashcode 定位出所在桶。

如果桶是一個鏈表則需要遍歷判斷裏面的 hashcode、key 是否和傳入 key 相等,如果相等則進行覆蓋,並返回原來的值。

如果桶是空的,說明當前位置沒有數據存入;新增一個 Entry 對象寫入當前位置。

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

}

void createEntry(int hash, K key, V value, int bucketIndex) {

    Entry e = table[bucketIndex];

    table[bucketIndex] = new Entry<>(hash, key, value, e);

    size++;

}

當調用 addEntry 寫入 Entry 時需要判斷是否需要擴容。

如果需要就進行兩倍擴充,並將當前的 key 重新 hash 並定位。

而在createEntry中會將當前位置的桶傳入到新建的桶中,如果當前桶有值就會在位置形成鏈表。

get 方法

再來看看 get 函數:

public V get(Object key) {

    if (key == null)

        return getForNullKey();

    Entry entry = getEntry(key);

    return null == entry ? null : entry.getValue();

}

final Entry getEntry(Object key) {

    if (size == 0) {

        return null;

    }

    int hash = (key == null) ? 0 : hash(key);

    for (Entry e = table[indexFor(hash, table.length)];

         e != null;

         e = e.next) {

        Object k;

        if (e.hash == hash &&

            ((k = e.key) == key || (key != null && key.equals(k))))

            return e;

    }

    return null;

}

首先也是根據 key 計算出 hashcode,然後定位到具體的桶中。

判斷該位置是否爲鏈表。

不是鏈表就根據key、key 的 hashcode是否相等來返回值。

爲鏈表則需要遍歷直到 key 及 hashcode 相等時候就返回值。

啥都沒取到就直接返回 null 。



作者:AI喬治
鏈接:https://www.jianshu.com/p/7e36a15f7d3a
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯繫作者獲得授權並註明出處。


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