Concurrent LRUCache?

Hi,

I'd like to know if you know of an LRU (least recently used) cache that would use java.util.concurrent.

I'm currently using the following implementation that is based on java.util.LinkedHashMap.

Do you know if there is an implementation based on java.util.concurrent.ConcurrentHashMap ?

publicclass LRUCache<K, V>extends java.util.LinkedHashMap<K, V>{

protectedint maxsize;

public LRUCache(int maxsize){

super(maxsize * 4 / 3 + 1, 0.75f,true);

this.maxsize = maxsize;

}

protectedboolean removeEldestEntry(Map.Entry eldest){

return size() > maxsize;

}

}

[1218 byte] By [urddda] at [2007-11-26 17:39:42]
# 1
I had to write my own LRU Cache implementation based on ConcurrentHashMap.
Caffeine0001a at 2007-7-9 0:07:51 > top of Java-index,Core,Core APIs...
# 2
Would you be will to share the implementation of an LRU Map that you wrote based on the ConcurrentHashMap?
b.s.kruga at 2007-7-9 0:07:51 > top of Java-index,Core,Core APIs...
# 3

I don't mind giving it out. Fill free to modify it or do anything you like with it. But, use this code at your own risk. I will not support it. The code is mostly undocumented.It uses a Timer that every 2 minutes checks the size and cleans out the old entries. You may need to add code to remove the TimerTask from the timer when the Map is no longer needed. I haven't since I've only used this class for static instances. It has two call back methods that can be overridden to find out what elements have been removed: removed() and expiredRemoved().

import java.util.AbstractCollection;

import java.util.AbstractSet;

import java.util.ArrayList;

import java.util.Arrays;

import java.util.Collection;

import java.util.Comparator;

import java.util.Enumeration;

import java.util.Iterator;

import java.util.Map;

import java.util.Set;

import java.util.Timer;

import java.util.TimerTask;

import java.util.concurrent.ConcurrentHashMap;

import java.util.concurrent.ConcurrentMap;

public class ConcurrentCacheMap<K,V> implements ConcurrentMap<K,V> {

static final int DEFAULT_MAX_ENTRIES = 100;

static final double PERCENT_CLEANUP = 0.05;

static final int DEFAULT_SEGMENTS = 16;

static final int CHECK_DURATION = 120 * 1000;

static final Timer timer = new Timer();

protected int maxEntries;

protected ConcurrentHashMap<K,ValueWrapper><V>> map;

/** Creates a new instance of CacheMap */

public ConcurrentCacheMap() {

this(DEFAULT_MAX_ENTRIES, DEFAULT_SEGMENTS);

}

public ConcurrentCacheMap(int maxEntries, int concurrencyLevel) {

map = new ConcurrentHashMap<K,ValueWrapper><V>>((maxEntries+1) * 4 / 3 + 1, 0.75f, concurrencyLevel);

this.maxEntries = maxEntries;

timer.schedule(new CacheCleaner(), CHECK_DURATION, CHECK_DURATION);

}

public boolean replace(K key, V oldValue, V newValue) {

return map.replace(key, new ValueWrapper<V>(oldValue), new ValueWrapper<V>(newValue));

}

public V replace(K key, V value) {

ValueWrapper<V> wrapper = map.replace(key, new ValueWrapper<V>(value));

return checkRemove(key, wrapper);

}

public V putIfAbsent(K key, V value) {

ValueWrapper<V> wrapper = map.putIfAbsent(key, new ValueWrapper<V>(value));

return checkRemove(key, wrapper);

}

public V put(K key, V value) {

ValueWrapper<V> wrapper = map.put(key, new ValueWrapper<V>(value));

return checkRemove(key, wrapper);

}

public void putAll(Map<? extends K,? extends V> t) {

for (Entry<? extends K,? extends V> entry : t.entrySet()) {

put(entry.getKey(), entry.getValue());

}

}

public V remove(Object key) {

if (key == null) {

return null;

}

ValueWrapper<V> wrapper = map.remove(key);

return checkRemove(key, wrapper);

}

public boolean remove(Object key, Object value) {

ValueWrapper<Object> wrapper = new ValueWrapper<Object>(value);

boolean rval = map.remove(key, wrapper);

if (rval) {

removed(key, (V)value);

}

return rval;

}

public V get(Object key) {

ValueWrapper<V> wrapper = map.get(key);

//don't increment age call .value

return wrapper == null ? null : wrapper.getValue();

}

public boolean contains(Object value) {

return map.contains(new ValueWrapper<Object>(value));

}

public boolean containsKey(Object key) {

return map.containsKey(key);

}

public boolean containsValue(Object value) {

return map.containsValue(new ValueWrapper<Object>(value));

}

public Collection<V> values() {

return new CollectionValueWrapper();

}

public Enumeration<V> elements() {

return new ValueIter();

}

public Enumeration<K> keys() {

return new KeyIter();

}

public boolean equals(Object obj) {

if (obj instanceof ConcurrentCacheMap) {

return ((ConcurrentCacheMap)obj).map.equals(map);

}

return false;

}

public Set<K> keySet() {

return map.keySet();

}

public int size() {

return map.size();

}

public boolean isEmpty() {

return map.isEmpty();

}

public void clear() {

map.clear();

}

public String toString() {

return map.toString();

}

public int hashCode() {

return map.hashCode();

}

public Set<Entry><K, V>> entrySet() {

return new EntrySetWrapper();

}

final V checkRemove(Object key, ValueWrapper<V> wrapper) {

if (wrapper == null){

return null;

}

V value = wrapper.value;

removed(key, value);

return value;

}

protected void removed(Object key, V value) {

}

protected void expiredRemoved(Object key, V value) {

//System.out.println(value + " " + value.getClass().getName());

}

public int getMaxEntries() {

return maxEntries;

}

public void setMaxEntries(int maxEntries) {

this.maxEntries = maxEntries;

}

static boolean eq(Object o1, Object o2) {

return (o1 == null ? o2 == null : o1.equals(o2));

}

public final static class ValueWrapper<V> {

// I just need approximate values for ageCounter so I don't care if

// ageCounter is concurrently updated and therefore age is skipped or

// duplicated amoung ValueWrapper instances. This should not be a

// problem since I clean groups of old entries.

// For small caches this may be a problem, but then so would be a 2 minute

// cleanup. So if this is a concern then change ageCounter to use an AtomicLong or

// a volatile and make schedule time variable.

static long ageCounter;

public V value;

public long age;

ValueWrapper(V value) {

this.value = value;

age = ageCounter++;

}

public boolean equals(Object o) {

if (!(o instanceof ValueWrapper)) {

return false;

}

return eq(value,((ValueWrapper)o).value);

}

public int hashCode() {

return value.hashCode();

}

public V getValue() {

age = ageCounter++;

return value;

}

public String toString() {

return value == null ? "null " : value.toString();

}

}

abstract class Iter {

Iterator<Entry><K,ValueWrapper><V>>> iter;

Entry<K,ValueWrapper><V>> current;

Iter() {

iter = map.entrySet().iterator();

}

public void remove() {

ConcurrentCacheMap.this.remove(current.getKey());

}

public boolean hasMoreElements() {

return iter.hasNext();

}

public boolean hasNext() {

return iter.hasNext();

}

final Entry<K,ValueWrapper><V>> advance() {

return (current = iter.next());

}

}

final class KeyIter extends Iter implements Iterator<K>, Enumeration<K> {

public K nextElement() { return next(); }

public K next() { return advance().getKey();}

}

final class ValueIter extends Iter implements Iterator<V>, Enumeration<V> {

public V nextElement() { return next(); }

public V next() {

ValueWrapper<V> valueWrapper = advance().getValue();

return valueWrapper == null ? null : valueWrapper.value;

}

}

final class EntryIter extends Iter implements Iterator<Entry><K, V>>, Enumeration<Entry><K, V>> {

public Entry<K, V> nextElement() { return next(); }

public Entry<K, V> next() { return new SimpleEntry<K,V>(advance());}

}

final class CollectionValueWrapper extends AbstractCollection<V> {

public Iterator<V> iterator() {

return new ValueIter();

}

public int size() {

return ConcurrentCacheMap.this.size();

}

public boolean contains(Object o) {

return ConcurrentCacheMap.this.containsValue(o);

}

public void clear() {

ConcurrentCacheMap.this.clear();

}

public Object[] toArray() {

Collection<V> c = new ArrayList<V>();

for (Iterator<V> i = iterator(); i.hasNext(); ) {

c.add(i.next());

}

return c.toArray();

}

public <T> T[] toArray(T[] a) {

Collection<V> c = new ArrayList<V>();

for (Iterator<V> i = iterator(); i.hasNext(); ) {

c.add(i.next());

}

return c.toArray(a);

}

}

final class EntrySetWrapper extends AbstractSet<Entry><K, V>> {

public Iterator<Entry><K, V>> iterator() {

return new EntryIter();

}

public boolean remove(Object o) {

if (!(o instanceof Map.Entry)) {

return false;

}

Map.Entry<K,V> entry = (Map.Entry<K,V>)o;

return ConcurrentCacheMap.this.remove(entry.getKey(), entry.getValue());

}

public boolean contains(Object o) {

if (!(o instanceof Map.Entry)) {

return false;

}

Map.Entry<K,V> e = (Map.Entry<K,V>)o;

V v = ConcurrentCacheMap.this.get(e.getKey());

return v != null && v.equals(e.getValue());

}

public int size() {

return ConcurrentCacheMap.this.size();

}

public void clear() {

ConcurrentCacheMap.this.clear();

}

}

final static class SimpleEntry<K,V> implements Entry<K,V> {

Entry<K, ValueWrapper><V>> entry;

SimpleEntry(Entry<K, ValueWrapper><V>> entry) {

this.entry = entry;

}

public V setValue(V value) {

//don't increment age call .value

return entry.setValue(new ValueWrapper<V>(value)).value;

}

public K getKey() {

return entry.getKey();

}

public V getValue() {

ValueWrapper<V> valueWrapper = entry.getValue();

return valueWrapper == null ? null : valueWrapper.value;

}

public boolean equals(Object o) {

if (!(o instanceof Map.Entry)) {

return false;

}

Entry e = (Entry)o;

return eq(getKey(), e.getKey()) && eq(getValue(), e.getValue());

}

public int hashCode() {

K key = getKey();

V value = getValue();

return ((key == null) ? 0 :key.hashCode()) ^

((value == null)? 0 : value.hashCode());

}

public String toString() {

return getKey() + "=" + getValue();

}

}

final class CacheCleaner extends TimerTask {

public void run() {

try {

clean();

} catch (Throwable th) {

th.printStackTrace();

}

}

}

public void clean() {

int maxEntries = getMaxEntries();

int size = size();

if (size > maxEntries) {

int removeCount = (int)(maxEntries * PERCENT_CLEANUP) + (size - maxEntries);

System.out.println("Starting to clean " + removeCount + " old items from cache(" + maxEntries +") with " + size + " items.");

long start = System.currentTimeMillis();

//Size can and will likely change during coping to an array inside of toArray

//and and new array will likely be need to be created. So to prevent three

//large arrays from being allocated. Just allocate an empty array for type purposes.

Entry[] allItem = new Entry[0];

allItem = map.entrySet().toArray(allItem);

Arrays.sort(allItem, new EntrySetComparator<K,V>());

int upper = allItem.length;

for (int i=0; i<removeCount; i++) {

Entry><K, ValueWrapper><V>> entry = (Entry<K, ValueWrapper><V>>)allItem[i];

ValueWrapper<V> valueWrapper = entry.getValue();

remove(entry.getKey());

if (valueWrapper != null) {

expiredRemoved(entry.getKey(), valueWrapper.value);

}

}

System.out.println("Cache(" + maxEntries +") took " + (System.currentTimeMillis() - start) + " ms to clean " + removeCount + " items");

}

}

static final class EntrySetComparator<K,V> implements Comparator<Entry><K, ValueWrapper><V>>> {

public int compare(Entry<K, ValueWrapper><V>> o1, Entry<K, ValueWrapper><V>> o2) {

ValueWrapper<V> valueWrapper1 = o1.getValue();

long age1 = valueWrapper1 != null ?

valueWrapper1.age :

-1;

ValueWrapper<V> valueWrapper2 = o2.getValue();

long age2 = valueWrapper2 != null ?

valueWrapper2.age :

-1;

int rval= age1 > age2 ?

1 :

age1 == age2 ?

0 :

-1;

return rval;

}

}

//perform a simple santity test.

public static void main(String ...args) {

final ConcurrentCacheMap<Integer,String> cache = new ConcurrentCacheMap<Integer,String>(300000,16);

new Thread() {

public void run() {

int negCounter = -1;

for (;;) {

cache.put(negCounter, Integer.toString(negCounter));

negCounter--;

try {

Thread.sleep(10);

} catch (InterruptedException ex){

Thread.currentThread().interrupt();

return;

}

}

}

}.start();

int counter = 0;

for (int i=0; i<301000; i++) {

counter++;

cache.put(counter,Integer.toString(counter));

}

for (int i=0; i<20; i++) {

cache.clean();

for (int j=0; j<16000; j++) {

counter++;

cache.put(counter,Integer.toString(counter));

}

}

System.exit(0);

}

}

Caffeine0001a at 2007-7-9 0:07:51 > top of Java-index,Core,Core APIs...