1. 程式人生 > >Java還要再學一遍基礎(十一)WeakHashMap詳解

Java還要再學一遍基礎(十一)WeakHashMap詳解

WeakHashMap概述

WeakHashMap是以弱鍵實現的基於雜湊表的儲存對映資料的Map。當JVM對於這些弱鍵所指向的物件進行了清理回收之後,WeakHashMap會自動有效的將被回收了的對映從map中移除。

引用的相關知識

Java中的引用一共分為四種,分別為強引用(Strong Reference),軟引用(Soft Reference),弱引用(Weak Reference)和幻影引用(Phantom Reference)。
可以看到java.lang.ref包中有對應的幾種:
這裡寫圖片描述

  • 強引用(Strong Reference)
    強引用是java預設實現的引用。JVM會盡可能長時間的保留強引用的存在。當沒有任何物件指向它時JVM將會回收。

    //這是一個強引用
    Object obj = new Object();
  • 弱引用(Weak Reference)
    弱引用是指當物件沒有任何的強引用存在,在下一次的JVM的gc回收的時候它將會被回收。

    //這裡是一個強引用
    Object obj = new Object();
    //new一個弱引用
    WeakReference<Object> weakRef = new WeakReference<Object>(obj);
    //obj = null後不再有強引用
    obj = null;
    //因為jvm的gc時間不確定所以迴圈
    while(true){
        //weakRef.get()當還未被回收的時候返回該物件,否則返回null
    if(weakRef.get() != null){ System.out.println("is alive"); } else{ System.out.println("is not alive"); break; } }

    執行結果:

    ...
    ...
    is alive
    is alive
    is alive
    is alive
    is alive
    is alive
    is not alive
  • 軟引用(Soft Reference)
    軟引用和弱引用基本性質一致,區別就是軟引用JVM只會在虛擬機器記憶體不足的時候才會去回收軟引用,這使得軟引用很適合做快取應用。
  • 幻影引用(Phantom Reference)
    這種引用型別的get方法無論什麼時候都會返回null。它唯一的作用就是可以用來記錄物件是什麼時候被gc回收的。

問題來了。怎麼去記錄物件的回收呢?
從上面的圖片中包含的class可以看到其中有一個ReferenceQueue的class,翻譯成中文就是引用佇列,用來幹什麼?
原來java提供這個引用佇列,可以把這個隊列當作引數傳到引用的構造器中,那麼之後當物件被回收之後便會做一件事情,就是把被回收的物件放到這個因喲個佇列中去。

利用ReferenceQueue記錄物件被回收的例子:

Object obj = new Object();
ReferenceQueue<Object> queue = new ReferenceQueue<>();
WeakReference<Object> weakRef = new WeakReference<Object>(obj, queue);

System.out.println(obj);
obj = null;
System.gc();
while(true){
    //已經被回收
    if(weakRef.get() == null){
        Object o;
        //從佇列中取
        if((o = queue.poll()) != null){
            System.out.println(o);
            break;
        }
    }
}

執行結果:

java.lang.Object@15db9742
java.lang.ref.WeakReference@6d06d69c

WeakHashMap

有了引用以及弱引用的相關概念並且瞭解HashMap的話WeakHashMap便很容易理解了。本文基於JDK1.8

1. 類定義

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

跟HashMap一樣繼承自AbstractMap並且實現Map介面,與之不同的是HashMap同時還實現了Cloneable, Serializable介面,這意味著WeakHashMap將不支援克隆和序列化。

2. 屬性

//預設容量
private static final int DEFAULT_INITIAL_CAPACITY = 16;
//最大容量
private static final int MAXIMUM_CAPACITY = 1 << 30;
//預設載入因子
private static final float DEFAULT_LOAD_FACTOR = 0.75f;
//節點陣列
Entry<K,V>[] table;
//size
private int size;
//閾值
private int threshold;
//載入因子
private final float loadFactor;
//ReferenceQueue用於記錄gc回收
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
//修改標誌
int modCount;

可以看到基本上和HashMap沒有什麼太大的出入,明顯的區別則是多了一個ReferenceQueue,很明顯WeakHashMap就是通過這個引用佇列來實現自動清理的。

2. 構造方法

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)
        capacity <<= 1;
    table = newTable(capacity);
    this.loadFactor = loadFactor;
    threshold = (int)(capacity * loadFactor);
}
//其他構造引數基本與HashMap相同,省略

這裡有一點點與HashMap不同的是,HashMap在確定capacity的演算法上有些不同。HashMap是通五次的無符號右移並或運算

    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

而WeakHashMap則是通過while迴圈實現。
同時HashMap的table陣列並沒有直接在構造器裡面初始化,而是在加入元素之後延遲初始化的。

3. Entry節點

private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
    V value;
    final int hash;
    Entry<K,V> next;

    /**
     * Creates new entry.
     */
    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;
    }
   //......

這裡看看構造器,傳入了一個ReferenceQueue,似乎根最開始講的ReferenceQueue記錄gc回收很相似的用法。因為Entry繼承了WeakReference類,所以現在的Entry物件管理的並不是強引用,而是弱引用也就是WeakReference。所以當被回收的時候自然就將會被這個引用佇列所記錄下來。之後只需要進行清理操作即可。

3. 關鍵的方法

  1. get方法。為了能讓每次獲取map中的元素的時候都能從已經自動清理過後的table陣列中獲取,所以get方法中加入了對應了清理操作。

    public V get(Object key) {
    Object k = maskNull(key);
    int h = hash(k);
    //這個getTable便是獲取清理後的table
    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;
    }
    
    
    

    如何清理的:

    private Entry<K,V>[] getTable() {
        //expunge是抹去的意思  Stale:陳舊的
        expungeStaleEntries();
        return table;
    }
    private void expungeStaleEntries() {
        //從佇列中出隊遍歷
        for (Object x; (x = queue.poll()) != null; ) {
            synchronized (queue) {
                @SuppressWarnings("unchecked")
                    Entry<K,V> e = (Entry<K,V>) x;
                //通過hash找到index
                int i = indexFor(e.hash, table.length);
    
                Entry<K,V> prev = table[i];
                Entry<K,V> p = prev;
                //遍歷連結串列
                while (p != null) {
                    //記錄下一個節點next
                    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;
                    }
                    //p記錄下一個節點用作遍歷,pref則是p的上一個節點
                    prev = p;
                    p = next;
                }
            }
        }
    }

    這下就很清楚了,原來是遍歷佇列中被回收的記錄,再在table陣列以及處理hash衝突的連結串列中去找到該記錄並刪除。
    初次之外,在resize和size方法呼叫的時候也使用到了上面的expungeStaleEntries方法。這裡不做詳細介紹。

  2. hash方法

    
    final int hash(Object k) {
        int 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);
    }

和HashMap不相同,但是有點複雜….

WeakHashMap的特性

  • 儲存鍵值對對映。
  • key和value可以為null
  • 執行緒不安全
  • 當鍵被gc回收能夠自動清理map中的對映
  • 不支援克隆和序列化