Java WeakHashMap

  做爲一個java開發者確定都知道且使用HashMap,但估計大部分人都不太知道WeakHashMap。從類定義上來看,它和普通的HashMap同樣,繼承了AbstractMap類和實現了Map接口,也就是說它有着與HashMap差很少的功能。那麼既然jdk已經提供了HashMap,爲何還要再提供一個WeakHashMap呢? 黑格爾曾經說過,存在必合理,接下來咱們來看下爲何有WeakHashMap。
  先來想象一下你由於某種需求須要一個Cache,你確定會面臨一個問題,就是全部數據不可能都放到Cache裏,或者放到Cache裏性價比過低了。這個時候你可能很快就想到了各類Cache數據過時策略,目前也有一些優秀的包提供了功能豐富的Cache,好比Google的Guava Cache,它支持數據按期過時、LRU、LFU等策略,但它任然有可能會致使有用的數據被淘汰,沒用的數據遲遲不淘汰(若是策略使用得當的狀況下這都是小几率事件)。
  若是我如今說有種機制,可讓你Cache裏不用的key數據自動清理掉,用的還留着,沒有誤殺也沒有漏殺你信不信!沒錯WeakHashMap就是能實現這種功能的東西,這也是它和普通的HashMap不一樣的地方——它有自清理的機制。  若是讓你實現一種自清理的HashMap,你怎麼作? 個人作法確定是想辦法先知道某個Key確定沒有在用了,而後清理到HashMap中對應的K-V。在JVM裏一個對象沒用了是指沒有任何其餘有用對象直接或者間接執行它,具體點就是在GC過程當中它是GCRoots不可達的。 Jvm提供了一種機制能讓咱們感知到一個對象是否已經變成了垃圾對象,這就是WeakReference,不瞭解WeakReference的能夠看下我上一篇介紹博客Java弱引用(WeakReferences)
  某個WeakReference對象所指向的對象若是被斷定爲垃圾對象,Jvm會將該WeakReference對象放到一個ReferenceQueue裏,咱們只要看下這個Queue裏的內容就知道某個對象還有沒有用了。 WeakHashMap就是這麼作的,因此這裏的Weak是指WeakReference。接下來讓咱們看下它的代碼,看它具體是怎麼實現的。
  
java

源碼分析

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;複製代碼

  和普通HashMap同樣,WeakHashMap也有一些默認值,好比默認容量是16,最大容量2^30,使用超過75%它就會自動擴容。
git

Entry

private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
        V value;
        final int hash;
        Entry<K,V> 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;
        }
        /*
        * 其餘代碼  
        */
}複製代碼

  它的Entry和普通HashMap的Entry最大的不一樣是它繼承了WeakReference,而後把Key作成了弱引用(注意只有Key沒有Value),而後傳入了一個ReferenceQueue,這就讓它能在某個key失去全部強引用的時候感知到。
  github

put

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;
    }複製代碼

  put方法也很簡單,用key的hashcode在tab中定位,而後判斷是不是已經存在的key,已經存在就替換舊值,不然就新建Entry,遇到多個不一樣的key有一樣的hashCode就採用開鏈的方式解決hash衝突。注意這裏和HashMap不太同樣的地方,HashMap會在鏈表太長的時候對鏈表作樹化,把單鏈錶轉換爲紅黑樹,防止極端狀況下hashcode衝突致使的性能問題,但在WeakHashMap中沒有樹化。
  一樣,在size大於閾值的時候,WeakHashMap也對作resize的操做,也就是把tab擴大一倍。WeakHashMap中的resize比HashMap中的resize要簡單好懂些,但沒HashMap中的resize優雅。WeakHashMap中resize有另一個額外的操做,就是expungeStaleEntries(),就是對tab中的死對象作清理,稍後會詳細介紹。緩存

get

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;
    }複製代碼

  get方法就沒什麼特別的了,由於Entry裏存了hash值和key的值,因此只要用indexFor定位到tab中的位置,而後遍歷一下單鏈表就知道了。
安全

expungeStaleEntries

private void expungeStaleEntries() {
        for (Object x; (x = queue.poll()) != null; ) {
            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;
                }
            }
        }
    }複製代碼

  expungeStaleEntries就是WeakHashMap的核心了,它承擔着Map中死對象的清理工做。原理就是依賴WeakReference和ReferenceQueue的特性。在每一個WeakHashMap都有個ReferenceQueue queue,在Entry初始化的時候也會將queue傳給WeakReference,這樣當某個能夠key失去全部強應用以後,其key對應的WeakReference對象會被放到queue裏,有了queue就知道須要清理哪些Entry裏。這裏也是整個WeakHashMap裏惟一加了同步的地方。  除了上文說的到resize中調用了expungeStaleEntries(),size()中也調用了這個清理方法。另外 getTable()也調了,這就意味着幾乎全部其餘方法都間接調用了清理。
多線程

其餘

  除了上述幾個和HashMap不太同樣的地方外,WeakHashMap也提供了其餘HashMap全部的方法,好比像remove、clean、putAll、entrySet…… 功能上幾乎能夠徹底替代HashMap,但WeakHashMap也有一些本身的缺陷。
工具

缺陷

1.非線程安全

  關鍵修改方法沒有提供任何同步,多線程環境下確定會致使數據不一致的狀況,因此使用時須要多注意。源碼分析

2.單純做爲Map沒有HashMap好

  HashMap在Jdk8作了好多優化,好比單鏈表在過長時會轉化爲紅黑樹,下降極端狀況下的操做複雜度。但WeakHashMap沒有相應的優化,有點像jdk8以前的HashMap版本。
  性能

3.不能自定義ReferenceQueue

  WeakHashMap構造方法中無法指定自定的ReferenceQueue,若是用戶想用ReferenceQueue作一些額外的清理工做的話就行不通了。若是即想用WeakHashMap的功能,也想用ReferenceQueue,貌似得本身實現一套新的WeakHashMap了。
優化

用途

  這裏列舉幾個我所知道的WeakHashMap的使用場景。

1.阿里Arthas

  在阿里開源的Java診斷工具中使用了WeakHashMap作類-字節碼的緩存。

// 類-字節碼緩存
    private final static Map<Class<?>/*Class*/, byte[]/*bytes of Class*/> classBytesCache
            = new WeakHashMap<Class<?>, byte[]>();複製代碼

2.Cache

  WeakHashMap這種自清理的機制,很是適合作緩存了。

3.ThreadLocalMap

  ThreadLocal中用ThreadLocalMap存儲Thread對象,雖然ThreadLocalMap和WeakHashMap不是一個東西,但ThreadLocalMap也利用到了WeakReference的特性,功能用途很相似,因此我很好奇爲何ThreadLocalMap不直接用WeakHashMap呢!
版權聲明:本文爲博主原創文章,轉載請註明出處。 博客地址:xindoo.blog.csdn.net/

相關文章
相關標籤/搜索