我应该使用哪个Java集合来实现线程安全的缓存?

10

我想实现一个简单的缓存,不想做太多工作(当然)。 我认为其中一个标准的Java集合应该足够,只需要进行一些额外的工作。 具体来说,我正在存储来自服务器的响应,键可以是请求URL字符串或从URL生成的哈希码。

我最初认为我可以使用WeakHashMap,但看起来这种方法强制我管理我想要保留的对象,并且我没有用强引用管理的任何对象都会立即被清除。 我应该尝试使用SoftReference值的ConcurrentHashMap吗? 还是那些也会被很积极地清理掉?

我现在正在查看LinkedHashMap类。 经过一些修改,它看起来很有前途,可以用作MRU缓存。 还有其他建议吗?

无论我使用哪个集合,我是否应该尝试手动修剪LRU值,还是可以信任VM偏向于不回收最近访问的对象?

顺便说一下,我正在Android上开发,因此我不想导入任何第三方库。 我处理的堆非常小(16到24 MB),因此VM可能非常渴望回收资源。 我假设GC会很积极。


4
java.util.concurrent.ConcurrentHashMap<K,V> http://developer.android.com/reference/java/util/concurrent/ConcurrentHashMap.html 可以。 - Jose Diaz
不,使用 LinkedHashMap 我不一定需要执行 remove() 然后再执行 put(),因为它还可以修改链表的顺序为“访问顺序”,而不是默认的“插入顺序”。 - Neil Traft
2
我承认错误,我没有注意到那个构造函数。在这种情况下,这是最好的方法。我强烈建议 不要 使用软引用,因为可用内存是控制缓存的一个非常糟糕的方式(尽管在有限内存设备中可能相对较少)。 - kdgregory
5个回答

6

LinkedHashMap很容易用于缓存。这将创建一个大小为10的MRU缓存。

private LinkedHashMap<File, ImageIcon> cache = new LinkedHashMap<File, ImageIcon>(10, 0.7f, true) {
    @Override
    protected boolean removeEldestEntry(Map.Entry<File, ImageIcon> eldest) {
        return size() > 10;
    }
};

我猜你可以创建一个带有同步委托的类来处理这个LinkedHashMap。如果我的同步理解有误,请原谅我。

好的答案!然后我可以使用Collections.synchronizedMap使地图同步。这可能会降低性能,但在这种情况下,我不太担心访问/插入时间;我不会经常这样做。 - Neil Traft
这个答案和Andrzej上面的一样。我选择他的,因为提供了更多的信息。 - Neil Traft
是的,那应该可以。我只是认为Collections.synchronizedMap()会创建一个新的映射而不是包装它。 - Denis Tulskiy

6
如果您使用基于SoftReference的键,则VM将有偏见(强)反对最近访问的对象。但要确定缓存语义将非常困难-与WeakReference相比,SoftReference给您的唯一保证是它会在抛出OutOfMemoryError之前被清除。对于JVM实现,将其视为WeakReferences的相同部分是完全合法的,在这种情况下,您可能会得到一个不缓存任何内容的缓存。
我不知道Android上的情况如何,但是在Sun最新的JVM中,可以使用-XX:SoftRefLRUPolicyMSPerMB命令行选项来调整SoftReference的行为,该选项确定柔性可访问对象将保留的毫秒数每MB自由内存在堆中。正如您所看到的,在这方面获得任何可预测的寿命行为将异常困难,并且该设置适用于VM中所有软引用,并且无法针对单个类使用SoftReferences进行调整(每个使用都有不同的参数)。
创建LRU高速缓存的最简单方法是通过扩展LinkedHashMap(如此处所述)。由于您需要线程安全,因此最初扩展它的最简单方法只是在此自定义类的实例上使用Collections.synchronizedMap,以确保安全的并发行为。
谨防过早优化-除非您需要非常高的吞吐量,否则理论上次优的粗同步开销不太可能成为问题。好消息是-如果分析显示由于重锁等待而执行速度太慢,则您将有足够的可用信息来了解缓存的运行时使用情况,因此您将能够提出适当的无锁替代方案(可能基于ConcurrentHashMap与手动LRU处理),而不必猜测其负载特征。

3
8个月前我采用了这种解决方案,一直沿用至今。不过,我刚刚注意到一个需要注意的问题:LinkedHashMapputAll 操作时不会调用 removeEldestEntry方法。所以如果你使用这种方式来实现缓存,就不应该调用putAll,否则缓存将会出错! 这只是需要注意的一点;我仍然很喜欢这种方法。你可能想要像我一样重写 putAll 方法,使其抛出 UnsupportedOperationException 异常。 - Neil Traft
Neil - 非常好的观点。另一种选择是覆盖putAll,例如for (Entry e : m.entrySet()) put(e.getKey(), e.getValue())。调用一系列单元素插入既尊重缓存的语义,也保证了putAll的正确性。但我对此有两种想法,因为这有点暗示putAll是一个高效的批量操作。也许抛出异常,并要求客户端自己执行此循环(如果这是可接受的备选方案),会导致整体代码更清晰。 - Andrzej Doyle

1

为了同步,Collections 框架提供了一个同步的映射:

Map<V,T> myMap = Collections.synchronizedMap(new HashMap<V, T>());

你可以将其包装起来,或在缓存对象中处理LRU逻辑。

2
我在这个问题中真正询问的是缓存,我已经知道如何使集合同步。 - Neil Traft
2
通常情况下,对于这种类型的使用,ConcurrentMap是一个更好的选择,因为它提供了原子的putIfAbsent()操作符,而这两个操作(containsKey()put())在同步Map中并没有被同步。 - matt b
@Neil Traft:抱歉,我从你的问题中没有清楚地理解那点。 - aperkins

1

www.javolution.org有一些有趣的功能 - 同步快速集合。 在您的情况下,它值得一试,因为它还为Android等小型设备提供了一些巧妙的增强功能。


我之前确实使用过Javolution,我同意它非常棒。虽然我仍然不太愿意包含外部库,但是Javolution具体有哪些类可以解决创建MRU缓存的问题呢? - Neil Traft
我会扩展FastMap,并将映射中的值作为元组(时间戳,值),或者可能将键作为实际键+时间戳的组合。在这两种情况下,LRU都很容易实现。 - Daniel Voina

0

我喜欢使用Apache Commons Collections LRUMap


嗯,好建议。我想知道他们的许可证是否允许我在不导入所有Commons Collections的情况下窃取源代码? - Neil Traft
据我所知,Apache 大多数情况下是宽容的,只要你保留头部信息,就应该没问题。虽然该库非常小且快速,但非常有用(请参见 CollectionUtils,它提供了 subtract、intersec、collect、transform 等功能)。 - Julio Faerman

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接