load(K key);
}
\ No newline at end of file
diff --git a/src/main/java/com/io/yutian/elementoriginlib/expiringmap/ExpiringMap.java b/src/main/java/com/io/yutian/elementoriginlib/expiringmap/ExpiringMap.java
index 475bfa2..89f8376 100644
--- a/src/main/java/com/io/yutian/elementoriginlib/expiringmap/ExpiringMap.java
+++ b/src/main/java/com/io/yutian/elementoriginlib/expiringmap/ExpiringMap.java
@@ -1,55 +1,35 @@
package com.io.yutian.elementoriginlib.expiringmap;
+import com.io.yutian.elementoriginlib.expiringmap.internal.Assert;
+import com.io.yutian.elementoriginlib.expiringmap.internal.NamedThreadFactory;
+
import java.lang.ref.WeakReference;
-import java.util.AbstractCollection;
-import java.util.AbstractSet;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.NoSuchElementException;
-import java.util.Set;
-import java.util.SortedSet;
-import java.util.concurrent.ConcurrentMap;
-import java.util.concurrent.ConcurrentSkipListSet;
-import java.util.concurrent.CopyOnWriteArrayList;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ThreadFactory;
-import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
+import java.util.*;
+import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
-import com.io.yutian.elementoriginlib.expiringmap.internal.Assert;
-import com.io.yutian.elementoriginlib.expiringmap.internal.NamedThreadFactory;
-
/**
* A thread-safe map that expires entries. Optional features include expiration policies, variable entry expiration,
* lazy entry loading, and expiration listeners.
- *
+ *
*
* Entries are tracked by expiration time and expired by a single thread.
- *
+ *
*
* Expiration listeners are called synchronously as entries are expired and block write operations to the map until they
* completed. Asynchronous expiration listeners are called on a separate thread pool and do not block map operations.
- *
+ *
*
* When variable expiration is disabled (default), put/remove operations have a time complexity O(1) . When
* variable expiration is enabled, put/remove operations have time complexity of O(log n) .
- *
+ *
*
* Example usages:
- *
+ *
*
* {@code
* Map map = ExpiringMap.create();
@@ -69,115 +49,332 @@ import com.io.yutian.elementoriginlib.expiringmap.internal.NamedThreadFactory;
* .build();
* }
*
- *
- * @author Jonathan Halterman
+ *
* @param Key type
* @param Value type
+ * @author Jonathan Halterman
*/
public class ExpiringMap implements ConcurrentMap {
- static volatile ScheduledExecutorService EXPIRER;
- static volatile ThreadPoolExecutor LISTENER_SERVICE;
- static ThreadFactory THREAD_FACTORY;
-
- List> expirationListeners;
- List> asyncExpirationListeners;
- private AtomicLong expirationNanos;
- private int maxSize;
- private final AtomicReference expirationPolicy;
- private final EntryLoader super K, ? extends V> entryLoader;
- private final ExpiringEntryLoader super K, ? extends V> expiringEntryLoader;
- private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
- private final Lock readLock = readWriteLock.readLock();
- private final Lock writeLock = readWriteLock.writeLock();
- /** Guarded by "readWriteLock" */
- private final EntryMap entries;
- private final boolean variableExpiration;
-
- /**
- * Sets the {@link ThreadFactory} that is used to create expiration and listener callback threads for all ExpiringMap
- * instances.
- *
- * @param threadFactory
- * @throws NullPointerException if {@code threadFactory} is null
- */
- public static void setThreadFactory(ThreadFactory threadFactory) {
- THREAD_FACTORY = Assert.notNull(threadFactory, "threadFactory");
- }
-
- /**
- * Creates a new instance of ExpiringMap.
- *
- * @param builder The map builder
- */
- private ExpiringMap(final Builder builder) {
- if (EXPIRER == null) {
- synchronized (ExpiringMap.class) {
- if (EXPIRER == null) {
- EXPIRER = Executors.newSingleThreadScheduledExecutor(
- THREAD_FACTORY == null ? new NamedThreadFactory("ExpiringMap-Expirer") : THREAD_FACTORY);
- }
- }
- }
-
- if (LISTENER_SERVICE == null && builder.asyncExpirationListeners != null)
- initListenerService();
-
- variableExpiration = builder.variableExpiration;
- entries = variableExpiration ? new EntryTreeHashMap() : new EntryLinkedHashMap();
- if (builder.expirationListeners != null)
- expirationListeners = new CopyOnWriteArrayList>(builder.expirationListeners);
- if (builder.asyncExpirationListeners != null)
- asyncExpirationListeners = new CopyOnWriteArrayList>(builder.asyncExpirationListeners);
- expirationPolicy = new AtomicReference(builder.expirationPolicy);
- expirationNanos = new AtomicLong(TimeUnit.NANOSECONDS.convert(builder.duration, builder.timeUnit));
- maxSize = builder.maxSize;
- entryLoader = builder.entryLoader;
- expiringEntryLoader = builder.expiringEntryLoader;
- }
-
- /**
- * Builds ExpiringMap instances. Defaults to ExpirationPolicy.CREATED, expiration of 60 TimeUnit.SECONDS and
- * a maxSize of Integer.MAX_VALUE.
- */
- public static final class Builder {
- private ExpirationPolicy expirationPolicy = ExpirationPolicy.CREATED;
- private List> expirationListeners;
- private List> asyncExpirationListeners;
- private TimeUnit timeUnit = TimeUnit.SECONDS;
- private boolean variableExpiration;
- private long duration = 60;
- private int maxSize = Integer.MAX_VALUE;
- private EntryLoader entryLoader;
- private ExpiringEntryLoader expiringEntryLoader;
-
+ static volatile ScheduledExecutorService EXPIRER;
+ static volatile ThreadPoolExecutor LISTENER_SERVICE;
+ static ThreadFactory THREAD_FACTORY;
+ private final AtomicReference expirationPolicy;
+ private final EntryLoader super K, ? extends V> entryLoader;
+ private final ExpiringEntryLoader super K, ? extends V> expiringEntryLoader;
+ private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
+ private final Lock readLock = readWriteLock.readLock();
+ private final Lock writeLock = readWriteLock.writeLock();
/**
- * Creates a new Builder object.
+ * Guarded by "readWriteLock"
*/
- private Builder() {
+ private final EntryMap entries;
+ private final boolean variableExpiration;
+ List> expirationListeners;
+ List> asyncExpirationListeners;
+ private AtomicLong expirationNanos;
+ private int maxSize;
+
+ /**
+ * Creates a new instance of ExpiringMap.
+ *
+ * @param builder The map builder
+ */
+ private ExpiringMap(final Builder builder) {
+ if (EXPIRER == null) {
+ synchronized (ExpiringMap.class) {
+ if (EXPIRER == null) {
+ EXPIRER = Executors.newSingleThreadScheduledExecutor(
+ THREAD_FACTORY == null ? new NamedThreadFactory("ExpiringMap-Expirer") : THREAD_FACTORY);
+ }
+ }
+ }
+
+ if (LISTENER_SERVICE == null && builder.asyncExpirationListeners != null)
+ initListenerService();
+
+ variableExpiration = builder.variableExpiration;
+ entries = variableExpiration ? new EntryTreeHashMap() : new EntryLinkedHashMap();
+ if (builder.expirationListeners != null)
+ expirationListeners = new CopyOnWriteArrayList>(builder.expirationListeners);
+ if (builder.asyncExpirationListeners != null)
+ asyncExpirationListeners = new CopyOnWriteArrayList>(builder.asyncExpirationListeners);
+ expirationPolicy = new AtomicReference(builder.expirationPolicy);
+ expirationNanos = new AtomicLong(TimeUnit.NANOSECONDS.convert(builder.duration, builder.timeUnit));
+ maxSize = builder.maxSize;
+ entryLoader = builder.entryLoader;
+ expiringEntryLoader = builder.expiringEntryLoader;
}
/**
- * Builds and returns an expiring map.
- *
- * @param Key type
- * @param Value type
+ * Sets the {@link ThreadFactory} that is used to create expiration and listener callback threads for all ExpiringMap
+ * instances.
+ *
+ * @param threadFactory
+ * @throws NullPointerException if {@code threadFactory} is null
+ */
+ public static void setThreadFactory(ThreadFactory threadFactory) {
+ THREAD_FACTORY = Assert.notNull(threadFactory, "threadFactory");
+ }
+
+ /**
+ * Creates an ExpiringMap builder.
+ *
+ * @return New ExpiringMap builder
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Creates a new instance of ExpiringMap with ExpirationPolicy.CREATED and an expiration of 60 seconds.
*/
@SuppressWarnings("unchecked")
- public ExpiringMap build() {
- return new ExpiringMap((Builder) this);
+ public static ExpiringMap create() {
+ return new ExpiringMap((Builder) ExpiringMap.builder());
+ }
+
+ private static Entry mapEntryFor(final ExpiringEntry entry) {
+ return new Entry() {
+ @Override
+ public K getKey() {
+ return entry.key;
+ }
+
+ @Override
+ public V getValue() {
+ return entry.value;
+ }
+
+ @Override
+ public V setValue(V value) {
+ throw new UnsupportedOperationException();
+ }
+ };
}
/**
- * Sets the default map entry expiration.
- *
- * @param duration the length of time after an entry is created that it should be removed
- * @param timeUnit the unit that {@code duration} is expressed in
- * @throws NullPointerException if {@code timeUnit} is null
+ * Adds an expiration listener.
+ *
+ * @param listener to add
+ * @throws NullPointerException if {@code listener} is null
*/
- public Builder expiration(long duration, TimeUnit timeUnit) {
- this.duration = duration;
- this.timeUnit = Assert.notNull(timeUnit, "timeUnit");
- return this;
+ public synchronized void addExpirationListener(ExpirationListener listener) {
+ Assert.notNull(listener, "listener");
+ if (expirationListeners == null)
+ expirationListeners = new CopyOnWriteArrayList>();
+ expirationListeners.add(listener);
+ }
+
+ /**
+ * Adds an asynchronous expiration listener.
+ *
+ * @param listener to add
+ * @throws NullPointerException if {@code listener} is null
+ */
+ public synchronized void addAsyncExpirationListener(ExpirationListener listener) {
+ Assert.notNull(listener, "listener");
+ if (asyncExpirationListeners == null)
+ asyncExpirationListeners = new CopyOnWriteArrayList>();
+ asyncExpirationListeners.add(listener);
+ // If asyncListener was not added on Builder, LISTENER_SERVICE was not initialized and remain null
+ if (LISTENER_SERVICE == null)
+ initListenerService();
+ }
+
+ @Override
+ public void clear() {
+ writeLock.lock();
+ try {
+ for (ExpiringEntry entry : entries.values())
+ entry.cancel();
+ entries.clear();
+ } finally {
+ writeLock.unlock();
+ }
+ }
+
+ @Override
+ public boolean containsKey(Object key) {
+ readLock.lock();
+ try {
+ return entries.containsKey(key);
+ } finally {
+ readLock.unlock();
+ }
+ }
+
+ @Override
+ public boolean containsValue(Object value) {
+ readLock.lock();
+ try {
+ return entries.containsValue(value);
+ } finally {
+ readLock.unlock();
+ }
+ }
+
+ /**
+ * Returns a copy of the map's entries, which can be iterated over safely by multiple threads.
+ *
+ * @return Copied set of map entries.
+ */
+ @Override
+ public Set> entrySet() {
+ return new AbstractSet>() {
+ @Override
+ public void clear() {
+ ExpiringMap.this.clear();
+ }
+
+ @Override
+ public boolean contains(Object entry) {
+ if (!(entry instanceof Map.Entry))
+ return false;
+ Entry, ?> e = (Entry, ?>) entry;
+ return containsKey(e.getKey());
+ }
+
+ @Override
+ public Iterator> iterator() {
+ return (entries instanceof EntryLinkedHashMap) ? ((EntryLinkedHashMap) entries).new EntryIterator()
+ : ((EntryTreeHashMap) entries).new EntryIterator();
+ }
+
+ @Override
+ public boolean remove(Object entry) {
+ if (entry instanceof Map.Entry) {
+ Entry, ?> e = (Entry, ?>) entry;
+ return ExpiringMap.this.remove(e.getKey()) != null;
+ }
+ return false;
+ }
+
+ @Override
+ public int size() {
+ return ExpiringMap.this.size();
+ }
+ };
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ readLock.lock();
+ try {
+ return entries.equals(obj);
+ } finally {
+ readLock.unlock();
+ }
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public V get(Object key) {
+ ExpiringEntry entry = getEntry(key);
+
+ if (entry == null) {
+ return load((K) key);
+ } else if (ExpirationPolicy.ACCESSED.equals(entry.expirationPolicy.get()))
+ resetEntry(entry, false);
+
+ return entry.getValue();
+ }
+
+ private V load(K key) {
+ if (entryLoader == null && expiringEntryLoader == null)
+ return null;
+
+ writeLock.lock();
+ try {
+ // Double check for entry
+ ExpiringEntry entry = getEntry(key);
+ if (entry != null)
+ return entry.getValue();
+
+ if (entryLoader != null) {
+ V value = entryLoader.load(key);
+ put(key, value);
+ return value;
+ } else {
+ ExpiringValue extends V> expiringValue = expiringEntryLoader.load(key);
+ if (expiringValue == null) {
+ put(key, null);
+ return null;
+ } else {
+ long duration = expiringValue.getTimeUnit() == null ? expirationNanos.get() : expiringValue.getDuration();
+ TimeUnit timeUnit = expiringValue.getTimeUnit() == null ? TimeUnit.NANOSECONDS : expiringValue.getTimeUnit();
+ put(key, expiringValue.getValue(), expiringValue.getExpirationPolicy() == null ? expirationPolicy.get()
+ : expiringValue.getExpirationPolicy(), duration, timeUnit);
+ return expiringValue.getValue();
+ }
+ }
+ } finally {
+ writeLock.unlock();
+ }
+ }
+
+ /**
+ * Returns the map's default expiration duration in milliseconds.
+ *
+ * @return The expiration duration (milliseconds)
+ */
+ public long getExpiration() {
+ return TimeUnit.NANOSECONDS.toMillis(expirationNanos.get());
+ }
+
+ /**
+ * Gets the expiration duration in milliseconds for the entry corresponding to the given key.
+ *
+ * @param key
+ * @return The expiration duration in milliseconds
+ * @throws NullPointerException if {@code key} is null
+ * @throws NoSuchElementException If no entry exists for the given key
+ */
+ public long getExpiration(K key) {
+ Assert.notNull(key, "key");
+ ExpiringEntry entry = getEntry(key);
+ Assert.element(entry, key);
+ return TimeUnit.NANOSECONDS.toMillis(entry.expirationNanos.get());
+ }
+
+ /**
+ * Gets the ExpirationPolicy for the entry corresponding to the given {@code key}.
+ *
+ * @param key
+ * @return The ExpirationPolicy for the {@code key}
+ * @throws NullPointerException if {@code key} is null
+ * @throws NoSuchElementException If no entry exists for the given key
+ */
+ public ExpirationPolicy getExpirationPolicy(K key) {
+ Assert.notNull(key, "key");
+ ExpiringEntry entry = getEntry(key);
+ Assert.element(entry, key);
+ return entry.expirationPolicy.get();
+ }
+
+ /**
+ * Gets the expected expiration, in milliseconds from the current time, for the entry corresponding to the given
+ * {@code key}.
+ *
+ * @param key
+ * @return The expiration duration in milliseconds
+ * @throws NullPointerException if {@code key} is null
+ * @throws NoSuchElementException If no entry exists for the given key
+ */
+ public long getExpectedExpiration(K key) {
+ Assert.notNull(key, "key");
+ ExpiringEntry entry = getEntry(key);
+ Assert.element(entry, key);
+ return TimeUnit.NANOSECONDS.toMillis(entry.expectedExpiration.get() - System.nanoTime());
+ }
+
+ /**
+ * Gets the maximum size of the map. Once this size has been reached, adding an additional entry will expire the
+ * first entry in line for expiration based on the expiration policy.
+ *
+ * @return The maximum size of the map.
+ */
+ public int getMaxSize() {
+ return maxSize;
}
/**
@@ -186,1237 +383,1049 @@ public class ExpiringMap implements ConcurrentMap {
*
* @param maxSize The maximum size of the map.
*/
- public Builder maxSize(int maxSize) {
- Assert.operation(maxSize > 0, "maxSize");
- this.maxSize = maxSize;
- return this;
- }
-
- /**
- * Sets the EntryLoader to use when loading entries. Either an EntryLoader or ExpiringEntryLoader may be set, not
- * both.
- *
- * @param loader to set
- * @throws NullPointerException if {@code loader} is null
- * @throws IllegalStateException if an {@link #expiringEntryLoader(ExpiringEntryLoader) ExpiringEntryLoader} is set
- */
- @SuppressWarnings("unchecked")
- public Builder entryLoader(EntryLoader super K1, ? super V1> loader) {
- assertNoLoaderSet();
- entryLoader = (EntryLoader) Assert.notNull(loader, "loader");
- return (Builder) this;
- }
-
- /**
- * Sets the ExpiringEntryLoader to use when loading entries and configures {@link #variableExpiration() variable
- * expiration}. Either an EntryLoader or ExpiringEntryLoader may be set, not both.
- *
- * @param loader to set
- * @throws NullPointerException if {@code loader} is null
- * @throws IllegalStateException if an {@link #entryLoader(EntryLoader) EntryLoader} is set
- */
- @SuppressWarnings("unchecked")
- public Builder expiringEntryLoader(
- ExpiringEntryLoader super K1, ? super V1> loader) {
- assertNoLoaderSet();
- expiringEntryLoader = (ExpiringEntryLoader) Assert.notNull(loader, "loader");
- variableExpiration();
- return (Builder) this;
- }
-
- /**
- * Configures the expiration listener that will receive notifications upon each map entry's expiration.
- * Notifications are delivered synchronously and block map write operations.
- *
- * @param listener to set
- * @throws NullPointerException if {@code listener} is null
- */
- @SuppressWarnings("unchecked")
- public Builder expirationListener(
- ExpirationListener super K1, ? super V1> listener) {
- Assert.notNull(listener, "listener");
- if (expirationListeners == null)
- expirationListeners = new ArrayList>();
- expirationListeners.add((ExpirationListener) listener);
- return (Builder) this;
- }
-
- /**
- * Configures the expiration listeners which will receive notifications upon each map entry's expiration.
- * Notifications are delivered synchronously and block map write operations.
- *
- * @param listeners to set
- * @throws NullPointerException if {@code listener} is null
- */
- @SuppressWarnings("unchecked")
- public Builder expirationListeners(
- List> listeners) {
- Assert.notNull(listeners, "listeners");
- if (expirationListeners == null)
- expirationListeners = new ArrayList>(listeners.size());
- for (ExpirationListener super K1, ? super V1> listener : listeners)
- expirationListeners.add((ExpirationListener) listener);
- return (Builder) this;
- }
-
- /**
- * Configures the expiration listener which will receive asynchronous notifications upon each map entry's
- * expiration.
- *
- * @param listener to set
- * @throws NullPointerException if {@code listener} is null
- */
- @SuppressWarnings("unchecked")
- public Builder asyncExpirationListener(
- ExpirationListener super K1, ? super V1> listener) {
- Assert.notNull(listener, "listener");
- if (asyncExpirationListeners == null)
- asyncExpirationListeners = new ArrayList>();
- asyncExpirationListeners.add((ExpirationListener) listener);
- return (Builder) this;
- }
-
- /**
- * Configures the expiration listeners which will receive asynchronous notifications upon each map entry's
- * expiration.
- *
- * @param listeners to set
- * @throws NullPointerException if {@code listener} is null
- */
- @SuppressWarnings("unchecked")
- public Builder asyncExpirationListeners(
- List> listeners) {
- Assert.notNull(listeners, "listeners");
- if (asyncExpirationListeners == null)
- asyncExpirationListeners = new ArrayList>(listeners.size());
- for (ExpirationListener super K1, ? super V1> listener : listeners)
- asyncExpirationListeners.add((ExpirationListener) listener);
- return (Builder) this;
- }
-
- /**
- * Configures the map entry expiration policy.
- *
- * @param expirationPolicy
- * @throws NullPointerException if {@code expirationPolicy} is null
- */
- public Builder expirationPolicy(ExpirationPolicy expirationPolicy) {
- this.expirationPolicy = Assert.notNull(expirationPolicy, "expirationPolicy");
- return this;
- }
-
- /**
- * Allows for map entries to have individual expirations and for expirations to be changed.
- */
- public Builder variableExpiration() {
- variableExpiration = true;
- return this;
- }
-
- private void assertNoLoaderSet() {
- Assert.state(entryLoader == null && expiringEntryLoader == null,
- "Either entryLoader or expiringEntryLoader may be set, not both");
- }
- }
-
- /** Entry map definition. */
- private interface EntryMap extends Map> {
- /** Returns the first entry in the map or null if the map is empty. */
- ExpiringEntry first();
-
- /**
- * Reorders the given entry in the map.
- *
- * @param entry to reorder
- */
- void reorder(ExpiringEntry entry);
-
- /** Returns a values iterator. */
- Iterator> valuesIterator();
- }
-
- /** Entry LinkedHashMap implementation. */
- private static class EntryLinkedHashMap extends LinkedHashMap>
- implements EntryMap {
- private static final long serialVersionUID = 1L;
-
- @Override
- public boolean containsValue(Object value) {
- for (ExpiringEntry entry : values()) {
- V v = entry.value;
- if (v == value || (value != null && value.equals(v)))
- return true;
- }
- return false;
- }
-
- @Override
- public ExpiringEntry first() {
- return isEmpty() ? null : values().iterator().next();
- }
-
- @Override
- public void reorder(ExpiringEntry value) {
- remove(value.key);
- value.resetExpiration();
- put(value.key, value);
- }
-
- @Override
- public Iterator> valuesIterator() {
- return values().iterator();
- }
-
- abstract class AbstractHashIterator {
- private final Iterator>> iterator;
- private ExpiringEntry next;
-
- @SuppressWarnings({"unchecked", "rawtypes"})
- AbstractHashIterator() {
- iterator = (Iterator) Arrays.asList(entrySet().toArray(new Map.Entry[0])).iterator();
- }
-
- public boolean hasNext() {
- return iterator.hasNext();
- }
-
- public ExpiringEntry getNext() {
- next = iterator.next().getValue();
- return next;
- }
-
- public void remove() {
- iterator.remove();
- }
- }
-
- final class KeyIterator extends AbstractHashIterator implements Iterator {
- public K next() {
- return getNext().key;
- }
- }
-
- final class ValueIterator extends AbstractHashIterator implements Iterator {
- public V next() {
- return getNext().value;
- }
- }
-
- public final class EntryIterator extends AbstractHashIterator implements Iterator> {
- public Map.Entry next() {
- return mapEntryFor(getNext());
- }
- }
- }
-
- /** Entry TreeHashMap implementation for variable expiration ExpiringMap entries. */
- private static class EntryTreeHashMap extends HashMap> implements EntryMap {
- private static final long serialVersionUID = 1L;
- SortedSet> sortedSet = new ConcurrentSkipListSet>();
-
- @Override
- public void clear() {
- super.clear();
- sortedSet.clear();
- }
-
- @Override
- public boolean containsValue(Object value) {
- for (ExpiringEntry entry : values()) {
- V v = entry.value;
- if (v == value || (value != null && value.equals(v)))
- return true;
- }
- return false;
- }
-
- @Override
- public ExpiringEntry first() {
- return sortedSet.isEmpty() ? null : sortedSet.first();
- }
-
- @Override
- public ExpiringEntry put(K key, ExpiringEntry value) {
- sortedSet.add(value);
- return super.put(key, value);
- }
-
- @Override
- public ExpiringEntry remove(Object key) {
- ExpiringEntry entry = super.remove(key);
- if (entry != null)
- sortedSet.remove(entry);
- return entry;
- }
-
- @Override
- public void reorder(ExpiringEntry value) {
- sortedSet.remove(value);
- value.resetExpiration();
- sortedSet.add(value);
- }
-
- @Override
- public Iterator> valuesIterator() {
- return new ExpiringEntryIterator();
- }
-
- abstract class AbstractHashIterator {
- private final Iterator> iterator = sortedSet.iterator();
- protected ExpiringEntry next;
-
- public boolean hasNext() {
- return iterator.hasNext();
- }
-
- public ExpiringEntry getNext() {
- next = iterator.next();
- return next;
- }
-
- public void remove() {
- EntryTreeHashMap.super.remove(next.key);
- iterator.remove();
- }
- }
-
- final class ExpiringEntryIterator extends AbstractHashIterator implements Iterator> {
- public final ExpiringEntry next() {
- return getNext();
- }
- }
-
- final class KeyIterator extends AbstractHashIterator implements Iterator {
- public final K next() {
- return getNext().key;
- }
- }
-
- final class ValueIterator extends AbstractHashIterator implements Iterator {
- public final V next() {
- return getNext().value;
- }
- }
-
- final class EntryIterator extends AbstractHashIterator implements Iterator> {
- public final Entry next() {
- return mapEntryFor(getNext());
- }
- }
- }
-
- /** Expiring map entry implementation. */
- static class ExpiringEntry implements Comparable> {
- final AtomicLong expirationNanos;
- /** Epoch time at which the entry is expected to expire */
- final AtomicLong expectedExpiration;
- final AtomicReference expirationPolicy;
- final K key;
- /** Guarded by "this" */
- volatile Future> entryFuture;
- /** Guarded by "this" */
- V value;
- /** Guarded by "this" */
- volatile boolean scheduled;
-
- /**
- * Creates a new ExpiringEntry object.
- *
- * @param key for the entry
- * @param value for the entry
- * @param expirationPolicy for the entry
- * @param expirationNanos for the entry
- */
- ExpiringEntry(K key, V value, AtomicReference expirationPolicy, AtomicLong expirationNanos) {
- this.key = key;
- this.value = value;
- this.expirationPolicy = expirationPolicy;
- this.expirationNanos = expirationNanos;
- this.expectedExpiration = new AtomicLong();
- resetExpiration();
- }
-
- @Override
- public int compareTo(ExpiringEntry other) {
- if (key.equals(other.key))
- return 0;
- return expectedExpiration.get() < other.expectedExpiration.get() ? -1 : 1;
+ public void setMaxSize(int maxSize) {
+ Assert.operation(maxSize > 0, "maxSize");
+ this.maxSize = maxSize;
}
@Override
public int hashCode() {
- final int prime = 31;
- int result = 1;
- result = prime * result + ((key == null) ? 0 : key.hashCode());
- result = prime * result + ((value == null) ? 0 : value.hashCode());
- return result;
+ readLock.lock();
+ try {
+ return entries.hashCode();
+ } finally {
+ readLock.unlock();
+ }
}
@Override
- public boolean equals(Object obj) {
- if (this == obj)
- return true;
- if (obj == null)
- return false;
- if (getClass() != obj.getClass())
- return false;
- ExpiringEntry, ?> other = (ExpiringEntry, ?>) obj;
- if (!key.equals(other.key))
- return false;
- if (value == null) {
- if (other.value != null)
- return false;
- } else if (!value.equals(other.value))
- return false;
- return true;
+ public boolean isEmpty() {
+ readLock.lock();
+ try {
+ return entries.isEmpty();
+ } finally {
+ readLock.unlock();
+ }
+ }
+
+ /**
+ * Returns a copy of the map's keys, which can be iterated over safely by multiple threads.
+ *
+ * @return Copied set of map keys.
+ */
+ @Override
+ public Set keySet() {
+ return new AbstractSet() {
+ @Override
+ public void clear() {
+ ExpiringMap.this.clear();
+ }
+
+ @Override
+ public boolean contains(Object key) {
+ return containsKey(key);
+ }
+
+ @Override
+ public Iterator iterator() {
+ readLock.lock();
+ try {
+ return (entries instanceof EntryLinkedHashMap) ? ((EntryLinkedHashMap) entries).new KeyIterator()
+ : ((EntryTreeHashMap) entries).new KeyIterator();
+ } finally {
+ readLock.unlock();
+ }
+ }
+
+ @Override
+ public boolean remove(Object value) {
+ return ExpiringMap.this.remove(value) != null;
+ }
+
+ @Override
+ public int size() {
+ return ExpiringMap.this.size();
+ }
+ };
+ }
+
+ /**
+ * Puts {@code value} in the map for {@code key}. Resets the entry's expiration unless an entry already exists for the
+ * same {@code key} and {@code value}.
+ *
+ * @param key to put value for
+ * @param value to put for key
+ * @return the old value
+ * @throws NullPointerException if {@code key} is null
+ */
+ @Override
+ public V put(K key, V value) {
+ Assert.notNull(key, "key");
+ return putInternal(key, value, expirationPolicy.get(), expirationNanos.get());
+ }
+
+ /**
+ * @see #put(Object, Object, ExpirationPolicy, long, TimeUnit)
+ */
+ public V put(K key, V value, ExpirationPolicy expirationPolicy) {
+ return put(key, value, expirationPolicy, expirationNanos.get(), TimeUnit.NANOSECONDS);
+ }
+
+ /**
+ * @see #put(Object, Object, ExpirationPolicy, long, TimeUnit)
+ */
+ public V put(K key, V value, long duration, TimeUnit timeUnit) {
+ return put(key, value, expirationPolicy.get(), duration, timeUnit);
+ }
+
+ /**
+ * Puts {@code value} in the map for {@code key}. Resets the entry's expiration unless an entry already exists for the
+ * same {@code key} and {@code value}. Requires that variable expiration be enabled.
+ *
+ * @param key Key to put value for
+ * @param value Value to put for key
+ * @param duration the length of time after an entry is created that it should be removed
+ * @param timeUnit the unit that {@code duration} is expressed in
+ * @return the old value
+ * @throws UnsupportedOperationException If variable expiration is not enabled
+ * @throws NullPointerException if {@code key}, {@code expirationPolicy} or {@code timeUnit} are null
+ */
+ public V put(K key, V value, ExpirationPolicy expirationPolicy, long duration, TimeUnit timeUnit) {
+ Assert.notNull(key, "key");
+ Assert.notNull(expirationPolicy, "expirationPolicy");
+ Assert.notNull(timeUnit, "timeUnit");
+ Assert.operation(variableExpiration, "Variable expiration is not enabled");
+ return putInternal(key, value, expirationPolicy, TimeUnit.NANOSECONDS.convert(duration, timeUnit));
+ }
+
+ @Override
+ public void putAll(Map extends K, ? extends V> map) {
+ Assert.notNull(map, "map");
+ long expiration = expirationNanos.get();
+ ExpirationPolicy expirationPolicy = this.expirationPolicy.get();
+ writeLock.lock();
+ try {
+ for (Entry extends K, ? extends V> entry : map.entrySet())
+ putInternal(entry.getKey(), entry.getValue(), expirationPolicy, expiration);
+ } finally {
+ writeLock.unlock();
+ }
+ }
+
+ @Override
+ public V putIfAbsent(K key, V value) {
+ Assert.notNull(key, "key");
+ writeLock.lock();
+ try {
+ if (!entries.containsKey(key))
+ return putInternal(key, value, expirationPolicy.get(), expirationNanos.get());
+ else
+ return entries.get(key).getValue();
+ } finally {
+ writeLock.unlock();
+ }
+ }
+
+ @Override
+ public V remove(Object key) {
+ Assert.notNull(key, "key");
+ writeLock.lock();
+ try {
+ ExpiringEntry entry = entries.remove(key);
+ if (entry == null)
+ return null;
+ if (entry.cancel())
+ scheduleEntry(entries.first());
+ return entry.getValue();
+ } finally {
+ writeLock.unlock();
+ }
+ }
+
+ @Override
+ public boolean remove(Object key, Object value) {
+ Assert.notNull(key, "key");
+ writeLock.lock();
+ try {
+ ExpiringEntry entry = entries.get(key);
+ if (entry != null && entry.getValue().equals(value)) {
+ entries.remove(key);
+ if (entry.cancel())
+ scheduleEntry(entries.first());
+ return true;
+ } else
+ return false;
+ } finally {
+ writeLock.unlock();
+ }
+ }
+
+ @Override
+ public V replace(K key, V value) {
+ Assert.notNull(key, "key");
+ writeLock.lock();
+ try {
+ if (entries.containsKey(key)) {
+ return putInternal(key, value, expirationPolicy.get(), expirationNanos.get());
+ } else
+ return null;
+ } finally {
+ writeLock.unlock();
+ }
+ }
+
+ @Override
+ public boolean replace(K key, V oldValue, V newValue) {
+ Assert.notNull(key, "key");
+ writeLock.lock();
+ try {
+ ExpiringEntry entry = entries.get(key);
+ if (entry != null && entry.getValue().equals(oldValue)) {
+ putInternal(key, newValue, expirationPolicy.get(), expirationNanos.get());
+ return true;
+ } else
+ return false;
+ } finally {
+ writeLock.unlock();
+ }
+ }
+
+ /**
+ * Removes an expiration listener.
+ *
+ * @param listener
+ * @throws NullPointerException if {@code listener} is null
+ */
+ public void removeExpirationListener(ExpirationListener listener) {
+ Assert.notNull(listener, "listener");
+ for (int i = 0; i < expirationListeners.size(); i++) {
+ if (expirationListeners.get(i).equals(listener)) {
+ expirationListeners.remove(i);
+ return;
+ }
+ }
+ }
+
+ /**
+ * Removes an asynchronous expiration listener.
+ *
+ * @param listener
+ * @throws NullPointerException if {@code listener} is null
+ */
+ public void removeAsyncExpirationListener(ExpirationListener listener) {
+ Assert.notNull(listener, "listener");
+ for (int i = 0; i < asyncExpirationListeners.size(); i++) {
+ if (asyncExpirationListeners.get(i).equals(listener)) {
+ asyncExpirationListeners.remove(i);
+ return;
+ }
+ }
+ }
+
+ /**
+ * Resets expiration for the entry corresponding to {@code key}.
+ *
+ * @param key to reset expiration for
+ * @throws NullPointerException if {@code key} is null
+ */
+ public void resetExpiration(K key) {
+ Assert.notNull(key, "key");
+ ExpiringEntry entry = getEntry(key);
+ if (entry != null)
+ resetEntry(entry, false);
+ }
+
+ /**
+ * Sets the expiration duration for the entry corresponding to the given key. Supported only if variable expiration is
+ * enabled.
+ *
+ * @param key Key to set expiration for
+ * @param duration the length of time after an entry is created that it should be removed
+ * @param timeUnit the unit that {@code duration} is expressed in
+ * @throws NullPointerException if {@code key} or {@code timeUnit} are null
+ * @throws UnsupportedOperationException If variable expiration is not enabled
+ */
+ public void setExpiration(K key, long duration, TimeUnit timeUnit) {
+ Assert.notNull(key, "key");
+ Assert.notNull(timeUnit, "timeUnit");
+ Assert.operation(variableExpiration, "Variable expiration is not enabled");
+ writeLock.lock();
+ try {
+ ExpiringEntry entry = entries.get(key);
+ if (entry != null) {
+ entry.expirationNanos.set(TimeUnit.NANOSECONDS.convert(duration, timeUnit));
+ resetEntry(entry, true);
+ }
+ } finally {
+ writeLock.unlock();
+ }
+ }
+
+ /**
+ * Updates the default map entry expiration. Supported only if variable expiration is enabled.
+ *
+ * @param duration the length of time after an entry is created that it should be removed
+ * @param timeUnit the unit that {@code duration} is expressed in
+ * @throws NullPointerException {@code timeUnit} is null
+ * @throws UnsupportedOperationException If variable expiration is not enabled
+ */
+ public void setExpiration(long duration, TimeUnit timeUnit) {
+ Assert.notNull(timeUnit, "timeUnit");
+ Assert.operation(variableExpiration, "Variable expiration is not enabled");
+ expirationNanos.set(TimeUnit.NANOSECONDS.convert(duration, timeUnit));
+ }
+
+ /**
+ * Sets the global expiration policy for the map. Individual expiration policies may override the global policy.
+ *
+ * @param expirationPolicy
+ * @throws NullPointerException {@code expirationPolicy} is null
+ */
+ public void setExpirationPolicy(ExpirationPolicy expirationPolicy) {
+ Assert.notNull(expirationPolicy, "expirationPolicy");
+ this.expirationPolicy.set(expirationPolicy);
+ }
+
+ /**
+ * Sets the expiration policy for the entry corresponding to the given key.
+ *
+ * @param key to set policy for
+ * @param expirationPolicy to set
+ * @throws NullPointerException if {@code key} or {@code expirationPolicy} are null
+ * @throws UnsupportedOperationException If variable expiration is not enabled
+ */
+ public void setExpirationPolicy(K key, ExpirationPolicy expirationPolicy) {
+ Assert.notNull(key, "key");
+ Assert.notNull(expirationPolicy, "expirationPolicy");
+ Assert.operation(variableExpiration, "Variable expiration is not enabled");
+ ExpiringEntry entry = getEntry(key);
+ if (entry != null)
+ entry.expirationPolicy.set(expirationPolicy);
+ }
+
+ @Override
+ public int size() {
+ readLock.lock();
+ try {
+ return entries.size();
+ } finally {
+ readLock.unlock();
+ }
}
@Override
public String toString() {
- return value.toString();
+ readLock.lock();
+ try {
+ return entries.toString();
+ } finally {
+ readLock.unlock();
+ }
}
/**
- * Marks the entry as canceled.
- *
- * @return true if the entry was scheduled
+ * Returns a copy of the map's values, which can be iterated over safely by multiple threads.
+ *
+ * @return Copied set of map values.
*/
- synchronized boolean cancel() {
- boolean result = scheduled;
- if (entryFuture != null)
- entryFuture.cancel(false);
-
- entryFuture = null;
- scheduled = false;
- return result;
- }
-
- /** Gets the entry value. */
- synchronized V getValue() {
- return value;
- }
-
- /** Resets the entry's expected expiration. */
- void resetExpiration() {
- expectedExpiration.set(expirationNanos.get() + System.nanoTime());
- }
-
- /** Marks the entry as scheduled. */
- synchronized void schedule(Future> entryFuture) {
- this.entryFuture = entryFuture;
- scheduled = true;
- }
-
- /** Sets the entry value. */
- synchronized void setValue(V value) {
- this.value = value;
- }
- }
-
- /**
- * Creates an ExpiringMap builder.
- *
- * @return New ExpiringMap builder
- */
- public static Builder builder() {
- return new Builder();
- }
-
- /**
- * Creates a new instance of ExpiringMap with ExpirationPolicy.CREATED and an expiration of 60 seconds.
- */
- @SuppressWarnings("unchecked")
- public static ExpiringMap create() {
- return new ExpiringMap((Builder) ExpiringMap.builder());
- }
-
- /**
- * Adds an expiration listener.
- *
- * @param listener to add
- * @throws NullPointerException if {@code listener} is null
- */
- public synchronized void addExpirationListener(ExpirationListener listener) {
- Assert.notNull(listener, "listener");
- if (expirationListeners == null)
- expirationListeners = new CopyOnWriteArrayList>();
- expirationListeners.add(listener);
- }
-
- /**
- * Adds an asynchronous expiration listener.
- *
- * @param listener to add
- * @throws NullPointerException if {@code listener} is null
- */
- public synchronized void addAsyncExpirationListener(ExpirationListener listener) {
- Assert.notNull(listener, "listener");
- if (asyncExpirationListeners == null)
- asyncExpirationListeners = new CopyOnWriteArrayList>();
- asyncExpirationListeners.add(listener);
- // If asyncListener was not added on Builder, LISTENER_SERVICE was not initialized and remain null
- if (LISTENER_SERVICE == null)
- initListenerService();
- }
-
- @Override
- public void clear() {
- writeLock.lock();
- try {
- for (ExpiringEntry entry : entries.values())
- entry.cancel();
- entries.clear();
- } finally {
- writeLock.unlock();
- }
- }
-
- @Override
- public boolean containsKey(Object key) {
- readLock.lock();
- try {
- return entries.containsKey(key);
- } finally {
- readLock.unlock();
- }
- }
-
- @Override
- public boolean containsValue(Object value) {
- readLock.lock();
- try {
- return entries.containsValue(value);
- } finally {
- readLock.unlock();
- }
- }
-
- /**
- * Returns a copy of the map's entries, which can be iterated over safely by multiple threads.
- *
- * @return Copied set of map entries.
- */
- @Override
- public Set> entrySet() {
- return new AbstractSet>() {
- @Override
- public void clear() {
- ExpiringMap.this.clear();
- }
-
- @Override
- public boolean contains(Object entry) {
- if (!(entry instanceof Map.Entry))
- return false;
- Entry, ?> e = (Entry, ?>) entry;
- return containsKey(e.getKey());
- }
-
- @Override
- public Iterator> iterator() {
- return (entries instanceof EntryLinkedHashMap) ? ((EntryLinkedHashMap) entries).new EntryIterator()
- : ((EntryTreeHashMap) entries).new EntryIterator();
- }
-
- @Override
- public boolean remove(Object entry) {
- if (entry instanceof Map.Entry) {
- Entry, ?> e = (Entry, ?>) entry;
- return ExpiringMap.this.remove(e.getKey()) != null;
- }
- return false;
- }
-
- @Override
- public int size() {
- return ExpiringMap.this.size();
- }
- };
- }
-
- @Override
- public boolean equals(Object obj) {
- readLock.lock();
- try {
- return entries.equals(obj);
- } finally {
- readLock.unlock();
- }
- }
-
- @Override
- @SuppressWarnings("unchecked")
- public V get(Object key) {
- ExpiringEntry entry = getEntry(key);
-
- if (entry == null) {
- return load((K) key);
- } else if (ExpirationPolicy.ACCESSED.equals(entry.expirationPolicy.get()))
- resetEntry(entry, false);
-
- return entry.getValue();
- }
-
- private V load(K key) {
- if (entryLoader == null && expiringEntryLoader == null)
- return null;
-
- writeLock.lock();
- try {
- // Double check for entry
- ExpiringEntry entry = getEntry(key);
- if (entry != null)
- return entry.getValue();
-
- if (entryLoader != null) {
- V value = entryLoader.load(key);
- put(key, value);
- return value;
- } else {
- ExpiringValue extends V> expiringValue = expiringEntryLoader.load(key);
- if (expiringValue == null) {
- put(key, null);
- return null;
- } else {
- long duration = expiringValue.getTimeUnit() == null ? expirationNanos.get() : expiringValue.getDuration();
- TimeUnit timeUnit = expiringValue.getTimeUnit() == null ? TimeUnit.NANOSECONDS : expiringValue.getTimeUnit();
- put(key, expiringValue.getValue(), expiringValue.getExpirationPolicy() == null ? expirationPolicy.get()
- : expiringValue.getExpirationPolicy(), duration, timeUnit);
- return expiringValue.getValue();
- }
- }
- } finally {
- writeLock.unlock();
- }
- }
-
- /**
- * Returns the map's default expiration duration in milliseconds.
- *
- * @return The expiration duration (milliseconds)
- */
- public long getExpiration() {
- return TimeUnit.NANOSECONDS.toMillis(expirationNanos.get());
- }
-
- /**
- * Gets the expiration duration in milliseconds for the entry corresponding to the given key.
- *
- * @param key
- * @return The expiration duration in milliseconds
- * @throws NullPointerException if {@code key} is null
- * @throws NoSuchElementException If no entry exists for the given key
- */
- public long getExpiration(K key) {
- Assert.notNull(key, "key");
- ExpiringEntry entry = getEntry(key);
- Assert.element(entry, key);
- return TimeUnit.NANOSECONDS.toMillis(entry.expirationNanos.get());
- }
-
- /**
- * Gets the ExpirationPolicy for the entry corresponding to the given {@code key}.
- *
- * @param key
- * @return The ExpirationPolicy for the {@code key}
- * @throws NullPointerException if {@code key} is null
- * @throws NoSuchElementException If no entry exists for the given key
- */
- public ExpirationPolicy getExpirationPolicy(K key) {
- Assert.notNull(key, "key");
- ExpiringEntry entry = getEntry(key);
- Assert.element(entry, key);
- return entry.expirationPolicy.get();
- }
-
- /**
- * Gets the expected expiration, in milliseconds from the current time, for the entry corresponding to the given
- * {@code key}.
- *
- * @param key
- * @return The expiration duration in milliseconds
- * @throws NullPointerException if {@code key} is null
- * @throws NoSuchElementException If no entry exists for the given key
- */
- public long getExpectedExpiration(K key) {
- Assert.notNull(key, "key");
- ExpiringEntry entry = getEntry(key);
- Assert.element(entry, key);
- return TimeUnit.NANOSECONDS.toMillis(entry.expectedExpiration.get() - System.nanoTime());
- }
-
- /**
- * Gets the maximum size of the map. Once this size has been reached, adding an additional entry will expire the
- * first entry in line for expiration based on the expiration policy.
- *
- * @return The maximum size of the map.
- */
- public int getMaxSize() {
- return maxSize;
- }
-
- @Override
- public int hashCode() {
- readLock.lock();
- try {
- return entries.hashCode();
- } finally {
- readLock.unlock();
- }
- }
-
- @Override
- public boolean isEmpty() {
- readLock.lock();
- try {
- return entries.isEmpty();
- } finally {
- readLock.unlock();
- }
- }
-
- /**
- * Returns a copy of the map's keys, which can be iterated over safely by multiple threads.
- *
- * @return Copied set of map keys.
- */
- @Override
- public Set keySet() {
- return new AbstractSet() {
- @Override
- public void clear() {
- ExpiringMap.this.clear();
- }
-
- @Override
- public boolean contains(Object key) {
- return containsKey(key);
- }
-
- @Override
- public Iterator iterator() {
- readLock.lock();
- try {
- return (entries instanceof EntryLinkedHashMap) ? ((EntryLinkedHashMap) entries).new KeyIterator()
- : ((EntryTreeHashMap) entries).new KeyIterator();
- } finally {
- readLock.unlock();
- }
- }
-
- @Override
- public boolean remove(Object value) {
- return ExpiringMap.this.remove(value) != null;
- }
-
- @Override
- public int size() {
- return ExpiringMap.this.size();
- }
- };
- }
-
- /**
- * Puts {@code value} in the map for {@code key}. Resets the entry's expiration unless an entry already exists for the
- * same {@code key} and {@code value}.
- *
- * @param key to put value for
- * @param value to put for key
- * @return the old value
- * @throws NullPointerException if {@code key} is null
- */
- @Override
- public V put(K key, V value) {
- Assert.notNull(key, "key");
- return putInternal(key, value, expirationPolicy.get(), expirationNanos.get());
- }
-
- /**
- * @see #put(Object, Object, ExpirationPolicy, long, TimeUnit)
- */
- public V put(K key, V value, ExpirationPolicy expirationPolicy) {
- return put(key, value, expirationPolicy, expirationNanos.get(), TimeUnit.NANOSECONDS);
- }
-
- /**
- * @see #put(Object, Object, ExpirationPolicy, long, TimeUnit)
- */
- public V put(K key, V value, long duration, TimeUnit timeUnit) {
- return put(key, value, expirationPolicy.get(), duration, timeUnit);
- }
-
- /**
- * Puts {@code value} in the map for {@code key}. Resets the entry's expiration unless an entry already exists for the
- * same {@code key} and {@code value}. Requires that variable expiration be enabled.
- *
- * @param key Key to put value for
- * @param value Value to put for key
- * @param duration the length of time after an entry is created that it should be removed
- * @param timeUnit the unit that {@code duration} is expressed in
- * @return the old value
- * @throws UnsupportedOperationException If variable expiration is not enabled
- * @throws NullPointerException if {@code key}, {@code expirationPolicy} or {@code timeUnit} are null
- */
- public V put(K key, V value, ExpirationPolicy expirationPolicy, long duration, TimeUnit timeUnit) {
- Assert.notNull(key, "key");
- Assert.notNull(expirationPolicy, "expirationPolicy");
- Assert.notNull(timeUnit, "timeUnit");
- Assert.operation(variableExpiration, "Variable expiration is not enabled");
- return putInternal(key, value, expirationPolicy, TimeUnit.NANOSECONDS.convert(duration, timeUnit));
- }
-
- @Override
- public void putAll(Map extends K, ? extends V> map) {
- Assert.notNull(map, "map");
- long expiration = expirationNanos.get();
- ExpirationPolicy expirationPolicy = this.expirationPolicy.get();
- writeLock.lock();
- try {
- for (Entry extends K, ? extends V> entry : map.entrySet())
- putInternal(entry.getKey(), entry.getValue(), expirationPolicy, expiration);
- } finally {
- writeLock.unlock();
- }
- }
-
- @Override
- public V putIfAbsent(K key, V value) {
- Assert.notNull(key, "key");
- writeLock.lock();
- try {
- if (!entries.containsKey(key))
- return putInternal(key, value, expirationPolicy.get(), expirationNanos.get());
- else
- return entries.get(key).getValue();
- } finally {
- writeLock.unlock();
- }
- }
-
- @Override
- public V remove(Object key) {
- Assert.notNull(key, "key");
- writeLock.lock();
- try {
- ExpiringEntry entry = entries.remove(key);
- if (entry == null)
- return null;
- if (entry.cancel())
- scheduleEntry(entries.first());
- return entry.getValue();
- } finally {
- writeLock.unlock();
- }
- }
-
- @Override
- public boolean remove(Object key, Object value) {
- Assert.notNull(key, "key");
- writeLock.lock();
- try {
- ExpiringEntry entry = entries.get(key);
- if (entry != null && entry.getValue().equals(value)) {
- entries.remove(key);
- if (entry.cancel())
- scheduleEntry(entries.first());
- return true;
- } else
- return false;
- } finally {
- writeLock.unlock();
- }
- }
-
- @Override
- public V replace(K key, V value) {
- Assert.notNull(key, "key");
- writeLock.lock();
- try {
- if (entries.containsKey(key)) {
- return putInternal(key, value, expirationPolicy.get(), expirationNanos.get());
- } else
- return null;
- } finally {
- writeLock.unlock();
- }
- }
-
- @Override
- public boolean replace(K key, V oldValue, V newValue) {
- Assert.notNull(key, "key");
- writeLock.lock();
- try {
- ExpiringEntry entry = entries.get(key);
- if (entry != null && entry.getValue().equals(oldValue)) {
- putInternal(key, newValue, expirationPolicy.get(), expirationNanos.get());
- return true;
- } else
- return false;
- } finally {
- writeLock.unlock();
- }
- }
-
- /**
- * Removes an expiration listener.
- *
- * @param listener
- * @throws NullPointerException if {@code listener} is null
- */
- public void removeExpirationListener(ExpirationListener listener) {
- Assert.notNull(listener, "listener");
- for (int i = 0; i < expirationListeners.size(); i++) {
- if (expirationListeners.get(i).equals(listener)) {
- expirationListeners.remove(i);
- return;
- }
- }
- }
-
- /**
- * Removes an asynchronous expiration listener.
- *
- * @param listener
- * @throws NullPointerException if {@code listener} is null
- */
- public void removeAsyncExpirationListener(ExpirationListener listener) {
- Assert.notNull(listener, "listener");
- for (int i = 0; i < asyncExpirationListeners.size(); i++) {
- if (asyncExpirationListeners.get(i).equals(listener)) {
- asyncExpirationListeners.remove(i);
- return;
- }
- }
- }
-
- /**
- * Resets expiration for the entry corresponding to {@code key}.
- *
- * @param key to reset expiration for
- * @throws NullPointerException if {@code key} is null
- */
- public void resetExpiration(K key) {
- Assert.notNull(key, "key");
- ExpiringEntry entry = getEntry(key);
- if (entry != null)
- resetEntry(entry, false);
- }
-
- /**
- * Sets the expiration duration for the entry corresponding to the given key. Supported only if variable expiration is
- * enabled.
- *
- * @param key Key to set expiration for
- * @param duration the length of time after an entry is created that it should be removed
- * @param timeUnit the unit that {@code duration} is expressed in
- * @throws NullPointerException if {@code key} or {@code timeUnit} are null
- * @throws UnsupportedOperationException If variable expiration is not enabled
- */
- public void setExpiration(K key, long duration, TimeUnit timeUnit) {
- Assert.notNull(key, "key");
- Assert.notNull(timeUnit, "timeUnit");
- Assert.operation(variableExpiration, "Variable expiration is not enabled");
- writeLock.lock();
- try {
- ExpiringEntry entry = entries.get(key);
- if (entry != null) {
- entry.expirationNanos.set(TimeUnit.NANOSECONDS.convert(duration, timeUnit));
- resetEntry(entry, true);
- }
- } finally {
- writeLock.unlock();
- }
- }
-
- /**
- * Updates the default map entry expiration. Supported only if variable expiration is enabled.
- *
- * @param duration the length of time after an entry is created that it should be removed
- * @param timeUnit the unit that {@code duration} is expressed in
- * @throws NullPointerException {@code timeUnit} is null
- * @throws UnsupportedOperationException If variable expiration is not enabled
- */
- public void setExpiration(long duration, TimeUnit timeUnit) {
- Assert.notNull(timeUnit, "timeUnit");
- Assert.operation(variableExpiration, "Variable expiration is not enabled");
- expirationNanos.set(TimeUnit.NANOSECONDS.convert(duration, timeUnit));
- }
-
- /**
- * Sets the global expiration policy for the map. Individual expiration policies may override the global policy.
- *
- * @param expirationPolicy
- * @throws NullPointerException {@code expirationPolicy} is null
- */
- public void setExpirationPolicy(ExpirationPolicy expirationPolicy) {
- Assert.notNull(expirationPolicy, "expirationPolicy");
- this.expirationPolicy.set(expirationPolicy);
- }
-
- /**
- * Sets the expiration policy for the entry corresponding to the given key.
- *
- * @param key to set policy for
- * @param expirationPolicy to set
- * @throws NullPointerException if {@code key} or {@code expirationPolicy} are null
- * @throws UnsupportedOperationException If variable expiration is not enabled
- */
- public void setExpirationPolicy(K key, ExpirationPolicy expirationPolicy) {
- Assert.notNull(key, "key");
- Assert.notNull(expirationPolicy, "expirationPolicy");
- Assert.operation(variableExpiration, "Variable expiration is not enabled");
- ExpiringEntry entry = getEntry(key);
- if (entry != null)
- entry.expirationPolicy.set(expirationPolicy);
- }
-
- /**
- * Sets the maximum size of the map. Once this size has been reached, adding an additional entry will expire the
- * first entry in line for expiration based on the expiration policy.
- *
- * @param maxSize The maximum size of the map.
- */
- public void setMaxSize(int maxSize) {
- Assert.operation(maxSize > 0, "maxSize");
- this.maxSize = maxSize;
- }
-
- @Override
- public int size() {
- readLock.lock();
- try {
- return entries.size();
- } finally {
- readLock.unlock();
- }
- }
-
- @Override
- public String toString() {
- readLock.lock();
- try {
- return entries.toString();
- } finally {
- readLock.unlock();
- }
- }
-
- /**
- * Returns a copy of the map's values, which can be iterated over safely by multiple threads.
- *
- * @return Copied set of map values.
- */
- @Override
- public Collection values() {
- return new AbstractCollection() {
- @Override
- public void clear() {
- ExpiringMap.this.clear();
- }
-
- @Override
- public boolean contains(Object value) {
- return containsValue(value);
- }
-
- @Override
- public Iterator iterator() {
- readLock.lock();
- try {
- return (entries instanceof EntryLinkedHashMap) ? ((EntryLinkedHashMap) entries).new ValueIterator()
- : ((EntryTreeHashMap) entries).new ValueIterator();
- } finally {
- readLock.unlock();
- }
- }
-
- @Override
- public int size() {
- return ExpiringMap.this.size();
- }
- };
- }
-
- /**
- * Notifies expiration listeners that the given entry expired. Must not be called from within a locked context.
- *
- * @param entry Entry to expire
- */
- void notifyListeners(final ExpiringEntry entry) {
- if (asyncExpirationListeners != null)
- for (final ExpirationListener listener : asyncExpirationListeners) {
- LISTENER_SERVICE.execute(new Runnable() {
- public void run() {
- try {
- listener.expired(entry.key, entry.getValue());
- } catch (Exception ignoreUserExceptions) {
- }
- }
- });
- }
-
- if (expirationListeners != null)
- for (final ExpirationListener listener : expirationListeners) {
- try {
- listener.expired(entry.key, entry.getValue());
- } catch (Exception ignoreUserExceptions) {
- }
- }
- }
-
- /**
- * Returns the internal ExpiringEntry for the {@code key}, obtaining a read lock.
- */
- ExpiringEntry getEntry(Object key) {
- readLock.lock();
- try {
- return entries.get(key);
- } finally {
- readLock.unlock();
- }
- }
-
- /**
- * Puts the given key/value in storage, scheduling the new entry for expiration if needed. If a previous value existed
- * for the given key, it is first cancelled and the entries reordered to reflect the new expiration.
- */
- V putInternal(K key, V value, ExpirationPolicy expirationPolicy, long expirationNanos) {
- writeLock.lock();
- try {
- ExpiringEntry entry = entries.get(key);
- V oldValue = null;
-
- if (entry == null) {
- entry = new ExpiringEntry(key, value,
- variableExpiration ? new AtomicReference(expirationPolicy) : this.expirationPolicy,
- variableExpiration ? new AtomicLong(expirationNanos) : this.expirationNanos);
- if (entries.size() >= maxSize) {
- ExpiringEntry expiredEntry = entries.first();
- entries.remove(expiredEntry.key);
- notifyListeners(expiredEntry);
- }
- entries.put(key, entry);
- if (entries.size() == 1 || entries.first().equals(entry))
- scheduleEntry(entry);
- } else {
- oldValue = entry.getValue();
- if (!ExpirationPolicy.ACCESSED.equals(expirationPolicy)
- && ((oldValue == null && value == null) || (oldValue != null && oldValue.equals(value))))
- return value;
-
- entry.setValue(value);
- resetEntry(entry, false);
- }
-
- return oldValue;
- } finally {
- writeLock.unlock();
- }
- }
-
- /**
- * Resets the given entry's schedule canceling any existing scheduled expiration and reordering the entry in the
- * internal map. Schedules the next entry in the map if the given {@code entry} was scheduled or if
- * {@code scheduleNext} is true.
- *
- * @param entry to reset
- * @param scheduleFirstEntry whether the first entry should be automatically scheduled
- */
- void resetEntry(ExpiringEntry entry, boolean scheduleFirstEntry) {
- writeLock.lock();
- try {
- boolean scheduled = entry.cancel();
- entries.reorder(entry);
-
- if (scheduled || scheduleFirstEntry)
- scheduleEntry(entries.first());
- } finally {
- writeLock.unlock();
- }
- }
-
- /**
- * Schedules an entry for expiration. Guards against concurrent schedule/schedule, cancel/schedule and schedule/cancel
- * calls.
- *
- * @param entry Entry to schedule
- */
- void scheduleEntry(ExpiringEntry entry) {
- if (entry == null || entry.scheduled)
- return;
-
- Runnable runnable = null;
- synchronized (entry) {
- if (entry.scheduled)
- return;
-
- final WeakReference> entryReference = new WeakReference>(entry);
- runnable = new Runnable() {
- @Override
- public void run() {
- ExpiringEntry entry = entryReference.get();
-
- writeLock.lock();
- try {
- if (entry != null && entry.scheduled) {
- entries.remove(entry.key);
- notifyListeners(entry);
+ @Override
+ public Collection values() {
+ return new AbstractCollection() {
+ @Override
+ public void clear() {
+ ExpiringMap.this.clear();
}
- try {
- // Expires entries and schedules the next entry
- Iterator> iterator = entries.valuesIterator();
- boolean schedulePending = true;
+ @Override
+ public boolean contains(Object value) {
+ return containsValue(value);
+ }
- while (iterator.hasNext() && schedulePending) {
- ExpiringEntry nextEntry = iterator.next();
- if (nextEntry.expectedExpiration.get() <= System.nanoTime()) {
- iterator.remove();
- notifyListeners(nextEntry);
- } else {
- scheduleEntry(nextEntry);
- schedulePending = false;
+ @Override
+ public Iterator iterator() {
+ readLock.lock();
+ try {
+ return (entries instanceof EntryLinkedHashMap) ? ((EntryLinkedHashMap) entries).new ValueIterator()
+ : ((EntryTreeHashMap) entries).new ValueIterator();
+ } finally {
+ readLock.unlock();
}
- }
- } catch (NoSuchElementException ignored) {
}
- } finally {
- writeLock.unlock();
- }
+
+ @Override
+ public int size() {
+ return ExpiringMap.this.size();
+ }
+ };
+ }
+
+ /**
+ * Notifies expiration listeners that the given entry expired. Must not be called from within a locked context.
+ *
+ * @param entry Entry to expire
+ */
+ void notifyListeners(final ExpiringEntry entry) {
+ if (asyncExpirationListeners != null)
+ for (final ExpirationListener listener : asyncExpirationListeners) {
+ LISTENER_SERVICE.execute(new Runnable() {
+ public void run() {
+ try {
+ listener.expired(entry.key, entry.getValue());
+ } catch (Exception ignoreUserExceptions) {
+ }
+ }
+ });
+ }
+
+ if (expirationListeners != null)
+ for (final ExpirationListener listener : expirationListeners) {
+ try {
+ listener.expired(entry.key, entry.getValue());
+ } catch (Exception ignoreUserExceptions) {
+ }
+ }
+ }
+
+ /**
+ * Returns the internal ExpiringEntry for the {@code key}, obtaining a read lock.
+ */
+ ExpiringEntry getEntry(Object key) {
+ readLock.lock();
+ try {
+ return entries.get(key);
+ } finally {
+ readLock.unlock();
}
- };
-
- Future> entryFuture = EXPIRER.schedule(runnable, entry.expectedExpiration.get() - System.nanoTime(),
- TimeUnit.NANOSECONDS);
- entry.schedule(entryFuture);
}
- }
- private static Entry mapEntryFor(final ExpiringEntry entry) {
- return new Entry() {
- @Override
- public K getKey() {
- return entry.key;
- }
+ /**
+ * Puts the given key/value in storage, scheduling the new entry for expiration if needed. If a previous value existed
+ * for the given key, it is first cancelled and the entries reordered to reflect the new expiration.
+ */
+ V putInternal(K key, V value, ExpirationPolicy expirationPolicy, long expirationNanos) {
+ writeLock.lock();
+ try {
+ ExpiringEntry entry = entries.get(key);
+ V oldValue = null;
- @Override
- public V getValue() {
- return entry.value;
- }
+ if (entry == null) {
+ entry = new ExpiringEntry(key, value,
+ variableExpiration ? new AtomicReference(expirationPolicy) : this.expirationPolicy,
+ variableExpiration ? new AtomicLong(expirationNanos) : this.expirationNanos);
+ if (entries.size() >= maxSize) {
+ ExpiringEntry expiredEntry = entries.first();
+ entries.remove(expiredEntry.key);
+ notifyListeners(expiredEntry);
+ }
+ entries.put(key, entry);
+ if (entries.size() == 1 || entries.first().equals(entry))
+ scheduleEntry(entry);
+ } else {
+ oldValue = entry.getValue();
+ if (!ExpirationPolicy.ACCESSED.equals(expirationPolicy)
+ && ((oldValue == null && value == null) || (oldValue != null && oldValue.equals(value))))
+ return value;
- @Override
- public V setValue(V value) {
- throw new UnsupportedOperationException();
- }
- };
- }
+ entry.setValue(value);
+ resetEntry(entry, false);
+ }
- private void initListenerService() {
- synchronized (ExpiringMap.class) {
- if (LISTENER_SERVICE == null) {
- LISTENER_SERVICE = (ThreadPoolExecutor) Executors.newCachedThreadPool(
- THREAD_FACTORY == null ? new NamedThreadFactory("ExpiringMap-Listener-%s") : THREAD_FACTORY);
- }
+ return oldValue;
+ } finally {
+ writeLock.unlock();
+ }
+ }
+
+ /**
+ * Resets the given entry's schedule canceling any existing scheduled expiration and reordering the entry in the
+ * internal map. Schedules the next entry in the map if the given {@code entry} was scheduled or if
+ * {@code scheduleNext} is true.
+ *
+ * @param entry to reset
+ * @param scheduleFirstEntry whether the first entry should be automatically scheduled
+ */
+ void resetEntry(ExpiringEntry entry, boolean scheduleFirstEntry) {
+ writeLock.lock();
+ try {
+ boolean scheduled = entry.cancel();
+ entries.reorder(entry);
+
+ if (scheduled || scheduleFirstEntry)
+ scheduleEntry(entries.first());
+ } finally {
+ writeLock.unlock();
+ }
+ }
+
+ /**
+ * Schedules an entry for expiration. Guards against concurrent schedule/schedule, cancel/schedule and schedule/cancel
+ * calls.
+ *
+ * @param entry Entry to schedule
+ */
+ void scheduleEntry(ExpiringEntry entry) {
+ if (entry == null || entry.scheduled)
+ return;
+
+ Runnable runnable = null;
+ synchronized (entry) {
+ if (entry.scheduled)
+ return;
+
+ final WeakReference> entryReference = new WeakReference>(entry);
+ runnable = new Runnable() {
+ @Override
+ public void run() {
+ ExpiringEntry entry = entryReference.get();
+
+ writeLock.lock();
+ try {
+ if (entry != null && entry.scheduled) {
+ entries.remove(entry.key);
+ notifyListeners(entry);
+ }
+
+ try {
+ // Expires entries and schedules the next entry
+ Iterator> iterator = entries.valuesIterator();
+ boolean schedulePending = true;
+
+ while (iterator.hasNext() && schedulePending) {
+ ExpiringEntry nextEntry = iterator.next();
+ if (nextEntry.expectedExpiration.get() <= System.nanoTime()) {
+ iterator.remove();
+ notifyListeners(nextEntry);
+ } else {
+ scheduleEntry(nextEntry);
+ schedulePending = false;
+ }
+ }
+ } catch (NoSuchElementException ignored) {
+ }
+ } finally {
+ writeLock.unlock();
+ }
+ }
+ };
+
+ Future> entryFuture = EXPIRER.schedule(runnable, entry.expectedExpiration.get() - System.nanoTime(),
+ TimeUnit.NANOSECONDS);
+ entry.schedule(entryFuture);
+ }
+ }
+
+ private void initListenerService() {
+ synchronized (ExpiringMap.class) {
+ if (LISTENER_SERVICE == null) {
+ LISTENER_SERVICE = (ThreadPoolExecutor) Executors.newCachedThreadPool(
+ THREAD_FACTORY == null ? new NamedThreadFactory("ExpiringMap-Listener-%s") : THREAD_FACTORY);
+ }
+ }
+ }
+
+ /**
+ * Entry map definition.
+ */
+ private interface EntryMap extends Map> {
+ /**
+ * Returns the first entry in the map or null if the map is empty.
+ */
+ ExpiringEntry first();
+
+ /**
+ * Reorders the given entry in the map.
+ *
+ * @param entry to reorder
+ */
+ void reorder(ExpiringEntry entry);
+
+ /**
+ * Returns a values iterator.
+ */
+ Iterator> valuesIterator();
+ }
+
+ /**
+ * Builds ExpiringMap instances. Defaults to ExpirationPolicy.CREATED, expiration of 60 TimeUnit.SECONDS and
+ * a maxSize of Integer.MAX_VALUE.
+ */
+ public static final class Builder {
+ private ExpirationPolicy expirationPolicy = ExpirationPolicy.CREATED;
+ private List> expirationListeners;
+ private List> asyncExpirationListeners;
+ private TimeUnit timeUnit = TimeUnit.SECONDS;
+ private boolean variableExpiration;
+ private long duration = 60;
+ private int maxSize = Integer.MAX_VALUE;
+ private EntryLoader entryLoader;
+ private ExpiringEntryLoader expiringEntryLoader;
+
+ /**
+ * Creates a new Builder object.
+ */
+ private Builder() {
+ }
+
+ /**
+ * Builds and returns an expiring map.
+ *
+ * @param