1. 程式人生 > 實用技巧 >探索WeakHashMap底層實現

探索WeakHashMap底層實現

前言

探索WeakHashMap底層實現是基於JDK1.8,它的資料結構是陣列 + 連結串列。就不貼它的註釋了,直接總結一下吧:

WeakHashMap基於弱鍵實現了Map介面,也就是說,當某個鍵不在使用時會被丟棄,對應的鍵值對將會被自動移除。如何確定不在使用取決於GC是否執行,而對於GC何時執行我們並不知道,所以某個鍵何時被丟棄我們也不得而知,至於GC如何執行就是另外一個話題了,有可能導致上一分鐘與下一分鐘獲取到的結果是不一致的。另一個方面,WeakHashMap的值物件由強引用所持有(何為強引用下面會介紹),應確保值物件不會直接或間接引用自身的鍵或其他鍵,這會導致鍵無法被丟棄。

  • 強引用:簡單來說指向new出來的物件就是一個強引用,可以說是經常使用。對於強引用來說,它們不會被GC回收,即使記憶體空間不足,JVM寧願丟擲記憶體溢位錯誤也不敢動它們,總體來說還是很有威信的。

  • 軟引用:首先給強引用包裹上一層SoftReference,通過SoftReference獲取到的引用即為軟引用。對於軟引用來說,在記憶體充足的情況下,GC可以選擇性的清除,而一旦記憶體不足了,它們一個都跑不了,都會被清除掉。軟引用最常用用於實現對記憶體敏感的快取。

  • 弱引用:首先給強引用包裹上一層WeakReference,通過WeakReference獲取到的引用即為弱引用,看到這裡你應該就已經明白了WeakHashMap內部的機制。對於弱引用來說,GC壓根就不管記憶體是否充足,直接回收,很沒有人性!

  • 虛引用:首先給強引用包裹上一層PhantomReference,通過PhantomReference獲取到的引用即為虛引用。對於虛引用來說,它在任何時候都可能被回收,常用於跟蹤物件。

還有一個方面,讀者最好去了解下Reference類,內部通過佇列實現了一些機制。

資料結構

前奏都準備好了,開始進入正題吧。


    public class WeakHashMap<K,V> extends AbstractMap<K,V> implements Map<K,V> {

        /**
         * 預設初始容量,必須是2的冪次方,可參考HashMap
         */
        private static final int DEFAULT_INITIAL_CAPACITY = 16;

        /**
         * 最大容量,必須是2的冪次方
         */
        private static final int MAXIMUM_CAPACITY = 1 << 30;

        /**
         * 預設載入因子
         */
        private static final float DEFAULT_LOAD_FACTOR = 0.75f;

        /**
         * 雜湊表,長度必須是2的冪次方
         */
        Entry<K,V>[] table;

        /**
         * 雜湊表中包含節點的個數
         */
        private int size;

        /**
         * 擴容前需要判斷的閾值
         * 若超過該值則擴容,若沒超過則不需要
         * 該值的計算方式:capacity * load factor
         */
        private int threshold;

        /**
         * 載入因子
         */
        private final float loadFactor;

        /**
         * 引用佇列
         *
         * 為什麼需要引用佇列呢?
         * 通過上面的介紹我們可以知道雜湊表中某些鍵可能會被移除掉,而移除是GC幫我們做的,那WeakHashMap怎麼知道哪些鍵被移除掉了以便更新自己的鍵值對,就是該佇列做了它們兩個之間的媒介
         * 上面讓讀者去了解Reference類,下面講的內容其實都在該類中有提到,比較簡單
         * GC在丟棄某個鍵時會將它的鍵值對,也就是節點資訊存放到Reference類中的pending佇列中,Reference類在初始化時會啟動一個執行緒,那麼該執行緒會將pending佇列中的節點資訊放入到queue佇列中
         * 也就是在告訴WeakHashMap,佇列中的這些節點是我要刪除的,你記得更新
         */
        private final ReferenceQueue<Object> queue = new ReferenceQueue<>();

        /**
         * 快取entrySet方法的返回值
         */
        private transient Set<Map.Entry<K,V>> entrySet;

        /**
         * 結構修改的次數
         */
        int modCount;
    }

建構函式


    /**
     * 指定初始容量與載入因子構造雜湊表
     * 在上面中我們提到了容量必須是2的冪次方,所以呼叫tableSizeFor方法來進行調整
     * Float.isNaN:檢測是否是數字
     * @param initialCapacity 指定初始容量
     * @param loadFactor 指定載入因子
     */
    public WeakHashMap(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);
        int capacity = 1;
        while (capacity < initialCapacity) //這段程式碼有點精髓啊,個人感覺比HashMap中的演算法簡單,兩者要表達的意思是一致的,都是獲取大於initialCapacity的最小值
            capacity <<= 1;
        table = newTable(capacity);
        this.loadFactor = loadFactor;
        threshold = (int)(capacity * loadFactor);
    }

    /**
     * 指定初始容量與預設載入因子(0.75)構造雜湊表
     * @param initialCapacity 指定初始容量
     */
    public WeakHashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    /**
     * 預設初始容量(16)與預設載入因子(0.75)構造雜湊表
     */
    public WeakHashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

    /**
     * 將指定集合新增到雜湊表中,採用預設載入因子
     * @param m 指定集合
     */
    public WeakHashMap(Map<? extends K, ? extends V> m) {
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1, DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);//Math.max是為了獲取儘可能大的容量
        putAll(m);
    }

簡單方法


    /**
     * 根據指定長度構造雜湊表
     * @param n 指定長度
     * @return 雜湊表
     */
    @SuppressWarnings("unchecked")
    private Entry<K,V>[] newTable(int n) {
        return (Entry<K,V>[]) new Entry<?,?>[n];
    }

    /**
     * 倘若鍵為null則採用NULL_KEY作為鍵
     * 正如方法名一樣,隱藏Null
     * @param key 指定鍵
     * @return NULL_KEY或指定鍵
     */
    private static Object maskNull(Object key) {
        return (key == null) ? NULL_KEY : key;
    }

    /**
     * 倘若鍵為NULL_KEY則返回null
     * 正如方法名一樣,揭露Null
     * @param key 雜湊表中的鍵
     * @return null或指定鍵
     */
    static Object unmaskNull(Object key) {
        return (key == NULL_KEY) ? null : key;
    }

    /**
     * 兩個物件是否相等
     * @param x 物件
     * @param y 另外一個物件
     * @return 是否相等
     */
    private static boolean eq(Object x, Object y) {
        return x == y || x.equals(y);
    }

    /**
     * 計算雜湊值
     * 這邊的計算雜湊值比HashMap複雜多了,涉及到演算法的內容我感覺我沒辦法理解到位
     * @param k 物件
     * @return 雜湊值
     */
    final int hash(Object k) {
        int h = k.hashCode();
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

    /**
     * 計算雜湊表中的索引
     * @param h 雜湊值
     * @param length 雜湊表的長度
     * @return 索引
     */
    private static int indexFor(int h, int length) {
        return h & (length-1);
    }

    /**
     * 清除雜湊表中過時的節點資訊
     * 過時指的是已經被丟棄的鍵,也可以說是被GC回收的鍵
     */
    private void expungeStaleEntries() {
        for (Object x; (x = queue.poll()) != null; ) {//poll:佇列中獲取首部節點並刪除
            synchronized (queue) {
                @SuppressWarnings("unchecked")
                    Entry<K,V> e = (Entry<K,V>) x;
                int i = indexFor(e.hash, table.length);

                Entry<K,V> prev = table[i]; //代表當前節點的上一個節點
                Entry<K,V> p = prev;
                while (p != null) {
                    Entry<K,V> next = p.next;
                    if (p == e) {
                        if (prev == e) //說明當前節點是連結串列的首部節點
                            table[i] = next;
                        else //說明當前節點不是首部節點
                            prev.next = next;
                        // Must not null out e.next;
                        // stale entries may be in use by a HashIterator
                        e.value = null; // Help GC
                        size--;
                        break;
                    }
                    prev = p;
                    p = next;
                }
            }
        }
    }

    /**
     * 獲取雜湊表
     * @return 雜湊表
     */
    private Entry<K,V>[] getTable() {
        expungeStaleEntries();
        return table;
    }

    /**
     * 獲取雜湊表的長度
     * @return 雜湊表的長度
     */
    public int size() {
        if (size == 0)
            return 0;
        expungeStaleEntries();
        return size;
    }

    /**
     * 雜湊表是否為空
     * @return 雜湊表是否為空
     */
    public boolean isEmpty() {
        return size() == 0;
    }

    /**
     * 指定鍵獲取指
     * @param key 指定鍵
     * @return null或值
     */
    public V get(Object key) {
        Object k = maskNull(key);
        int h = hash(k);
        Entry<K,V>[] tab = getTable();
        int index = indexFor(h, tab.length);
        Entry<K,V> e = tab[index];
        while (e != null) {
            if (e.hash == h && eq(k, e.get()))
                return e.value;
            e = e.next;
        }
        return null;
    }

    /**
     * 是否包含指定鍵
     * @param key 指定鍵
     * @return 是否包含指定鍵
     */
    public boolean containsKey(Object key) {
        return getEntry(key) != null;
    }

    /**
     * 指定鍵獲取節點
     * @param key 指定鍵
     * @return null或節點
     */
    Entry<K,V> getEntry(Object key) {
        Object k = maskNull(key);
        int h = hash(k);
        Entry<K,V>[] tab = getTable();
        int index = indexFor(h, tab.length);
        Entry<K,V> e = tab[index];
        while (e != null && !(e.hash == h && eq(k, e.get())))
            e = e.next;
        return e;
    }

    /**
     * 新增節點
     * 連結串列中採用頭插法的方式進行新增節點
     * 若超過閾值則會進行擴容
     * @param key 指定鍵
     * @param value 指定值
     * @return null或舊值
     */
    public V put(K key, V value) {
        Object k = maskNull(key);
        int h = hash(k);
        Entry<K,V>[] tab = getTable();
        int i = indexFor(h, tab.length); //獲取索引

        for (Entry<K,V> e = tab[i]; e != null; e = e.next) { //連結串列中判斷是否重複
            if (h == e.hash && eq(k, e.get())) {
                V oldValue = e.value;
                if (value != oldValue)
                    e.value = value;
                return oldValue;
            }
        }

        modCount++;
        Entry<K,V> e = tab[i];
        tab[i] = new Entry<>(k, value, queue, h, e);
        if (++size >= threshold)
            resize(tab.length * 2);
        return null;
    }

    void resize(int newCapacity) {
        Entry<K,V>[] oldTable = getTable();
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry<K,V>[] newTable = newTable(newCapacity);
        transfer(oldTable, newTable); //將源雜湊表中的所有節點資訊複製到目標雜湊表中
        table = newTable;

       /**
        * 如果忽略null元素並處理佇列導致大量收縮,則還原舊錶。 這應該很少見,但是可以避免持有大量無用節點的雜湊表的無限擴充套件。
        */
        if (size >= threshold / 2) {
            threshold = (int)(newCapacity * loadFactor);
        } else { //GC回收了大量的節點後則不進行擴容
            expungeStaleEntries(); //檢測新表中哪些節點已經被丟棄了
            transfer(newTable, oldTable);
            table = oldTable;
        }
    }

    /**
     * 將源雜湊表中的所有節點資訊複製到目標雜湊表中
     * 源雜湊表中可能出現被丟棄的鍵
     * @param src 源雜湊表
     * @param dest 目標雜湊表
     */
    private void transfer(Entry<K,V>[] src, Entry<K,V>[] dest) {
        for (int j = 0; j < src.length; ++j) {
            Entry<K,V> e = src[j];
            src[j] = null;
            while (e != null) {
                Entry<K,V> next = e.next;
                Object key = e.get(); //若當前節點已經被GC回收了,則此方法返回將返回null
                if (key == null) {
                    e.next = null;  // Help GC
                    e.value = null; //  "   "
                    size--;
                } else {
                    int i = indexFor(e.hash, dest.length); //該索引出現的可能應該跟HashMap是一樣的,原索引或與原索引 + 舊容量的大小,只不過它是一個一個的計算並新增,而HashMap是分批計算,一次性新增
                    e.next = dest[i];
                    dest[i] = e;
                }
                e = next;
            }
        }
    }

    /**
     * 批量新增節點到雜湊表中
     * @param m 集合
     */
    public void putAll(Map<? extends K, ? extends V> m) {
        int numKeysToBeAdded = m.size();
        if (numKeysToBeAdded == 0)
            return;

         /**
          * 倘若指定集合的鍵值對數量超過閾值則進行擴容. 這是保守的;
          * 很明顯的條件應該是 (m.size + size) >= threshold, 但是這個條件會導致適當的容量變成2倍,如果被新增的鍵已經存在於雜湊表中.
          * 通過使用保守的計算,我們最多隻能調整一種尺寸。
          */
        if (numKeysToBeAdded > threshold) {
            int targetCapacity = (int)(numKeysToBeAdded / loadFactor + 1);
            if (targetCapacity > MAXIMUM_CAPACITY)
                targetCapacity = MAXIMUM_CAPACITY;
            int newCapacity = table.length;
            while (newCapacity < targetCapacity)
                newCapacity <<= 1;
            if (newCapacity > table.length) //預先計算好要新增節點的數量以便進行一次性擴容
                resize(newCapacity);
        }

        for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
            put(e.getKey(), e.getValue());
    }

    /**
     * 指定鍵移除節點
     * @param key 指定鍵
     * @return null或值
     */
    public V remove(Object key) {
        Object k = maskNull(key);
        int h = hash(k);
        Entry<K,V>[] tab = getTable();
        int i = indexFor(h, tab.length);
        Entry<K,V> prev = tab[i];
        Entry<K,V> e = prev;

        while (e != null) {
            Entry<K,V> next = e.next;
            if (h == e.hash && eq(k, e.get())) {
                modCount++;
                size--;
                if (prev == e)
                    tab[i] = next;
                else
                    prev.next = next;
                return e.value;
            }
            prev = e;
            e = next;
        }

        return null;
    }

    /**
     * 指定鍵移除節點是否成功
     * @param o 指定鍵
     * @return 移除節點是否成功
     */
    boolean removeMapping(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Entry<K,V>[] tab = getTable();
        Map.Entry<?,?> entry = (Map.Entry<?,?>)o;
        Object k = maskNull(entry.getKey());
        int h = hash(k);
        int i = indexFor(h, tab.length);
        Entry<K,V> prev = tab[i];
        Entry<K,V> e = prev;

        while (e != null) {
            Entry<K,V> next = e.next;
            if (h == e.hash && e.equals(entry)) {
                modCount++;
                size--;
                if (prev == e)
                    tab[i] = next;
                else
                    prev.next = next;
                return true;
            }
            prev = e;
            e = next;
        }

        return false;
    }

    /**
     * 清空雜湊表
     */
    public void clear() {
        while (queue.poll() != null) //清空佇列中只有一部分過時節點
            ;

        modCount++;
        Arrays.fill(table, null); //清空雜湊表後
        size = 0;

        /**
         * 清空雜湊表後可能導致GC,另外一部分節點會被新增到佇列中,所以此處需要再次清空佇列
         */
        while (queue.poll() != null)
            ;
    }

    /**
     * 雜湊表中是否包含指定值
     * @param value 指定值
     * @return 是否包含指定值
     */
    public boolean containsValue(Object value) {
        if (value==null)
            return containsNullValue();

        Entry<K,V>[] tab = getTable();
        for (int i = tab.length; i-- > 0;)
            for (Entry<K,V> e = tab[i]; e != null; e = e.next)
                if (value.equals(e.value))
                    return true;
        return false;
    }

    /**
     * 雜湊表中是否包含null值
     * @return 是否包含null值 
     */
    private boolean containsNullValue() {
        Entry<K,V>[] tab = getTable();
        for (int i = tab.length; i-- > 0;)
            for (Entry<K,V> e = tab[i]; e != null; e = e.next)
                if (e.value==null)
                    return true;
        return false;
    }


    /**
     * 雜湊表中的節點,該類繼承了WeakReference加上呼叫了父類的構造,說明它的鍵是個弱引用
     * 該類中的其他方法就不做展示了,比較簡單
     */
    private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {

        V value;
        final int hash;
        Entry<K,V> next;

        /**
         * 初始化
         * 指定鍵生成弱引用
         * @param key 指定鍵
         * @param value 指定值
         * @param queue 與弱引用關聯的佇列
         * @param hash 雜湊值
         * @param next 下一個節點
         */
        Entry(Object key, V value, ReferenceQueue<Object> queue, int hash, Entry<K,V> next) {
            super(key, queue);
            this.value = value;
            this.hash  = hash;
            this.next  = next;
        }
    }

    /**
     * 遍歷所有鍵並執行指定動作
     * 遍歷過程中不允許WeakHashMap呼叫任何會修改結構的方法,否則最後會丟擲異常
     * @param action 指定動作
     */
    public void forEach(BiConsumer<? super K, ? super V> action) {
        Objects.requireNonNull(action);
        int expectedModCount = modCount;

        Entry<K, V>[] tab = getTable();
        for (Entry<K, V> entry : tab) {
            while (entry != null) {
                Object key = entry.get();
                if (key != null) {
                    action.accept((K)WeakHashMap.unmaskNull(key), entry.value);
                }
                entry = entry.next;

                if (expectedModCount != modCount) {
                    throw new ConcurrentModificationException();
                }
            }
        }
    }

    /**
     * 遍歷雜湊表並執行指定動作後獲取新值,利用新值替換所有節點的舊值
     * @param function 指定動作
     */
    public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
        Objects.requireNonNull(function);
        int expectedModCount = modCount;

        Entry<K, V>[] tab = getTable();;
        for (Entry<K, V> entry : tab) {
            while (entry != null) {
                Object key = entry.get();
                if (key != null) {
                    entry.value = function.apply((K)WeakHashMap.unmaskNull(key), entry.value);
                }
                entry = entry.next;

                if (expectedModCount != modCount) {
                    throw new ConcurrentModificationException();
                }
            }
        }
    }

    //一些重複性的東西,比如包含鍵、值、鍵值對的迭代器、可分割迭代器就不講解了,可參考HashMap

總結

  • WeakHashMap的鍵值對允許為null。

  • WeakHashMap採用弱鍵,當某個鍵不在使用時會被GC回收,而鍵對應的節點也會被移除掉。

  • WeakHashMap無序不可重複、非執行緒安全。

  • 在新增節點,值物件最好不要與任何的鍵直接或間接的關聯,否則GC無法丟棄該鍵。

  • WeakHashMap#ReferendeQueue是用來檢視雜湊表中哪些鍵被丟棄了,以便雜湊表能夠及時更新。

  • WeakHashMap的容量必須是2的冪次方。

  • WeakHashMap在新增節點時採用的是頭插法。

重點關注

弱鍵 ReferenceQueue 頭插法 強、軟、弱、虛引用 Reference