Last active
September 26, 2019 10:16
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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")); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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