Here is the implementation for a timed concurrent map implementation that I wrote a while ago. It delegates to an underlying ConcurrentMap
, and uses a single ReentrantLock
to synchronize writers. It is designed to use in situations where objects eventually age themselves out of cache storage.
package com.researchkitchen.map;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;/**
* A wrapper around a {@link ConcurrentMap} that expires entries from the underlying map
* after a specific period (the TTL value) has expired.
* @author winstor
*
* @param <K>
* @param <V>
*/
public final class TimedMap<K,V> {
private final ConcurrentMap<K, ExpiredObject<K,V>> map = new ConcurrentHashMap<K, ExpiredObject<K,V>>();
private final Lock writeLock = new ReentrantLock();
private static final long DEFAULT_TTL = 60000L;
private final Timer timer = new Timer("TimedMap Timer", true);
/**
* A wrapper for a underlying object that associates a {@link TimerTask} instance
* with the object.
* @author winstor
*
* @param <K> The key type K
* @param <V> The value type V
*/
class ExpiredObject<K,V> {
private final V value;
private final ExpiryTask<K> task;
private final long ttl;
public ExpiredObject(K key, V value) {
this(key, value, DEFAULT_TTL);
}
public ExpiredObject(K key, V value, long ttl) {
this.value = value;
this.task = new ExpiryTask<K>(key);
this.ttl = ttl;
timer.schedule(this.task, ttl);
}
public ExpiryTask<K> getTask() { return task; }
public V getValue() { return value; }
public long getTtl() { return ttl; }
}
/**
* A {@link TimerTask} implementation that removes its
* associated entry (identified by a key K) from the
* internal {@link Map}.
* @author winstor
*
* @param <K> The object key
*/
class ExpiryTask<K> extends TimerTask {
private final K key;
public ExpiryTask(K key) {
this.key = key;
}
public K getKey() {
return key;
}
@Override
public void run() {
System.out.println("Expiring element with key [" + key + "]");
try {
writeLock.lock();
if (map.containsKey(key))
map.remove(getKey());
}
finally {
writeLock.unlock();
}
}
}
/**
* Insert an entry into the underlying map, specifying a time-to-live
* for the element to be inserted.
* <p/>
* @param key The key of the element to be inserted.
* @param value The item to be inserted.
* @param expiry A time-to-live value (specified in milliseconds).
*/
@SuppressWarnings("unchecked")
public void put(K key, V value, long expiry) {
try {
writeLock.lock();
final ExpiredObject<K, V> object =
map.putIfAbsent(key, new ExpiredObject<K, V>(key, value, expiry));
/* Was there an existing entry for this key? If so, cancel the existing timer */
if (object != null)
object.getTask().cancel();
}
finally {
writeLock.unlock();
}
}
/**
* Insert an entry into the map with a default time-to-live value.
* @param key The key of the element to be inserted.
* @param value The item to be inserted.
*/
public void put(K key, V value) {
put(key, value, DEFAULT_TTL);
}
/**
* Retrieve the entry identified by <code>key</code>, or <code>null</code> if
* it does not exist.
* @param key The key identifying the entry to retrieve
* @return The entry corresponding to <code>key</code>, or <code>null</code>.
*/
@SuppressWarnings("unchecked")
public V get(K key) {
return (V) (map.containsKey(key) ? map.get(key).getValue() : null);
}
/**
* Clear the underlying map and cancel any outstanding expiry timers.
*/
public void clear() {
try {
writeLock.lock();
for (ExpiredObject<K, V> obj : map.values()) {
obj.getTask().cancel();
}
map.clear();
timer.purge(); // Force removal of all cancelled tasks
}
finally {
writeLock.unlock();
}
}
public boolean containsKey(K key) {
return map.containsKey(key);
}
public boolean isEmpty() {
return map.isEmpty();
}
/**
* Remov the element identified by <code>key</code> from the map, returning <code>null</code>
* if it does not exist.
* @param key The key identifying the element to remove
* @return The removed element, or null if it was not present.
*/
public V remove(K key) {
final ExpiredObject<K,V> object;
try {
writeLock.lock();
System.out.println("Removing element with key:" + key);
object = map.remove(key);
if (object != null)
object.getTask().cancel();
}
finally {
writeLock.unlock();
}
return (object == null ? null : object.getValue());
}
public int size() {
return map.size();
}
}