從多個角度瞭解HashMap。

轉載一個關於HashMap不錯的講解,作者通過圖形的方式很生動的把hashmap的源碼解釋出來。
http://www.cnblogs.com/ITtangtang/p/3948406.html

下面是本人對HashMap源碼的理解:
一、HashTable與HashMap的區別:

1.1 歷史

HashMap的出現時間要晚於HashTable。
HashTable在JDK1.1 版本時就已經出現,之後在JDK1.2 纔出現了HashMap。

1.2 實現的接口
這裏寫圖片描述
這裏寫圖片描述

從圖中可以看出,

  • 兩者都實現了Map、Cloneable、Seriallizable接口
  • 但繼承的抽象類是不一樣的, HashMap 繼承了AbstractMapHashTable 繼承的是已經 廢棄Dictionary
  • 從兩者提供的方法可以看出,這兩個類的功能是一樣的,都可以對鍵值對進行增刪改查,遍歷,序列化和拷貝。

1.2.1 關於空的鍵值對

//HashTable關於put的源碼及註釋

public synchronized V put(K key, V value) {

    // 如果value爲null,拋出NullPointerException
    if (value == null) {
        throw new NullPointerException();
    }

    // 如果key爲null,在調用key.hashCode()時拋出NullPointerException

    // ...
}


//HasMap關於put的源碼及註釋

public V put(K key, V value) {
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    // 當key爲null時,調用putForNullKey特殊處理
    if (key == null)
        return putForNullKey(value);
    // ...
}

private V putForNullKey(V value) {
    // key爲null時,放到table[0]也就是第0個bucket中
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    addEntry(0, null, value, 0);
    return null;
}

通過源碼我們看出, HashMap 是支持 空的(null)鍵值對 的,當遇到null時將null轉化成hashCode值0,而 HashTable 在遇到null時,會拋出 NullPointerException 異常。

1.3 實現的原理
下面我們深入到數據結構和算法層來分析一下兩者間的區別。

1.3.1 數據結構
HashTableHashMap 在數據結構上兩者是相同的,都使用哈希表來存儲鍵值對,繼承自Map.Entry中的Entry類,每一個Entry對象對應哈希表中的一個鍵值對。

散列表(Hash table,也叫哈希表),是根據關鍵碼值(Key value)而直接進行訪問的數據結構。也就是說,它通過把關鍵碼值映射到表中一個位置來訪問記錄,以加快查找的速度。這個映射函數叫做散列函數,存放記錄的數組叫做散列表。
給定表M,存在函數f(key),對任意給定的關鍵字值key,代入函數後若能得到包含該關鍵字的記錄在表中的地址,則稱表M爲哈希(Hash)表,函數f(key)爲哈希(Hash) 函數。

//Map.Entry源碼
interface Entry<K,V> {
        K getKey();

        V getValue();

        V setValue(V value);

        boolean equals(Object o);

        int hashCode();

        public static <K extends Comparable<? super K>, V> Comparator<Map.Entry<K,V>> comparingByKey() {
            return (Comparator<Map.Entry<K, V>> & Serializable)
                (c1, c2) -> c1.getKey().compareTo(c2.getKey());
        }

        public static <K, V extends Comparable<? super V>> Comparator<Map.Entry<K,V>> comparingByValue() {
            return (Comparator<Map.Entry<K, V>> & Serializable)
                (c1, c2) -> c1.getValue().compareTo(c2.getValue());
        }

        public static <K, V> Comparator<Map.Entry<K, V>> comparingByKey(Comparator<? super K> cmp) {
            Objects.requireNonNull(cmp);
            return (Comparator<Map.Entry<K, V>> & Serializable)
                (c1, c2) -> cmp.compare(c1.getKey(), c2.getKey());
        }

        public static <K, V> Comparator<Map.Entry<K, V>> comparingByValue(Comparator<? super V> cmp) {
            Objects.requireNonNull(cmp);
            return (Comparator<Map.Entry<K, V>> & Serializable)
                (c1, c2) -> cmp.compare(c1.getValue(), c2.getValue());
        }
    }

可以說有多少的 鍵值對 就有多少的 Entry對象
下圖說明HashTableHashMap 如何存儲鍵值對。
這裏寫圖片描述
上圖可以看出,哈希值相等的話,entry對象就以鏈表(HashMap在JDK1.8之後優化爲紅黑樹)的形式進行存儲。

//HashTable
private transient Entry<K,V>[] table;
//HashMap
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

得出結論HashTableHashMap 內部使用 Entry數組 進行 鍵值對 的存儲。

1.3.2 算法
下面一部分我們研究一下 HashTableHashMap 在底層初始化和將給定的key映射到對應hash值上的算法,從而發現兩者之間的異同。

初始化與擴容:

// HashTable
// 哈希表默認初始大小爲11
public Hashtable() {
    this(11, 0.75f);
}

protected void rehash() {
    int oldCapacity = table.length;
    Entry<K,V>[] oldMap = table;

    // 每次擴容爲原來的2n+1
    int newCapacity = (oldCapacity << 1) + 1;
    // ...
}

// HashMap
// 哈希表默認初始大小爲2^4=16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

void addEntry(int hash, K key, V value, int bucketIndex) {
    // 每次擴充爲原來的2n 
    if ((size >= threshold) && (null != table[bucketIndex])) {
       resize(2 * table.length);
}

從源碼我們發現,HashTable 每次初始化爲11,每次擴容爲2n+1。而 HashMap 初始化爲16,每次擴容爲原來的2倍。還有一點就是,HashTable 如果你給定大小,那麼其初始化時將按照你 給定的大小 來確定容量,但是HashMap 的大小爲你給定大小的 2的冪次方
在效率上HashMap 的方式直接使用位運算來得到結果更高效,但會因此造成hash表分佈的不均勻,

如何定位到 hash桶:

// HashTable
// hash 不能超過Integer.MAX_VALUE 所以要取其最小的31個bit
int hash = hash(key);
int index = (hash & 0x7FFFFFFF) % tab.length;

// 直接計算key.hashCode()
private int hash(Object k) {
    // hashSeed will be zero if alternative hashing is disabled.
    return hashSeed ^ k.hashCode();
}

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

// 在計算了key.hashCode()之後,做了一些位運算來減少哈希衝突
final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }

    h ^= k.hashCode();

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

// 取模不再需要做除法
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);
}

1.4 線程安全

//HashTable
public synchronized V get(Object key) {
    Entry tab[] = table;
    int hash = hash(key);
    int index = (hash & 0x7FFFFFFF) % tab.length;
    for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
        if ((e.hash == hash) && e.key.equals(key)) {
            return e.value;
        }
    }
    return null;
}

public Set<K> keySet() {
    if (keySet == null)
        keySet = Collections.synchronizedSet(new KeySet(), this);
    return keySet;
}

通過上面關於 HashTable 的源碼我們發現,HashTable公開的方法,比如get都使用了 synchronized 描述符。而遍歷視圖,比如keySet都使用了Collections.synchronizedXXX 進行了同步包裝。在多線程狀態下是同步的,而 HashMap 是不同步的。

1.5 HashTable已經被淘汰

以下描述來自於HashTable的類註釋:

If a thread-safe implementation is not needed, it is recommended to
use HashMap in place of Hashtable. If a thread-safe highly-concurrent
implementation is desired, then it is recommended to use
java.util.concurrent.ConcurrentHashMap in place of Hashtable

在不考慮線程安全的情況下,就使用HashMap,反之使用ConcurrentHashMap。HashTable已經被淘汰了,不要在新的代碼中再使用它。

二、ConcurrentHashMap與HashMap的區別:

2.1 ConcurrentHashMap

對於一般的增刪改查,ConcurrentHashMap 的設計思路和 HashMap 是基本相同的,只是,ConcurrentHashMap 由一個個 segment(部分/一段) 也可以叫做 分段鎖 。可以這麼理解,ConcurrentHashMap 由一個 segment數組 組成,每一個 segment 繼承了 ReentrantLock鎖 ,這樣可以確保每一個 segment 都是加鎖的,以保證線程安全。

這裏寫圖片描述

2.2 ConcurrentHashMap的初始化

ConcurrentHashMap 在默認初始化的時候就擁有有 16 個 Segments,也就是說默認情況下,支持最多16 個線程併發寫入,如果初始化設置爲其他值之後,便不可以擴容了。

//initialCapacity:初始容量,這個值指的是整個 ConcurrentHashMap 
//的初始容量,實際操作的時候需要平均分給每個 Segment。

//loadFactor:負載因子,由於Segment 數組不可以擴容,
//所以這個負載因子是給每個 Segment 內部使用的。

public ConcurrentHashMap(int initialCapacity,
                         float loadFactor, int concurrencyLevel) {
    if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
        throw new IllegalArgumentException();
    if (concurrencyLevel > MAX_SEGMENTS)
        concurrencyLevel = MAX_SEGMENTS;
    // Find power-of-two sizes best matching arguments
    int sshift = 0;
    int ssize = 1;
    // 計算並行級別 ssize,因爲要保持並行級別是 2 的 n 次方
    while (ssize < concurrencyLevel) {
        ++sshift;
        ssize <<= 1;
    }
    // 我們這裏先不要那麼燒腦,用默認值,concurrencyLevel 爲 16,sshift 爲 4
    // 那麼計算出 segmentShift 爲 28,segmentMask 爲 15,後面會用到這兩個值
    this.segmentShift = 32 - sshift;
    this.segmentMask = ssize - 1;

    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;

    // initialCapacity 是設置整個 map 初始的大小,
    // 這裏根據 initialCapacity 計算 Segment 數組中每個位置可以分到的大小
    // 如 initialCapacity 爲 64,那麼每個 Segment 或稱之爲"槽"可以分到 4 個
    int c = initialCapacity / ssize;
    if (c * ssize < initialCapacity)
        ++c;
    // 默認 MIN_SEGMENT_TABLE_CAPACITY 是 2,這個值也是有講究的,因爲這樣的話,對於具體的槽上,
    // 插入一個元素不至於擴容,插入第二個的時候纔會擴容
    int cap = MIN_SEGMENT_TABLE_CAPACITY; 
    while (cap < c)
        cap <<= 1;

    // 創建 Segment 數組,
    // 並創建數組的第一個元素 segment[0]
    Segment<K,V> s0 =
        new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                         (HashEntry<K,V>[])new HashEntry[cap]);
    Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
    // 往數組寫入 segment[0]
    UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
    this.segments = ss;
}

這樣 ConcurrentHashMap 便算是完成了初始化。

2.3 ConcurrentHashMap 和 HashMap 的 put 和 get 方法對比

2.3.1 ConcurrentHashMap的put方法過程

  • put的第一步:
public V put(K key, V value) {
    Segment<K,V> s;
    if (value == null)
        throw new NullPointerException();
    // 1. 計算 key 的 hash 值
    int hash = hash(key);
    // 2. 根據 hash 值找到 Segment 數組中的位置 j
    //    hash 是 32 位,無符號右移 segmentShift(28) 位,剩下低 4 位,
    //    然後和 segmentMask(15) 做一次與操作,
    //    也就是說 j 是 hash 值的最後 4 位,也就是槽的數組下標
    int j = (hash >>> segmentShift) & segmentMask;
    // 剛剛說了,初始化的時候初始化了 segment[0],但是其他位置還是 null,
    // ensureSegment(j) 對 segment[j] 進行初始化
    if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
         (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
        s = ensureSegment(j);
    // 3. 插入新值到 槽 s 中
    return s.put(key, hash, value, false);
}
  • Segment內部初始化,由數組+鏈表組成
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
    // 在往該 segment 寫入前,需要先獲取該 segment 的獨佔鎖
    //    先看主流程,後面還會具體介紹這部分內容
    HashEntry<K,V> node = tryLock() ? null :
        scanAndLockForPut(key, hash, value);
    V oldValue;
    try {
        // 這個是 segment 內部的數組
        HashEntry<K,V>[] tab = table;
        // 再利用 hash 值,求應該放置的數組下標
        int index = (tab.length - 1) & hash;
        // first 是數組該位置處的鏈表的表頭
        HashEntry<K,V> first = entryAt(tab, index);

        // 下面這串 for 循環雖然很長,不過也很好理解,想想該位置沒有任何元素和已經存在一個鏈表這兩種情況
        for (HashEntry<K,V> e = first;;) {
            if (e != null) {
                K k;
                if ((k = e.key) == key ||
                    (e.hash == hash && key.equals(k))) {
                    oldValue = e.value;
                    if (!onlyIfAbsent) {
                        // 覆蓋舊值
                        e.value = value;
                        ++modCount;
                    }
                    break;
                }
                // 繼續順着鏈表走
                e = e.next;
            }
            else {
                // node 到底是不是 null,這個要看獲取鎖的過程,不過和這裏都沒有關係。
                // 如果不爲 null,那就直接將它設置爲鏈表表頭;如果是null,初始化並設置爲鏈表表頭。
                if (node != null)
                    node.setNext(first);
                else
                    node = new HashEntry<K,V>(hash, key, value, first);

                int c = count + 1;
                // 如果超過了該 segment 的閾值,這個 segment 需要擴容
                if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                    rehash(node); // 擴容後面也會具體分析
                else
                    // 沒有達到閾值,將 node 放到數組 tab 的 index 位置,
                    // 其實就是將新的節點設置成原鏈表的表頭
                    setEntryAt(tab, index, node);
                ++modCount;
                count = c;
                oldValue = null;
                break;
            }
        }
    } finally {
        // 解鎖
        unlock();
    }
    return oldValue;
}
  • ensureSegment:初始化segment
private Segment<K,V> ensureSegment(int k) {
    final Segment<K,V>[] ss = this.segments;
    long u = (k << SSHIFT) + SBASE; // raw offset
    Segment<K,V> seg;
    if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
        // 這裏看到爲什麼之前要初始化 segment[0] 了,
        // 使用當前 segment[0] 處的數組長度和負載因子來初始化 segment[k]
        // 爲什麼要用“當前”,因爲 segment[0] 可能早就擴容過了
        Segment<K,V> proto = ss[0];
        int cap = proto.table.length;
        float lf = proto.loadFactor;
        int threshold = (int)(cap * lf);

        // 初始化 segment[k] 內部的數組
        HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
        if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
            == null) { // 再次檢查一遍該槽是否被其他線程初始化了。

            Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
            // 使用 while 循環,內部用 CAS,當前線程成功設值或其他線程成功設值後,退出
            while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                   == null) {
                if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
                    break;
            }
        }
    }
    return seg;
}
  • scanAndLockForPut:獲取寫入鎖

前面 segmentput 方法中,第一步就調用
node = tryLock() ? null : scanAndLockForPut(key, hash, value)
也就是說先進行一次 tryLock() 快速獲取該 segment 的獨佔鎖,如果失敗,那麼進入到 scanAndLockForPut 這個方法來獲取鎖。

scanAndLockForPut如何加鎖

private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
    HashEntry<K,V> first = entryForHash(this, hash);
    HashEntry<K,V> e = first;
    HashEntry<K,V> node = null;
    int retries = -1; // negative while locating node

    // 循環獲取鎖
    while (!tryLock()) {
        HashEntry<K,V> f; // to recheck first below
        if (retries < 0) {
            if (e == null) {
                if (node == null) // speculatively create node
                    // 進到這裏說明數組該位置的鏈表是空的,沒有任何元素
                    // 當然,進到這裏的另一個原因是 tryLock() 失敗,所以該槽存在併發,不一定是該位置
                    node = new HashEntry<K,V>(hash, key, value, null);
                retries = 0;
            }
            else if (key.equals(e.key))
                retries = 0;
            else
                // 順着鏈表往下走
                e = e.next;
        }
        // 重試次數如果超過 MAX_SCAN_RETRIES(單核1多核64),那麼不搶了,進入到阻塞隊列等待鎖
        //    lock() 是阻塞方法,直到獲取鎖後返回
        else if (++retries > MAX_SCAN_RETRIES) {
            lock();
            break;
        }
        else if ((retries & 1) == 0 &&
                 // 這個時候是有大問題了,那就是有新的元素進到了鏈表,成爲了新的表頭
                 //     所以這邊的策略是,相當於重新走一遍這個 scanAndLockForPut 方法
                 (f = entryForHash(this, hash)) != first) {
            e = first = f; // re-traverse if entry changed
            retries = -1;
        }
    }
    return node;
}

這個方法有兩個出口,一個是 tryLock() 成功了,循環終止,另一個就是重試次數超過了 MAX_SCAN_RETRIES,進到 lock() 方法,此方法會阻塞等待,直到成功拿到獨佔鎖。

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