Java 的 WeakHashMap 和缓存:为什么它引用的是键,而不是值?

问题描述 投票:0回答:5

Java 的 WeakHashMap 通常被认为对于缓存很有用。尽管它的弱引用是根据映射的键而不是其值来定义的,但这似乎很奇怪。我的意思是,这是我想要缓存的值,并且一旦除了缓存之外没有其他人强烈引用它们,我就希望对其进行垃圾收集,不是吗?

以何种方式保存对键的弱引用有帮助?如果您执行

ExpensiveObject o = weakHashMap.get("some_key")
,那么我希望缓存保留“o”,直到调用者不再保留强引用,并且我根本不关心字符串对象“some_key”。

我错过了什么吗?

java caching weak-references
5个回答
127
投票

WeakHashMap 没有用作缓存,至少大多数人是这样认为的。正如你所说,它使用弱,而不是弱,所以它不是为大多数人想要使用它的目的而设计的(事实上,我已经见过人们错误地使用它)。

WeakHashMap 最适合保存有关您无法控制生命周期的对象的元数据。例如,如果您有一堆对象通过您的类,并且您希望跟踪有关它们的额外数据,而无需在它们超出范围时收到通知,并且无需引用它们来保持它们的活动状态。

一个简单的例子(我以前用过)可能是这样的:

WeakHashMap<Thread, SomeMetaData>

您可以在其中跟踪系统中各个线程正在做什么;当线程终止时,该条目将从您的映射中默默删除,并且如果您是对该线程的最后一个引用,则不会阻止该线程被垃圾收集。然后,您可以迭代该映射中的条目,以找出有关系统中活动线程的元数据。

请参阅非缓存中的WeakHashMap!了解更多信息。

对于您想要的缓存类型,可以使用专用的缓存系统(例如EHCache)或查看GuavaMapMaker类;类似的东西

new MapMaker().weakValues().makeMap();

会做你想做的事,或者如果你想变得更奇特,你可以添加定时过期:

new MapMaker().weakValues().expiration(5, TimeUnit.MINUTES).makeMap();

40
投票

WeakHashMap
的主要用途是当您有想要在其键消失时消失的映射时。缓存则相反——当映射的值消失时,您希望这些映射也消失。

对于缓存,你想要的是一个

Map<K,SoftReference<V>>
。当内存紧张时,
SoftReference
将被垃圾收集。 (将此与
WeakReference
进行对比,一旦不再对其所指对象进行硬引用,它可能会被清除。)您希望您的引用在缓存中是软引用(至少在键值映射不存在的缓存中) t go stale),从那时起,如果您稍后查找您的值,则有可能仍然在缓存中。如果引用很弱,您的值将立即被GC,从而违背了缓存的目的。

为了方便起见,您可能希望隐藏

SoftReference
实现中的
Map
值,以便您的缓存看起来是
<K,V>
类型而不是
<K,SoftReference<V>>
类型。如果您想这样做,这个问题有网上可用的实现建议。

另请注意,当您在

SoftReference
中使用
Map
值时,您必须执行某些操作来手动删除已清除
SoftReferences
的键值对——否则您的
Map
只会增长永远大小,并泄漏内存。


9
投票

您需要两张映射:一张在缓存键和弱引用值之间映射,一张在弱引用值和键之间反向映射。并且您需要一个引用队列和一个清理线程。

弱引用能够在无法再访问引用的对象时将引用移动到队列中。该队列必须由清理线程清空。 为了清理,需要获取密钥作为参考。这就是需要第二张地图的原因。

以下示例展示了如何使用弱引用的哈希图创建缓存。当您运行该程序时,您会得到以下输出:

$ javac -Xlint:未选中的 Cache.java && java 缓存
{偶数:[2,4,6],奇数:[1,3,5]}
{偶数: [2, 4, 6]}

第一行显示删除赔率列表引用之前缓存的内容,第二行显示删除赔率之后的缓存内容。

这是代码:

import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

class Cache<K,V>
{
    ReferenceQueue<V> queue = null;
    Map<K,WeakReference<V>> values = null;
    Map<WeakReference<V>,K> keys = null;
    Thread cleanup = null;

    Cache ()
    {
        queue  = new ReferenceQueue<V>();
        keys   = Collections.synchronizedMap (new HashMap<WeakReference<V>,K>());
        values = Collections.synchronizedMap (new HashMap<K,WeakReference<V>>());
        cleanup = new Thread() {
                public void run() {
                    try {
                        for (;;) {
                            @SuppressWarnings("unchecked")
                            WeakReference<V> ref = (WeakReference<V>)queue.remove();
                            K key = keys.get(ref);
                            keys.remove(ref);
                            values.remove(key);
                        }
                    }
                    catch (InterruptedException e) {}
                }
            };
        cleanup.setDaemon (true);
        cleanup.start();
    }

    void stop () {
        cleanup.interrupt();
    }

    V get (K key) {
        return values.get(key).get();
    }

    void put (K key, V value) {
        WeakReference<V> ref = new WeakReference<V>(value, queue);
        keys.put (ref, key);
        values.put (key, ref);
    }

    public String toString() {
        StringBuilder str = new StringBuilder();
        str.append ("{");
        boolean first = true;
        for (Map.Entry<K,WeakReference<V>> entry : values.entrySet()) {
            if (first)
                first = false;
            else
                str.append (", ");
            str.append (entry.getKey());
            str.append (": ");
            str.append (entry.getValue().get());
        }
        str.append ("}");
        return str.toString();
    }

    static void gc (int loop, int delay) throws Exception
    {
        for (int n = loop; n > 0; n--) {
            Thread.sleep(delay);
            System.gc(); // <- obstinate donkey
        }
    }

    public static void main (String[] args) throws Exception
    {
        // Create the cache
        Cache<String,List> c = new Cache<String,List>();

        // Create some values
        List odd = Arrays.asList(new Object[]{1,3,5});
        List even = Arrays.asList(new Object[]{2,4,6});

        // Save them in the cache
        c.put ("odd", odd);
        c.put ("even", even);

        // Display the cache contents
        System.out.println (c);

        // Erase one value;
        odd = null;

        // Force garbage collection
        gc (10, 10);

        // Display the cache again
        System.out.println (c);

        // Stop cleanup thread
        c.stop();
    }
}

8
投票

另一件需要考虑的事情是,如果采用

Map<K, WeakReference<V>>
方法,值可能会消失,但映射不会。根据使用情况,您最终可能会得到一个包含许多弱引用已被 GC 处理的条目的 Map。


0
投票

如果您需要弱值,这非常简单:

public final class SimpleCache<K,V> {
    private final HashMap<K,Ref<K,V>> map = new HashMap<>();
    private final ReferenceQueue<V> queue = new ReferenceQueue<>();
    
    private static final class Ref<K,V> extends WeakReference<V> {
        final K key;
        
        Ref(K key, V value, ReferenceQueue<V> queue) {
            super(value, queue);
            this.key = key;
        }
    }
    
    private synchronized void gc() {
        for (Ref<?,?> ref; (ref = (Ref<?,?>)queue.poll()) != null;)
            map.remove(ref.key, ref);
    }
    
    public synchronized V getOrCreate(K key, Function<K,V> creator) {
        gc();
        Ref<K,V> ref = map.get(key);
        V v = ref == null ? null : ref.get();
        if (v == null) {
            v = Objects.requireNonNull(creator.apply(key));
            map.put(key, new Ref<>(key, v, queue));
        }
        return v;
    }
    
    public synchronized void remove(K key) {
        gc();
        map.remove(key);
    }
}

无需多线程;当调用其他方法时,通过机会性地轮询引用队列来删除陈旧的映射条目。 (这也是 WeakHashMap 的工作原理。)

示例:

static final SimpleCache<File,BigObject> cache = new SimpleCache<>();

...

// if there is already a BigObject generated for this file,
// and it is hasn't been garbage-collected yet, it is returned;
// otherwise, its constructor is called to create one
BigObject bo = cache.getOrCreate(fileName, BigObject::new)
// it will be gc'd after nothing in the program keeps a strong ref any more
© www.soinside.com 2019 - 2024. All rights reserved.