1. 程式人生 > >Java7、8中HashMap和ConcurrentHashMap原始碼閱讀

Java7、8中HashMap和ConcurrentHashMap原始碼閱讀

首先來看下HashMap的類繼承結構:

public class HashMap extends AbstractMap<K,V> impement Map<K,V>,Coloneable,Serializable{

 }

可以看出HashMap實現了Map介面。其裡面的方法都是非執行緒安全的,且不支援併發操作。
對於HashMap主要看的是get/put方法實現,其在jdk1.7,及1.8在解決雜湊衝突的上有所不同。
一、Java7 HashMap
Java7、8中HashMap和ConcurrentHashMap原始碼閱讀

從上面的結構圖中,可以大致看出,HashMap由陣列:transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;沒個元素對應為一個單向連結串列,連結串列資料結構如下:
    static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    Entry<K,V> next;
    int hash; 
        }

在HashMap中定義的成員變數:
capacity:當前陣列容量,始終保持 2^n,可以擴容,擴容後陣列大小為當前的 2 倍。
loadFactor:負載因子,預設為 0.75。
threshold:擴容的閾值,等於 capacity * loadFactor,當容量超過這個值時,陣列將擴容。
transient int modCount; //HashMap修改次數,這個值用於和expectedModCount期望修改次數比較。

1、put方法解析:
public V put(K key, V value) {
//1.當插入第一個元素時,需要建立並初始化指定大小的陣列
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}

            //2.如果 key 為 null,迴圈遍歷table[0]上的連結串列,最終會將這個 entry 放到 table[0] 中
    if (key == null)
        return putForNullKey(value);
            //3.計算key的雜湊值
    int hash = hash(key);
            //4、通過h & (length-1)即h%length求模找到鍵值對放在哪個位置。
    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))) {//hash值為整數,比較效能比equals高;另外短路運算,雜湊值系統了就沒必要在比較equals。
            V oldValue = e.value;//先將當前節點的鍵對應的值取出來。
            e.value = value; //替換為新值。
            e.recordAccess(this);
            return oldValue;
        }
    }

    modCount++; //容器修改次數加1
    addEntry(hash, key, value, i); //在指定的位置上新增資料,若空間不夠則動態擴充,當前容量乘以2,新建一個數組,長度為capacity*2;並將原來的陣列拷貝過來,更新對應變數。
    return null;
}

    陣列初始化:
        private void inflateTable(int toSize) {
    // Find a power of 2 >= toSize
    int capacity = roundUpToPowerOf2(toSize); //指定陣列容量,預設為16

    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    table = new Entry[capacity]; //改變陣列的引用,指向新建立的陣列
    initHashSeedAsNeeded(capacity);
}

    計算鍵值對的位置:
        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); //等同於求模:h%length
}

    新增節點到連結串列中
        void addEntry(int hash, K key, V value, int bucketIndex) {
            //假如map的元素個數大於等於閾值,並且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);
}
   //新建一個Entry物件,插入單向連結串列表頭,並增加size(不論是否擴容這一步都要進行)
   void createEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    size++;
}

    陣列擴容:
        void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }

            //新建一個容量擴充2倍的陣列
    Entry[] newTable = new Entry[newCapacity];
            //呼叫transfer方法將舊陣列中的鍵值對拷貝過來
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
            //舊陣列原來的堆空間設定為引用切斷,指向新陣列
    table = newTable;
            //重新計算閾值
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

    鍵值對移植:
    /**
 * Transfers all entries from current table to newTable.
 */
void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {
        while(null != e) {
            Entry<K,V> next = e.next;
                            //是否重新計算key的雜湊
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
                            //重新計算元素位置
            int i = indexFor(e.hash, newCapacity);
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}

    以上就是儲存鍵值對的主要程式碼,基本步驟:
    1)、計算key的雜湊值;
    2)、根據雜湊值計算陣列元素的儲存位置(h&(length-1)或h%length);
    3)、根據需要擴充陣列大小;
    4)、將鍵值對插入到對應的連結串列頭部或更新已有值;

    2、get方法解析
        public V get(Object key) {
            //如果key為空則直接,在存放元素時是直接存放到table[0],所以直接呼叫getForNullKey方法遍歷對應連結串列即可。
    if (key == null)
        return getForNullKey();
    Entry<K,V> entry = getEntry(key);

    return null == entry ? null : entry.getValue();
}
    遍歷table[0]位置的連結串列,返回對應key==null的值,若果返回null,則有兩種情況,要麼沒有key==null的鍵值對,要麼對應位置上的值為null。
private V getForNullKey() {
    if (size == 0) {
        return null;
    }
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null)
            return e.value;
    }
    return null;
}

    key值不為空,則呼叫返回對應的值:
        final Entry<K,V> getEntry(Object key) {
    if (size == 0) {
        return null;
    }

    int hash = (key == null) ? 0 : hash(key);
    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 != null && key.equals(k))))
            return e;
    }
    return null;
}

    總結基本流程:
    1、計算鍵的雜湊值;
    2、根據雜湊值找到陣列中對於的連結串列;
    3、遍歷連結串列,查詢對應key的值;
    4、在比較查詢的過程中,先快速比較雜湊值,hash相同則再繼續通過equals比較;

二、java7 ConcurrentHashMap
在java7 下ConcurrentHashMap結構如下:
Java7、8中HashMap和ConcurrentHashMap原始碼閱讀

    ConcurrentHashMap是併發版的HashMap,支援複雜的併發操作,通過降低鎖的粒度和cas等實現了高併發,支援原子條件的更新操作,不會丟擲ConcurrentModificationException,實現了弱一致性。
    ConCurrentHashMap是一個Segment陣列,每個segment元素對應一個雜湊表(結構類似於HashMap)

    未完待續....(ConcurrentHashMap沒太讀明白)