package com.martin.cache;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.io.Externalizable;
import java.util.Map;
import java.util.Set;
import java.util.HashSet;
import java.util.Iterator;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledThreadPoolExecutor;
/**
* Thread safe simple parameterized map cache with eviction reaper,
* generally non locking on retrieval + internal <V> wrapper class.
* @author MartinZ 12/07/2009
* @param K key type
* @param V value type
*/
public final class MapCache< K , V > {
private class Reaper implements Runnable {
public void run() {
evict();
}
}
public interface EvictionListener< K > {
void evicted( K key );
}
/////////////////////////////////////////////////////////////////////
public static final long DEFAULT_EVICT_INTERVAL_MS = (30 * 1000);
public static final long DEFULT_15MINUTES_CACHE_MS = (15 * 60 * 1000);
/////////////////////////////////////////////////////////////////////
private final ConcurrentHashMap< K , Value<V> > map;
private final ScheduledThreadPoolExecutor timer;
private final HashSet<EvictionListener<K>> listeners;
private final long interval;
public MapCache() {
this(DEFAULT_EVICT_INTERVAL_MS);
}
public MapCache( Long tm ) {
interval = (tm==null)?DEFAULT_EVICT_INTERVAL_MS:tm;
map = new ConcurrentHashMap<K,Value<V>>();
timer = new ScheduledThreadPoolExecutor(1);
listeners = new HashSet<EvictionListener<K>>();
}
public void addEvictionListener(EvictionListener<K> l) {
listeners.add(l);
}
public void removeEvictionListener(EvictionListener<K> l) {
listeners.remove(l);
}
public int getSize() {
return map.size();
}
public void clear() {
map.clear();
}
public void start() {
timer.scheduleWithFixedDelay(
new Reaper(),0,interval,TimeUnit.MILLISECONDS
);
}
public void stop() {
timer.shutdown();
}
/**
* @param key != null
* @param val != null
* uses default 15 minute time to live since last get
*/
public V put( K key , V val ) {
return put( key , val , DEFULT_15MINUTES_CACHE_MS );
}
/**
* @param key != null
* @param val != null
* @param time ms to keep an entry alive. 0 = never evict.
*/
public V put( K key , V val , long tm ) {
if(key == null || val == null ) return null;
Value<V> value = new Value<V>( val , tm );
Value<V> retval = map.put( key , value );
return (retval != null ? retval.value : null);
}
public V get(K key) {
if(key == null) return null;
Value<V> val = map.get(key);
if(val == null) return null;
val.refreshTimestamp();
return val.value;
}
public Value<V> getEntry( K key ) {
if(key == null) return null;
Value<V> val = map.get(key);
if(val == null) return null;
val.refreshTimestamp();
return val;
}
public V remove(K key) {
Value<V> val = map.remove(key);
return (val != null ? val.value : null);
}
public Set<Map.Entry<K,Value<V>>> entrySet() {
return map.entrySet();
}
private void evict() {
for( Iterator<Map.Entry<K,Value<V>>> it =
map.entrySet().iterator(); it.hasNext();) {
Map.Entry<K,Value<V>> entry = it.next();
Value<V> val = entry.getValue();
if(val != null) {
if( (val.timeout > 0) &&
(System.currentTimeMillis() >
(val.insertion + val.timeout))) {
it.remove(); // rm from iterator
notifyListeners( entry.getKey() );
}
}
}
}
private void notifyListeners( K key ) {
for(EvictionListener<K> l: listeners) {
try { l.evicted(key);
} catch(Throwable t) { }
}
}
public String toString() {
StringBuilder sb=new StringBuilder();
for(Map.Entry<K,Value<V>> entry: map.entrySet()) {
Value<V> val=entry.getValue();
sb.append(entry.getKey()).append(": ");
sb.append(entry.getValue().getValue());
sb.append(" (expiration tm.ms: ");
sb.append(val.getTimeout()).append(")\n");
}
return sb.toString();
}
///////////////////// internal wrapper ////////////////////
public static class Value< V > implements Externalizable {
private V value;
private long timeout;
private transient long insertion=System.currentTimeMillis();
private static final long serialVersionUID = 1100000000001L;
public Value( V value , long timeout ) {
this.value = value;
this.timeout = timeout;
}
public V getValue() { return value; }
public long getTimeout() { return timeout; }
public long getInsertionTime() { return insertion; }
public void refreshTimestamp() {
if( timeout > 0 )
insertion=System.currentTimeMillis();
}
public void writeExternal(ObjectOutput out)
throws IOException {
out.writeLong(timeout);
out.writeObject(value);
}
@SuppressWarnings("unchecked")
public void readExternal(ObjectInput in)
throws IOException,ClassNotFoundException {
timeout = in.readLong();
value = (V) in.readObject();
insertion = System.currentTimeMillis();
}
}
}
Monday, December 7, 2009
Thread-safe MapCache with Reaper
Sometimes when writing middle-ware applications there's a need for a simple thread safe parameterized mem map cache with eviction reaper, generally non locking on retrieval and with internal wrapper class. By reading the latest java.util.concurrent api doc there is a rich list of collections built with concurrency in mind, I could not find one with an eviction reaper built-in so today I assembled one and here's a reference implementation I'd like to share.
Subscribe to:
Post Comments (Atom)

2 comments:
Martin,
I liked your post, but the code was really tough to read.
regards,
-Frank
I agree, the SyntaxHighlighter I normally use choked with [Generics] so I had to use a simpler one. cheers
Post a Comment