4

Background Information

You can make a LRU cache with a LinkedHashMap as shown at this link. Basically, you just:

  • Extend linked hash map.
  • Provide a capacity parameter.
  • Initialize the super class (LinkedHashMap) with parameters to tell it its capacity, scaling factor (which should never be used), and to keep items in insertion/reference order.
  • Override removeEldestEntry to remove the oldest entry when the capacity is breached.

My Question

This is a pretty standard LRU cache implementation. But one thing that I can't figure out how to do is how to be notified when the LinkedHashMap removes an entry due to it not being used recently enough.

I know I can make removeEldestEntry provide some form of notification... but is there any way to retrieve the element that is removed from the cache right when a new one is inserted (put) into the underlying map? Alternatively, is there a way to query for the last item that was removed from the cache?

John Humphreys
  • 37,047
  • 37
  • 155
  • 255

2 Answers2

2

You can get it to work with some creative use of thread local storage:

class LRUCacheLHM<K,V> extends LinkedHashMap<K,V> {

    private int capacity;

    public LRUCacheLHM(int capacity) {
        //1 extra element as add happens before remove (101), and load factor big
        //enough to avoid triggering resize.  True = keep in access order.
        super(capacity + 1, 1.1f, true);
        this.capacity = capacity;
    }
    private ThreadLocal<Map.Entry<K,V>> removed = new ThreadLocal<Map.Entry<K,V>>();
    private ThreadLocal<Boolean> report = new ThreadLocal<Boolean>();
    {
        report.set(false);
    }
    @Override
    public boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        boolean res = size() > capacity;
        if (res && report.get()) {
            removed.set(eldest);
        }
        return res;
    }
    public Map.Entry<K,V> place(K k, V v) {
        report.set(true);
        put(k, v);
        try {
            return removed.get();
        } finally {
            removed.set(null);
            report.set(false);
        }
    }

}

Demo.

The idea behind the place(K,V) method is to signal to removeEldestEntry that we would like to get the eldest entry by setting a thread-local report flag to true. When removeEldestEntry sees this flag and knows that an entry is being removed, it places the eldest entry in the report variable, which is thread-local as well.

The call to removeEldestEntry happens inside the call to the put method. After that the eldest entry is either null, or is sitting inside the report variable ready to be harvested.

Calling set(null) on removed is important to avoid lingering memory leaks.

Sergey Kalinichenko
  • 714,442
  • 84
  • 1,110
  • 1,523
  • So... you're using place() in place of put to make a version of put() capable of returning the removed value, which is pretty cool. I'm a little confused about the thread-locals though. What benefit do they add over just having remove-eldest-entry set a local member directly? I don't think the underlying linkedhashmap data structure is synchronized so it probably can't be used by multiple threads anyway. I know I'm probably overlooking something :) – John Humphreys Apr 29 '15 at 15:04
  • and sorry; edited that first comment like 4 times after posting it haha. – John Humphreys Apr 29 '15 at 15:07
  • @JohnHumphreys-w00te Yes, that's the idea. Using `put` for this would be inappropriate, because it would break `Map`'s contract. Thread-locals are there to allow for concurrency (I made them non-static for better use of generic types, but static were OK too). Without thread-locals you would end up storing the value in an instance variable, which would introduce race conditions during concurrent uses of the cache, even if you put a synchronized wrapper around it. – Sergey Kalinichenko Apr 29 '15 at 15:11
1

is there any way to retrieve the element that is removed from the cache right when a new one is inserted (put) into the underlying map?

The removeEldestEntry is notified of the entry to be removed. You can add a listener which this method calls if you want to make it dynamically configurable.

From the Javadoc

protected boolean removeEldestEntry(Map.Entry eldest)

eldest - The least recently inserted entry in the map, or if this is an access-ordered map, the least recently accessed entry. This is the entry that will be removed it this method returns true. If the map was empty prior to the put or putAll invocation resulting in this invocation, this will be the entry that was just inserted; in other words, if the map contains a single entry, the eldest entry is also the newest.

.

is there a way to query for the last item that was removed from the cache?

The last item removed has been removed, however you could have the sub-class store this entry in a field you can retrieve later.

Peter Lawrey
  • 525,659
  • 79
  • 751
  • 1,130
  • 1
    Your last point about having the sub-class store it was what I was looking for. For some reason I hadn't thought it through that far :) Thanks! – John Humphreys Apr 29 '15 at 14:55