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

Java7、8中HashMap和ConcurrentHashMap源碼閱讀

動態擴容 nal das pub end flat 數據 算數 ext

首先來看下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
技術分享圖片

從上面的結構圖中,可以大致看出,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結構如下:
技術分享圖片

    ConcurrentHashMap是並發版的HashMap,支持復雜的並發操作,通過降低鎖的粒度和cas等實現了高並發,支持原子條件的更新操作,不會拋出ConcurrentModificationException,實現了弱一致性。
    ConCurrentHashMap是一個Segment數組,每個segment元素對應一個哈希表(結構類似於HashMap)

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

Java7、8中HashMap和ConcurrentHashMap源碼閱讀