views:

148

answers:

5

I'm looking to implement a simple cache without doing too much work (naturally). It seems to me that one of the standard Java collections ought to suffice, with a little extra work. Specifically, I'm storing responses from a server, and the keys can either be the request URL string or a hash code generated from the URL.

I originally thought I'd be able to use a WeakHashMap, but it looks like that method forces me to manage which objects I want to keep around, and any objects I don't manage with strong references are immediately swept away. Should I try out a ConcurrentHashMap of SoftReference values instead? Or will those be cleaned up pretty aggressively too?

I'm now looking at the LinkedHashMap class. With some modifications it looks promising for an MRU cache. Any other suggestions?

Whichever collection I use, should I attempt to manually prune the LRU values, or can I trust the VM to bias against reclaiming recently accessed objects?

FYI, I'm developing on Android so I'd prefer not to import any third-party libraries. I'm dealing with a very small heap (16 to 24 MB) so the VM is probably pretty eager to reclaim resources. I assume the GC will be aggressive.

+1  A: 

www.javolution.org has some interestig features - synchronized fast collections. In your case it worth a try as it offers also some nifty enhancements for small devices as Android ones.

Daniel Voina
I have actually used Javolution before and I agree it's pretty great. I'm still reluctant to include external libraries, but what class does Javolution specifically have to address the creation an MRU cache?
Neil Traft
I would extend FastMap and have the value in map as ab tuple (timestamp, value) or maybe the key as a composite of actual key + timestamp. In both situations the LRU will be easy to implement.
Daniel Voina
A: 

I like Apache Commons Collections LRUMap

Julio Faerman
Hmmm, good suggestion. I wonder if their license will let me steal the source code without importing all of Commons Collections?
Neil Traft
AFAIK apache is mostly permissive, if you keep the headers you should be fine. The library is very small and fast though, and very useful (see CollectionUtils, it provides subtract, intersec, collect, transform....)
Julio Faerman
+4  A: 

If you use SoftReference-based keys, the VM will bias (strongly) against recently accessed objects. However it would be quite difficult to determine the caching semantics - the only guarantee that a SoftReference gives you (over a WeakReference) is that it will be cleared before an OutOfMemoryError is thrown. It would be perfectly legal for a JVM implementation to treat them identically to WeakReferences, at which point you might end up with a cache that doesn't cache anything.

I don't know how things work on Android, but with Sun's recent JVMs one can tweak the SoftReference behaviour with the -XX:SoftRefLRUPolicyMSPerMB command-line option, which determines the number of milliseconds that a softly-reachable object will be retained for, per MB of free memory in the heap. As you can see, this is going to be exceptionally difficult to get any predictable lifespan behaviour out of, with the added pain that this setting is global for all soft references in the VM and can't be tweaked separately for individual classes' use of SoftReferences (chances are each use will want different parameters).


The simplest way to make an LRU cache is by extending LinkedHashMap as described here. Since you need thread-safety, the simplest way to extend this initially is to just use Collections.synchronizedMap on an instance of this custom class to ensure safe concurrent behaviour.

Beware premature optimisation - unless you need very high throughput, the theoretically suboptimal overhead of the coarse synchronization is not likely to be an issue. And the good news - if profiling shows that you are performing too slowly due to heavy lock contention, you'll have enough information available about the runtime use of your cache that you'll be able to come up with a suitable lockless alternative (probably based on ConcurrentHashMap with some manual LRU treatment) rather than having to guess at its load profile.

Andrzej Doyle
+2  A: 

For synchronization, the Collections framework provides a synchronized map:

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

You could then wrap this, or handle the LRU logic in a cache object.

aperkins
I'm really asking about caching in this question, I already know how to make a collection synchronized.
Neil Traft
Generally a ConcurrentMap is a better idea for this type of usage since it offers an atomic `putIfAbsent()` operator, whereas these two operations (`containsKey()` and `put()`) are not synchronized by the synchronized Map
matt b
@Neil Traft: sorry, I wasn't clear on that from reading your question.
aperkins
+2  A: 

LinkedHashMap is easy to use for cache. This creates an MRU cache of size 10.

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;
    }
};

I guess you can make a class with synchronized delegates to this LinkedHashMap. Forgive me if my understanding of synchronization is wrong.

tulskiy
Good answer! And I could then make the map synchronized with `Collections.synchronizedMap`. That may reduce performance but I'm not too worried about access/insertion time in this case; I won't be doing it very often.
Neil Traft
This is the same answer as Andrzej's above. I picked his instead of yours for all the extra information.
Neil Traft
Yes, that should work. I just though that `Collections.synchronizedMap()` creates a new map instead of wrapping it.
tulskiy