diff --git a/pom.xml b/pom.xml index 612a426..4613cc7 100644 --- a/pom.xml +++ b/pom.xml @@ -45,7 +45,7 @@ org.testng testng - 6.8.8 + 7.5.1 test diff --git a/src/main/java/net/jodah/expiringmap/ExpiringEntry.java b/src/main/java/net/jodah/expiringmap/ExpiringEntry.java new file mode 100644 index 0000000..9450dc8 --- /dev/null +++ b/src/main/java/net/jodah/expiringmap/ExpiringEntry.java @@ -0,0 +1,113 @@ +package net.jodah.expiringmap; + +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +/** Expiring map entry implementation. */ +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; + } +} diff --git a/src/main/java/net/jodah/expiringmap/ExpiringMap.java b/src/main/java/net/jodah/expiringmap/ExpiringMap.java index 26dc4e9..195dcb5 100644 --- a/src/main/java/net/jodah/expiringmap/ExpiringMap.java +++ b/src/main/java/net/jodah/expiringmap/ExpiringMap.java @@ -20,14 +20,14 @@ 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 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; @@ -81,7 +81,7 @@ public class ExpiringMap implements ConcurrentMap { List> expirationListeners; List> asyncExpirationListeners; - private AtomicLong expirationNanos; + private AtomicLong defaultExpirationDurationNanos; private int maxSize; private final AtomicReference expirationPolicy; private final EntryLoader entryLoader; @@ -129,7 +129,7 @@ private ExpiringMap(final Builder builder) { if (builder.asyncExpirationListeners != null) asyncExpirationListeners = new CopyOnWriteArrayList>(builder.asyncExpirationListeners); expirationPolicy = new AtomicReference(builder.expirationPolicy); - expirationNanos = new AtomicLong(TimeUnit.NANOSECONDS.convert(builder.duration, builder.timeUnit)); + defaultExpirationDurationNanos = new AtomicLong(TimeUnit.NANOSECONDS.convert(builder.duration, builder.timeUnit)); maxSize = builder.maxSize; entryLoader = builder.entryLoader; expiringEntryLoader = builder.expiringEntryLoader; @@ -320,9 +320,9 @@ private void assertNoLoaderSet() { } /** Entry map definition. */ - private interface EntryMap extends Map> { - /** Returns the first entry in the map or null if the map is empty. */ - ExpiringEntry first(); + private interface EntryMap extends Map> { + /** Returns the next entry to expire in the map or null if the map is empty. */ + ExpiringEntry getNextToExpire(); /** * Reorders the given entry in the map. @@ -351,9 +351,9 @@ public boolean containsValue(Object value) { } @Override - public ExpiringEntry first() { - return isEmpty() ? null : values().iterator().next(); - } + public ExpiringEntry getNextToExpire() { + return isEmpty() ? null : values().iterator().next(); + } @Override public void reorder(ExpiringEntry value) { @@ -431,9 +431,9 @@ public boolean containsValue(Object value) { } @Override - public ExpiringEntry first() { - return sortedSet.isEmpty() ? null : sortedSet.first(); - } + public ExpiringEntry getNextToExpire() { + return sortedSet.isEmpty() ? null : sortedSet.first(); + } @Override public ExpiringEntry put(K key, ExpiringEntry value) { @@ -505,114 +505,6 @@ public final Map.Entry next() { } } - /** 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. * @@ -659,36 +551,33 @@ public synchronized void addAsyncExpirationListener(ExpirationListener lis 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 void clear() { + withWriteLock(new Runnable() { + public void run() { + for (ExpiringEntry entry : entries.values()) + entry.cancel(); + entries.clear(); + } + }); + } @Override - public boolean containsValue(Object value) { - readLock.lock(); - try { - return entries.containsValue(value); - } finally { - readLock.unlock(); - } + public boolean containsKey(final Object key) { + return withReadLock(new LockOperation() { + public Boolean get() { + return entries.containsKey(key); + } + }); + } + + @Override + public boolean containsValue(final Object value) { + return withReadLock(new LockOperation() { + public Boolean get() { + return entries.containsValue(value); + } + }); } /** @@ -704,13 +593,13 @@ public void clear() { ExpiringMap.this.clear(); } - @Override - public boolean contains(Object entry) { - if (!(entry instanceof Map.Entry)) - return false; - Map.Entry e = (Map.Entry) entry; - return containsKey(e.getKey()); - } + @Override + public boolean contains(Object entry) { + if (!(entry instanceof Map.Entry)) + return false; + Map.Entry mapEntry = (Map.Entry) entry; + return containsKey(mapEntry.getKey()); + } @Override public Iterator> iterator() { @@ -723,14 +612,14 @@ public Iterator> iterator() { } } - @Override - public boolean remove(Object entry) { - if (entry instanceof Map.Entry) { - Map.Entry e = (Map.Entry) entry; - return ExpiringMap.this.remove(e.getKey()) != null; - } - return false; - } + @Override + public boolean remove(Object entry) { + if (entry instanceof Map.Entry) { + Map.Entry mapEntry = (Map.Entry) entry; + return ExpiringMap.this.remove(mapEntry.getKey()) != null; + } + return false; + } @Override public int size() { @@ -740,27 +629,31 @@ public int 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(); - } + public boolean equals(final Object obj) { + return withReadLock(new LockOperation() { + public Boolean get() { + return entries.equals(obj); + } + }); + } + + @Override + @SuppressWarnings("unchecked") + public V get(Object key) { + ExpiringEntry entry = getEntry(key); + + if (entry == null) + return load((K) key); + + if (isAccessBased(entry)) + resetEntry(entry, false); + + return entry.getValue(); + } + + private boolean isAccessBased(ExpiringEntry entry) { + return ExpirationPolicy.ACCESSED.equals(entry.expirationPolicy.get()); + } private V load(K key) { if (entryLoader == null && expiringEntryLoader == null) @@ -783,7 +676,8 @@ private V load(K key) { put(key, null); return null; } else { - long duration = expiringValue.getTimeUnit() == null ? expirationNanos.get() : expiringValue.getDuration(); + long duration = expiringValue.getTimeUnit() == null ? defaultExpirationDurationNanos.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); @@ -801,7 +695,7 @@ private V load(K key) { * @return The expiration duration (milliseconds) */ public long getExpiration() { - return TimeUnit.NANOSECONDS.toMillis(expirationNanos.get()); + return TimeUnit.NANOSECONDS.toMillis(defaultExpirationDurationNanos.get()); } /** @@ -861,23 +755,21 @@ public int getMaxSize() { } @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(); - } + public int hashCode() { + return withReadLock(new LockOperation() { + public Integer get() { + return entries.hashCode(); + } + }); + } + + @Override + public boolean isEmpty() { + return withReadLock(new LockOperation() { + public Boolean get() { + return entries.isEmpty(); + } + }); } /** @@ -931,17 +823,21 @@ public int size() { * @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()); - } + public V put(final K key, final V value) { + Assert.notNull(key, "key"); + return withWriteLock(new LockOperation() { + public V get() { + return putInternal(key, value, expirationPolicy.get(), defaultExpirationDurationNanos.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); - } + public V put(K key, V value, ExpirationPolicy expirationPolicy) { + return put(key, value, expirationPolicy, defaultExpirationDurationNanos.get(), TimeUnit.NANOSECONDS); + } /** * @see #put(Object, Object, ExpirationPolicy, long, TimeUnit) @@ -962,18 +858,23 @@ public V put(K key, V value, long duration, TimeUnit timeUnit) { * @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)); - } + public V put(final K key, final V value, final 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"); + final long expirationNanos = TimeUnit.NANOSECONDS.convert(duration, timeUnit); + return withWriteLock(new LockOperation() { + public V get() { + return putInternal(key, value, expirationPolicy, expirationNanos); + } + }); + } @Override public void putAll(Map map) { Assert.notNull(map, "map"); - long expiration = expirationNanos.get(); + long expiration = defaultExpirationDurationNanos.get(); ExpirationPolicy expirationPolicy = this.expirationPolicy.get(); writeLock.lock(); try { @@ -990,7 +891,7 @@ public V putIfAbsent(K key, V value) { writeLock.lock(); try { if (!entries.containsKey(key)) - return putInternal(key, value, expirationPolicy.get(), expirationNanos.get()); + return putInternal(key, value, expirationPolicy.get(), defaultExpirationDurationNanos.get()); else return entries.get(key).getValue(); } finally { @@ -1007,7 +908,7 @@ public V remove(Object key) { if (entry == null) return null; if (entry.cancel()) - scheduleEntry(entries.first()); + scheduleEntry(entries.getNextToExpire()); return entry.getValue(); } finally { writeLock.unlock(); @@ -1023,7 +924,7 @@ public boolean remove(Object key, Object value) { if (entry != null && entry.getValue().equals(value)) { entries.remove(key); if (entry.cancel()) - scheduleEntry(entries.first()); + scheduleEntry(entries.getNextToExpire()); return true; } else return false; @@ -1038,7 +939,7 @@ public V replace(K key, V value) { writeLock.lock(); try { if (entries.containsKey(key)) { - return putInternal(key, value, expirationPolicy.get(), expirationNanos.get()); + return putInternal(key, value, expirationPolicy.get(), defaultExpirationDurationNanos.get()); } else return null; } finally { @@ -1053,7 +954,7 @@ public boolean replace(K key, V oldValue, V newValue) { try { ExpiringEntry entry = entries.get(key); if (entry != null && entry.getValue().equals(oldValue)) { - putInternal(key, newValue, expirationPolicy.get(), expirationNanos.get()); + putInternal(key, newValue, expirationPolicy.get(), defaultExpirationDurationNanos.get()); return true; } else return false; @@ -1144,7 +1045,7 @@ public void setExpiration(K key, long duration, TimeUnit timeUnit) { 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)); + defaultExpirationDurationNanos.set(TimeUnit.NANOSECONDS.convert(duration, timeUnit)); } /** @@ -1187,24 +1088,22 @@ public void setMaxSize(int 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(); - } - } + public int size() { + return withReadLock(new LockOperation() { + public Integer get() { + return entries.size(); + } + }); + } + + @Override + public String toString() { + return withReadLock(new LockOperation() { + public String get() { + return entries.toString(); + } + }); + } /** * Returns a copy of the map's values, which can be iterated over safely by multiple threads. @@ -1272,52 +1171,53 @@ public void run() { /** * 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(); - } - } + ExpiringEntry getEntry(final Object key) { + return withReadLock(new LockOperation>() { + public ExpiringEntry get() { + return entries.get(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; - - 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(); - } - } + V putInternal(K key, V value, ExpirationPolicy expirationPolicy, long expirationNanos) { + ExpiringEntry entry = entries.get(key); + if (entry == null) + return createAndScheduleEntry(key, value, expirationPolicy, expirationNanos); + + return updateExistingEntry(entry, value, expirationPolicy); + } + + private V createAndScheduleEntry(K key, V value, ExpirationPolicy expirationPolicy, long expirationNanos) { + ExpiringEntry entry = new ExpiringEntry(key, value, + variableExpiration ? new AtomicReference(expirationPolicy) : this.expirationPolicy, + variableExpiration ? new AtomicLong(expirationNanos) : defaultExpirationDurationNanos); + + if (entries.size() >= maxSize) { + ExpiringEntry expiredEntry = entries.getNextToExpire(); + entries.remove(expiredEntry.key); + notifyListeners(expiredEntry); + } + + entries.put(key, entry); + if (entries.size() == 1 || entries.getNextToExpire().equals(entry)) + scheduleEntry(entry); + return null; + } + + private V updateExistingEntry(ExpiringEntry entry, V value, ExpirationPolicy expirationPolicy) { + V 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; + } /** * Resets the given entry's schedule canceling any existing scheduled expiration and reordering the entry in the @@ -1331,10 +1231,10 @@ void resetEntry(ExpiringEntry entry, boolean scheduleFirstEntry) { writeLock.lock(); try { boolean scheduled = entry.cancel(); - entries.reorder(entry); - - if (scheduled || scheduleFirstEntry) - scheduleEntry(entries.first()); + entries.reorder(entry); + + if (scheduled || scheduleFirstEntry) + scheduleEntry(entries.getNextToExpire()); } finally { writeLock.unlock(); } @@ -1397,7 +1297,7 @@ public void run() { } } - private static Map.Entry mapEntryFor(final ExpiringEntry entry) { + private static Map.Entry mapEntryFor(final ExpiringEntry entry) { return new Map.Entry() { @Override public K getKey() { @@ -1414,9 +1314,40 @@ public V setValue(V value) { throw new UnsupportedOperationException(); } }; - } - - private void initListenerService() { + } + + private T withReadLock(LockOperation operation) { + readLock.lock(); + try { + return operation.get(); + } finally { + readLock.unlock(); + } + } + + private T withWriteLock(LockOperation operation) { + writeLock.lock(); + try { + return operation.get(); + } finally { + writeLock.unlock(); + } + } + + private void withWriteLock(Runnable operation) { + writeLock.lock(); + try { + operation.run(); + } finally { + writeLock.unlock(); + } + } + + private interface LockOperation { + T get(); + } + + private void initListenerService() { synchronized (ExpiringMap.class) { if (LISTENER_SERVICE == null) { LISTENER_SERVICE = (ThreadPoolExecutor) Executors.newCachedThreadPool( diff --git a/src/test/java/net/jodah/expiringmap/ExpiringEntryTest.java b/src/test/java/net/jodah/expiringmap/ExpiringEntryTest.java index a22f8a4..58c2e61 100644 --- a/src/test/java/net/jodah/expiringmap/ExpiringEntryTest.java +++ b/src/test/java/net/jodah/expiringmap/ExpiringEntryTest.java @@ -11,8 +11,6 @@ import org.testng.annotations.Test; -import net.jodah.expiringmap.ExpiringMap.ExpiringEntry; - /** * Tests {@link ExpiringEntry}. */