Skip to content

Instantly share code, notes, and snippets.

@bvolpato
Last active September 26, 2019 10:16
Show Gist options
  • Save bvolpato/632a72622fff352fd875db0747a19b1a to your computer and use it in GitHub Desktop.
Save bvolpato/632a72622fff352fd875db0747a19b1a to your computer and use it in GitHub Desktop.
SelfExpiringHashMap - a Java Map which entries expire automatically after a given time; it uses a DelayQueue internally.
import java.util.Collection;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
/**
* A thread-safe implementation of a HashMap which entries expires after the specified life time.
* The life-time can be defined on a per-key basis, or using a default one, that is passed to the
* constructor.
*
* @author Pierantonio Cangianiello
* @param <K> the Key type
* @param <V> the Value type
*/
public class SelfExpiringHashMap<K, V> implements SelfExpiringMap<K, V> {
private final Map<K, V> internalMap;
private final Map<K, ExpiringKey<K>> expiringKeys;
/**
* Holds the map keys using the given life time for expiration.
*/
private final DelayQueue<ExpiringKey> delayQueue = new DelayQueue<ExpiringKey>();
/**
* The default max life time in milliseconds.
*/
private final long maxLifeTimeMillis;
public SelfExpiringHashMap() {
internalMap = new ConcurrentHashMap<K, V>();
expiringKeys = new WeakHashMap<K, ExpiringKey<K>>();
this.maxLifeTimeMillis = Long.MAX_VALUE;
}
public SelfExpiringHashMap(long defaultMaxLifeTimeMillis) {
internalMap = new ConcurrentHashMap<K, V>();
expiringKeys = new WeakHashMap<K, ExpiringKey<K>>();
this.maxLifeTimeMillis = defaultMaxLifeTimeMillis;
}
public SelfExpiringHashMap(long defaultMaxLifeTimeMillis, int initialCapacity) {
internalMap = new ConcurrentHashMap<K, V>(initialCapacity);
expiringKeys = new WeakHashMap<K, ExpiringKey<K>>(initialCapacity);
this.maxLifeTimeMillis = defaultMaxLifeTimeMillis;
}
public SelfExpiringHashMap(long defaultMaxLifeTimeMillis, int initialCapacity, float loadFactor) {
internalMap = new ConcurrentHashMap<K, V>(initialCapacity, loadFactor);
expiringKeys = new WeakHashMap<K, ExpiringKey<K>>(initialCapacity, loadFactor);
this.maxLifeTimeMillis = defaultMaxLifeTimeMillis;
}
/**
* {@inheritDoc}
*/
@Override
public int size() {
cleanup();
return internalMap.size();
}
/**
* {@inheritDoc}
*/
@Override
public boolean isEmpty() {
cleanup();
return internalMap.isEmpty();
}
/**
* {@inheritDoc}
*/
@Override
public boolean containsKey(Object key) {
cleanup();
return internalMap.containsKey((K) key);
}
/**
* {@inheritDoc}
*/
@Override
public boolean containsValue(Object value) {
cleanup();
return internalMap.containsValue((V) value);
}
@Override
public V get(Object key) {
cleanup();
renewKey((K) key);
return internalMap.get((K) key);
}
/**
* {@inheritDoc}
*/
@Override
public V put(K key, V value) {
return this.put(key, value, maxLifeTimeMillis);
}
/**
* {@inheritDoc}
*/
@Override
public V put(K key, V value, long lifeTimeMillis) {
cleanup();
ExpiringKey delayedKey = new ExpiringKey(key, lifeTimeMillis);
ExpiringKey oldKey = expiringKeys.put((K) key, delayedKey);
if(oldKey != null) {
expireKey(oldKey);
expiringKeys.put((K) key, delayedKey);
}
delayQueue.offer(delayedKey);
return internalMap.put(key, value);
}
/**
* {@inheritDoc}
*/
@Override
public V remove(Object key) {
V removedValue = internalMap.remove((K) key);
expireKey(expiringKeys.remove((K) key));
return removedValue;
}
/**
* Not supported.
*/
@Override
public void putAll(Map<? extends K, ? extends V> m) {
throw new UnsupportedOperationException();
}
/**
* {@inheritDoc}
*/
@Override
public boolean renewKey(K key) {
ExpiringKey<K> delayedKey = expiringKeys.get((K) key);
if (delayedKey != null) {
delayedKey.renew();
return true;
}
return false;
}
private void expireKey(ExpiringKey<K> delayedKey) {
if (delayedKey != null) {
delayedKey.expire();
cleanup();
}
}
/**
* {@inheritDoc}
*/
@Override
public void clear() {
delayQueue.clear();
expiringKeys.clear();
internalMap.clear();
}
/**
* Not supported.
*/
@Override
public Set<K> keySet() {
throw new UnsupportedOperationException();
}
/**
* Not supported.
*/
@Override
public Collection<V> values() {
throw new UnsupportedOperationException();
}
/**
* Not supported.
*/
@Override
public Set<Entry<K, V>> entrySet() {
throw new UnsupportedOperationException();
}
private void cleanup() {
ExpiringKey<K> delayedKey = delayQueue.poll();
while (delayedKey != null) {
internalMap.remove(delayedKey.getKey());
expiringKeys.remove(delayedKey.getKey());
delayedKey = delayQueue.poll();
}
}
private class ExpiringKey<K> implements Delayed {
private long startTime = System.currentTimeMillis();
private final long maxLifeTimeMillis;
private final K key;
public ExpiringKey(K key, long maxLifeTimeMillis) {
this.maxLifeTimeMillis = maxLifeTimeMillis;
this.key = key;
}
public K getKey() {
return key;
}
/**
* {@inheritDoc}
*/
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final ExpiringKey<K> other = (ExpiringKey<K>) obj;
if (this.key != other.key && (this.key == null || !this.key.equals(other.key))) {
return false;
}
return true;
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
int hash = 7;
hash = 31 * hash + (this.key != null ? this.key.hashCode() : 0);
return hash;
}
/**
* {@inheritDoc}
*/
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(getDelayMillis(), TimeUnit.MILLISECONDS);
}
private long getDelayMillis() {
return (startTime + maxLifeTimeMillis) - System.currentTimeMillis();
}
public void renew() {
startTime = System.currentTimeMillis();
}
public void expire() {
startTime = System.currentTimeMillis() - maxLifeTimeMillis - 1;
}
/**
* {@inheritDoc}
*/
@Override
public int compareTo(Delayed that) {
return Long.compare(this.getDelayMillis(), ((ExpiringKey) that).getDelayMillis());
}
}
}
import it.pcan.util.map.SelfExpiringHashMap;
import it.pcan.util.map.SelfExpiringMap;
import static org.junit.Assert.*;
import org.junit.Test;
/**
*
* @author Pierantonio Cangianiello
*/
public class SelfExpiringHashMapTests {
private final static int SLEEP_MULTIPLIER = 10;
@Test
public void basicGetTest() throws InterruptedException {
SelfExpiringMap<String, String> map = new SelfExpiringHashMap<String, String>();
map.put("a", "b", 2 * SLEEP_MULTIPLIER);
Thread.sleep(1 * SLEEP_MULTIPLIER);
assertEquals("b", map.get("a"));
}
@Test
public void basicExpireTest() throws InterruptedException {
SelfExpiringMap<String, String> map = new SelfExpiringHashMap<String, String>();
map.put("a", "b", 2 * SLEEP_MULTIPLIER);
Thread.sleep(3 * SLEEP_MULTIPLIER);
assertNull(map.get("a"));
}
@Test
public void basicRenewTest() throws InterruptedException {
SelfExpiringMap<String, String> map = new SelfExpiringHashMap<String, String>();
map.put("a", "b", 3 * SLEEP_MULTIPLIER);
Thread.sleep(2 * SLEEP_MULTIPLIER);
map.renewKey("a");
Thread.sleep(2 * SLEEP_MULTIPLIER);
assertEquals("b", map.get("a"));
}
@Test
public void getRenewTest() throws InterruptedException {
SelfExpiringMap<String, String> map = new SelfExpiringHashMap<String, String>();
map.put("a", "b", 3 * SLEEP_MULTIPLIER);
Thread.sleep(2 * SLEEP_MULTIPLIER);
assertEquals("b", map.get("a"));
Thread.sleep(2 * SLEEP_MULTIPLIER);
assertEquals("b", map.get("a"));
}
@Test
public void multiplePutThenRemoveTest() throws InterruptedException {
SelfExpiringMap<String, String> map = new SelfExpiringHashMap<String, String>();
map.put("a", "b", 2 * SLEEP_MULTIPLIER);
Thread.sleep(1 * SLEEP_MULTIPLIER);
map.put("a", "c", 2 * SLEEP_MULTIPLIER);
Thread.sleep(1 * SLEEP_MULTIPLIER);
map.put("a", "d", 400 * SLEEP_MULTIPLIER);
Thread.sleep(2 * SLEEP_MULTIPLIER);
assertEquals("d", map.remove("a"));
}
@Test
public void multiplePutThenGetTest() throws InterruptedException {
SelfExpiringMap<String, String> map = new SelfExpiringHashMap<String, String>();
map.put("a", "b", 2 * SLEEP_MULTIPLIER);
Thread.sleep(1 * SLEEP_MULTIPLIER);
map.put("a", "c", 2 * SLEEP_MULTIPLIER);
Thread.sleep(1 * SLEEP_MULTIPLIER);
map.put("a", "d", 400 * SLEEP_MULTIPLIER);
Thread.sleep(2 * SLEEP_MULTIPLIER);
assertEquals("d", map.get("a"));
}
}
import java.util.Map;
/**
*
* @author Pierantonio Cangianiello
* @param <K> the Key type
* @param <V> the Value type
*/
public interface SelfExpiringMap<K, V> extends Map<K, V> {
/**
* Renews the specified key, setting the life time to the initial value.
*
* @param key
* @return true if the key is found, false otherwise
*/
public boolean renewKey(K key);
/**
* Associates the given key to the given value in this map, with the specified life
* times in milliseconds.
*
* @param key
* @param value
* @param lifeTimeMillis
* @return a previously associated object for the given key (if exists).
*/
public V put(K key, V value, long lifeTimeMillis);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment