
net.jodah.expiringmap.ExpiringMap Maven / Gradle / Ivy
The newest version!
package net.jodah.expiringmap;
import java.lang.ref.WeakReference;
import java.util.AbstractCollection;
import java.util.AbstractSet;
import java.util.ArrayList;
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.TreeSet;
import java.util.concurrent.ConcurrentMap;
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.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 net.jodah.expiringmap.internal.Assert;
import net.jodah.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();
* Map map = ExpiringMap.builder().expiration(30, TimeUnit.SECONDS).build();
* Map map = ExpiringMap.builder()
* .expiration(10, TimeUnit.MINUTES)
* .entryLoader(new EntryLoader() {
* public Connection load(String address) {
* return new Connection(address);
* }
* })
* .expirationListener(new ExpirationListener() {
* public void expired(String key, Connection connection) {
* connection.close();
* }
* })
* .build();
* }
*
*
* @author Jonathan Halterman
* @param Key type
* @param Value type
*/
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) {
synchronized (ExpiringMap.class) {
if (LISTENER_SERVICE == null) {
LISTENER_SERVICE = (ThreadPoolExecutor) Executors.newCachedThreadPool(
THREAD_FACTORY == null ? new NamedThreadFactory("ExpiringMap-Listener-%s") : THREAD_FACTORY);
}
}
}
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;
/**
* Creates a new Builder object.
*/
private Builder() {
}
/**
* Builds and returns an expiring map.
*
* @param Key type
* @param Value type
*/
@SuppressWarnings("unchecked")
public ExpiringMap build() {
return new ExpiringMap((Builder) this);
}
/**
* 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
*/
public Builder expiration(long duration, TimeUnit timeUnit) {
this.duration = duration;
this.timeUnit = Assert.notNull(timeUnit, "timeUnit");
return this;
}
/**
* 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 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 = entrySet().iterator();
private ExpiringEntry next;
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 final K next() {
return getNext().key;
}
}
final class ValueIterator extends AbstractHashIterator implements Iterator {
public final V next() {
return getNext().value;
}
}
public final class EntryIterator extends AbstractHashIterator implements Iterator> {
public final 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 TreeSet>();
@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 Map.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;
}
@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;
}
@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;
}
@Override
public String toString() {
return value.toString();
}
/**
* Marks the entry as canceled.
*
* @return true if the entry was scheduled
*/
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
© 2015 - 2025 Weber Informatics LLC | Privacy Policy