All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.netty.core.util.ConcurrentLinkedHashMap Maven / Gradle / Ivy

package com.github.netty.core.util;

import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import static java.util.Collections.*;

/**
 * A hash table supporting full concurrency of retrievals, adjustable expected
 * concurrency for updates, and a maximum capacity to bound the map by. This
 * implementation differs from {@link ConcurrentHashMap} in that it maintains a
 * page replacement algorithm that is used to evict an entry when the map has
 * exceeded its capacity. Unlike the Java Collections Framework, this
 * map does not have a publicly visible constructor and instances are created
 * through a {@link Builder}.
 * 

* An entry is evicted from the map when the weighted capacity exceeds * its maximum weighted capacity threshold. A {@link Weigher} instance * determines how many units of capacity that a value consumes. The default * weigher assigns each value a weight of 1 to bound the map by the * total number of key-value pairs. A map that holds collections may choose to * weigh values by the number of elements in the collection and bound the map * by the total number of elements that it contains. A change to a value that * modifies its weight requires that an update operation is performed on the * map. *

* An {@link EvictionListener} may be supplied for notification when an entry * is evicted from the map. This listener is invoked on a caller's thread and * will not block other threads from operating on the map. An implementation * should be aware that the caller's thread will not expect long execution * times or failures as a side effect of the listener being notified. Execution * safety and a fast turn around time can be achieved by performing the * operation asynchronously, such as by submitting a task to an * {@link ExecutorService}. *

* The concurrency level determines the number of threads that can * concurrently modify the table. Using a significantly higher or lower value * than needed can waste space or lead to thread contention, but an estimate * within an order of magnitude of the ideal value does not usually have a * noticeable impact. Because placement in hash tables is essentially random, * the actual concurrency will vary. *

* This class and its views and iterators implement all of the * optional methods of the {@link Map} and {@link Iterator} * interfaces. *

* Like {@link Hashtable} but unlike {@link HashMap}, this class * does not allow null to be used as a key or value. Unlike * {@link LinkedHashMap}, this class does not provide * predictable iteration order. A snapshot of the keys and entries may be * obtained in ascending and descending order of retention. * * @param the type of keys maintained by this map * @param the type of mapped values * @author [email protected] (Ben Manes) * @see * http://code.google.com/p/concurrentlinkedhashmap/ */ public final class ConcurrentLinkedHashMap extends AbstractMap implements ConcurrentMap, Serializable { /* * This class performs a best-effort bounding of a ConcurrentHashMap using a * page-replacement algorithm to determine which entries to evict when the * capacity is exceeded. * * The page replacement algorithm's data structures are kept eventually * consistent with the map. An update to the map and recording of reads may * not be immediately reflected on the algorithm's data structures. These * structures are guarded by a lock and operations are applied in batches to * avoid lock contention. The penalty of applying the batches is spread across * threads so that the amortized cost is slightly higher than performing just * the ConcurrentHashMap operation. * * A memento of the reads and writes that were performed on the map are * recorded in buffers. These buffers are drained at the first opportunity * after a write or when the read buffer exceeds a threshold size. The reads * are recorded in a lossy buffer, allowing the reordering operations to be * discarded if the draining process cannot keep up. Due to the concurrent * nature of the read and write operations a strict policy ordering is not * possible, but is observably strict when single threaded. * * Due to a lack of a strict ordering guarantee, a task can be executed * out-of-order, such as a removal followed by its addition. The state of the * entry is encoded within the value's weight. * * Alive: The entry is in both the hash-table and the page replacement policy. * This is represented by a positive weight. * * Retired: The entry is not in the hash-table and is pending removal from the * page replacement policy. This is represented by a negative weight. * * Dead: The entry is not in the hash-table and is not in the page replacement * policy. This is represented by a weight of zero. * * The Least Recently Used page replacement algorithm was chosen due to its * simplicity, high hit rate, and ability to be implemented with O(1) time * complexity. */ /** * The number of CPUs */ static final int NCPU = Runtime.getRuntime().availableProcessors(); /** * The maximum weighted capacity of the map. */ static final long MAXIMUM_CAPACITY = Long.MAX_VALUE - Integer.MAX_VALUE; /** * The number of read buffers to use. */ static final int NUMBER_OF_READ_BUFFERS = ceilingNextPowerOfTwo(NCPU); /** * Mask value for indexing into the read buffers. */ static final int READ_BUFFERS_MASK = NUMBER_OF_READ_BUFFERS - 1; /** * The number of pending read operations before attempting to drain. */ static final int READ_BUFFER_THRESHOLD = 32; /** * The maximum number of read operations to perform per amortized drain. */ static final int READ_BUFFER_DRAIN_THRESHOLD = 2 * READ_BUFFER_THRESHOLD; /** * The maximum number of pending reads per buffer. */ static final int READ_BUFFER_SIZE = 2 * READ_BUFFER_DRAIN_THRESHOLD; /** * Mask value for indexing into the read buffer. */ static final int READ_BUFFER_INDEX_MASK = READ_BUFFER_SIZE - 1; /** * The maximum number of write operations to perform per amortized drain. */ static final int WRITE_BUFFER_DRAIN_THRESHOLD = 16; /** * A queue that discards all entries. */ static final Queue DISCARDING_QUEUE = new DiscardingQueue(); static final long serialVersionUID = 1; // The backing data store holding the key-value associations final ConcurrentMap> data; final int concurrencyLevel; // These fields provide support to bound the map by a maximum capacity // @GuardedBy("evictionLock") final long[] readBufferReadCount; // @GuardedBy("evictionLock") final LinkedDeque> evictionDeque; // @GuardedBy("evictionLock") // must write under lock final AtomicLong weightedSize; // @GuardedBy("evictionLock") // must write under lock final AtomicLong capacity; final Lock evictionLock; final Queue writeBuffer; final AtomicLong[] readBufferWriteCount; final AtomicLong[] readBufferDrainAtWriteCount; final AtomicReference>[][] readBuffers; final AtomicReference drainStatus; final EntryWeigher weigher; // These fields provide support for notifying a listener. final Queue> pendingNotifications; final EvictionListener listener; transient Set keySet; transient Collection values; transient Set> entrySet; public ConcurrentLinkedHashMap() { this(128, Long.MAX_VALUE); } public ConcurrentLinkedHashMap(int initialCapacity, long maximumWeightedCapacity) { this(new Builder().initialCapacity(initialCapacity).maximumWeightedCapacity(maximumWeightedCapacity)); } /** * Creates an instance based on the builder's configuration. */ @SuppressWarnings({"unchecked", "cast"}) private ConcurrentLinkedHashMap(Builder builder) { // The data store and its maximum capacity concurrencyLevel = builder.concurrencyLevel; capacity = new AtomicLong(Math.min(builder.capacity, MAXIMUM_CAPACITY)); if (builder.referenceType == WeakReference.class) { data = new ConcurrentReferenceHashMap<>(builder.initialCapacity, 0.75f, concurrencyLevel, ConcurrentReferenceHashMap.ReferenceType.WEAK); } else if (builder.referenceType == SoftReference.class) { data = new ConcurrentReferenceHashMap<>(builder.initialCapacity, 0.75f, concurrencyLevel, ConcurrentReferenceHashMap.ReferenceType.SOFT); } else { data = new ConcurrentHashMap<>(builder.initialCapacity, 0.75f, concurrencyLevel); } // The eviction support weigher = builder.weigher; evictionLock = new ReentrantLock(); weightedSize = new AtomicLong(); evictionDeque = new LinkedDeque>(); writeBuffer = new ConcurrentLinkedQueue(); drainStatus = new AtomicReference(DrainStatus.IDLE); readBufferReadCount = new long[NUMBER_OF_READ_BUFFERS]; readBufferWriteCount = new AtomicLong[NUMBER_OF_READ_BUFFERS]; readBufferDrainAtWriteCount = new AtomicLong[NUMBER_OF_READ_BUFFERS]; readBuffers = new AtomicReference[NUMBER_OF_READ_BUFFERS][READ_BUFFER_SIZE]; for (int i = 0; i < NUMBER_OF_READ_BUFFERS; i++) { readBufferWriteCount[i] = new AtomicLong(); readBufferDrainAtWriteCount[i] = new AtomicLong(); readBuffers[i] = new AtomicReference[READ_BUFFER_SIZE]; for (int j = 0; j < READ_BUFFER_SIZE; j++) { readBuffers[i][j] = new AtomicReference>(); } } // The notification queue and listener listener = builder.listener; pendingNotifications = (listener == DiscardingListener.INSTANCE) ? (Queue>) DISCARDING_QUEUE : new ConcurrentLinkedQueue>(); } static int ceilingNextPowerOfTwo(int x) { // From Hacker's Delight, Chapter 3, Harry S. Warren Jr. return 1 << (Integer.SIZE - Integer.numberOfLeadingZeros(x - 1)); } /** * Ensures that the object is not null. */ static void checkNotNull(Object o) { if (o == null) { throw new NullPointerException(); } } /** * Ensures that the argument expression is true. */ static void checkArgument(boolean expression) { if (!expression) { throw new IllegalArgumentException(); } } /* ---------------- Eviction Support -------------- */ /** * Ensures that the state expression is true. */ static void checkState(boolean expression) { if (!expression) { throw new IllegalStateException(); } } /** * Returns the index to the read buffer to record into. */ static int readBufferIndex() { // A buffer is chosen by the thread's id so that tasks are distributed in a // pseudo evenly manner. This helps avoid hot entries causing contention // due to other threads trying to append to the same buffer. return ((int) Thread.currentThread().getId()) & READ_BUFFERS_MASK; } /** * Retrieves the maximum weighted capacity of the map. * * @return the maximum weighted capacity */ public long capacity() { return capacity.get(); } /** * Sets the maximum weighted capacity of the map and eagerly evicts entries * until it shrinks to the appropriate size. * * @param capacity the maximum weighted capacity of the map * @throws IllegalArgumentException if the capacity is negative */ public void setCapacity(long capacity) { checkArgument(capacity >= 0); evictionLock.lock(); try { this.capacity.lazySet(Math.min(capacity, MAXIMUM_CAPACITY)); drainBuffers(); evict(); } finally { evictionLock.unlock(); } notifyListener(); } /** * Determines whether the map has exceeded its capacity. */ // @GuardedBy("evictionLock") boolean hasOverflowed() { return weightedSize.get() > capacity.get(); } /** * Evicts entries from the map while it exceeds the capacity and appends * evicted entries to the notification queue for processing. */ // @GuardedBy("evictionLock") void evict() { // Attempts to evict entries from the map if it exceeds the maximum // capacity. If the eviction fails due to a concurrent removal of the // victim, that removal may cancel out the addition that triggered this // eviction. The victim is eagerly unlinked before the removal task so // that if an eviction is still required then a new victim will be chosen // for removal. while (hasOverflowed()) { final Node node = evictionDeque.poll(); // If weighted values are used, then the pending operations will adjust // the size to reflect the correct weight if (node == null) { return; } // Notify the listener only if the entry was evicted if (data.remove(node.key, node)) { pendingNotifications.add(node); } makeDead(node); } } /** * Performs the post-processing work required after a read. * * @param node the entry in the page replacement policy */ void afterRead(Node node) { final int bufferIndex = readBufferIndex(); final long writeCount = recordRead(bufferIndex, node); drainOnReadIfNeeded(bufferIndex, writeCount); notifyListener(); } /** * Records a read in the buffer and return its write count. * * @param bufferIndex the index to the chosen read buffer * @param node the entry in the page replacement policy * @return the number of writes on the chosen read buffer */ long recordRead(int bufferIndex, Node node) { // The location in the buffer is chosen in a racy fashion as the increment // is not atomic with the insertion. This means that concurrent reads can // overlap and overwrite one another, resulting in a lossy buffer. final AtomicLong counter = readBufferWriteCount[bufferIndex]; final long writeCount = counter.get(); counter.lazySet(writeCount + 1); final int index = (int) (writeCount & READ_BUFFER_INDEX_MASK); readBuffers[bufferIndex][index].lazySet(node); return writeCount; } /** * Attempts to drain the buffers if it is determined to be needed when * post-processing a read. * * @param bufferIndex the index to the chosen read buffer * @param writeCount the number of writes on the chosen read buffer */ void drainOnReadIfNeeded(int bufferIndex, long writeCount) { final long pending = (writeCount - readBufferDrainAtWriteCount[bufferIndex].get()); final boolean delayable = (pending < READ_BUFFER_THRESHOLD); final DrainStatus status = drainStatus.get(); if (status.shouldDrainBuffers(delayable)) { tryToDrainBuffers(); } } public DrainStatus getDrainStatus() { return drainStatus.get(); } /** * Performs the post-processing work required after a write. * * @param task the pending operation to be applied */ void afterWrite(Runnable task) { writeBuffer.add(task); drainStatus.lazySet(DrainStatus.REQUIRED); tryToDrainBuffers(); notifyListener(); } /** * Attempts to acquire the eviction lock and apply the pending operations, up * to the amortized threshold, to the page replacement policy. */ void tryToDrainBuffers() { if (evictionLock.tryLock()) { try { drainStatus.lazySet(DrainStatus.PROCESSING); drainBuffers(); } finally { drainStatus.compareAndSet(DrainStatus.PROCESSING, DrainStatus.IDLE); evictionLock.unlock(); } } } /** * Drains the read and write buffers up to an amortized threshold. */ // @GuardedBy("evictionLock") void drainBuffers() { drainReadBuffers(); drainWriteBuffer(); } /** * Drains the read buffers, each up to an amortized threshold. */ // @GuardedBy("evictionLock") void drainReadBuffers() { final int start = (int) Thread.currentThread().getId(); final int end = start + NUMBER_OF_READ_BUFFERS; for (int i = start; i < end; i++) { drainReadBuffer(i & READ_BUFFERS_MASK); } } /** * Drains the read buffer up to an amortized threshold. */ // @GuardedBy("evictionLock") void drainReadBuffer(int bufferIndex) { final long writeCount = readBufferWriteCount[bufferIndex].get(); for (int i = 0; i < READ_BUFFER_DRAIN_THRESHOLD; i++) { final int index = (int) (readBufferReadCount[bufferIndex] & READ_BUFFER_INDEX_MASK); final AtomicReference> slot = readBuffers[bufferIndex][index]; final Node node = slot.get(); if (node == null) { break; } slot.lazySet(null); applyRead(node); readBufferReadCount[bufferIndex]++; } readBufferDrainAtWriteCount[bufferIndex].lazySet(writeCount); } /** * Updates the node's location in the page replacement policy. */ // @GuardedBy("evictionLock") void applyRead(Node node) { // An entry may be scheduled for reordering despite having been removed. // This can occur when the entry was concurrently read while a writer was // removing it. If the entry is no longer linked then it does not need to // be processed. if (evictionDeque.contains(node)) { evictionDeque.moveToBack(node); } } /** * Drains the read buffer up to an amortized threshold. */ // @GuardedBy("evictionLock") void drainWriteBuffer() { for (int i = 0; i < WRITE_BUFFER_DRAIN_THRESHOLD; i++) { final Runnable task = writeBuffer.poll(); if (task == null) { break; } task.run(); } } /** * Attempts to transition the node from the alive state to the * retired state. * * @param node the entry in the page replacement policy * @param expect the expected weighted value * @return if successful */ boolean tryToRetire(Node node, WeightedValue expect) { if (expect.isAlive()) { final WeightedValue retired = new WeightedValue(expect.value, -expect.weight); return node.compareAndSet(expect, retired); } return false; } /** * Atomically transitions the node from the alive state to the * retired state, if a valid transition. * * @param node the entry in the page replacement policy */ void makeRetired(Node node) { for (; ; ) { final WeightedValue current = node.get(); if (!current.isAlive()) { return; } final WeightedValue retired = new WeightedValue(current.value, -current.weight); if (node.compareAndSet(current, retired)) { return; } } } /** * Atomically transitions the node to the dead state and decrements * the weightedSize. * * @param node the entry in the page replacement policy */ // @GuardedBy("evictionLock") void makeDead(Node node) { for (; ; ) { WeightedValue current = node.get(); WeightedValue dead = new WeightedValue(current.value, 0); if (node.compareAndSet(current, dead)) { weightedSize.lazySet(weightedSize.get() - Math.abs(current.weight)); return; } } } /** * Notifies the listener of entries that were evicted. */ void notifyListener() { Node node; while ((node = pendingNotifications.poll()) != null) { listener.onEviction(node.key, node.getValue()); } } @Override public boolean isEmpty() { return data.isEmpty(); } /* ---------------- Concurrent Map Support -------------- */ @Override public int size() { return data.size(); } /** * Returns the weighted size of this map. * * @return the combined weight of the values in this map */ public long weightedSize() { return Math.max(0, weightedSize.get()); } @Override public void clear() { evictionLock.lock(); try { // Discard all entries Node node; while ((node = evictionDeque.poll()) != null) { data.remove(node.key, node); makeDead(node); } // Discard all pending reads for (AtomicReference>[] buffer : readBuffers) { for (AtomicReference> slot : buffer) { slot.lazySet(null); } } // Apply all pending writes Runnable task; while ((task = writeBuffer.poll()) != null) { task.run(); } } finally { evictionLock.unlock(); } } @Override public boolean containsKey(Object key) { return data.containsKey(key); } @Override public boolean containsValue(Object value) { checkNotNull(value); for (Node node : data.values()) { if (node.getValue().equals(value)) { return true; } } return false; } @Override public V get(Object key) { final Node node = data.get(key); if (node == null) { return null; } afterRead(node); return node.getValue(); } /** * Returns the value to which the specified key is mapped, or {@code null} * if this map contains no mapping for the key. This method differs from * {@link #get(Object)} in that it does not record the operation with the * page replacement policy. * * @param key the key whose associated value is to be returned * @return the value to which the specified key is mapped, or * {@code null} if this map contains no mapping for the key * @throws NullPointerException if the specified key is null */ public V getQuietly(Object key) { final Node node = data.get(key); return (node == null) ? null : node.getValue(); } @Override public V put(K key, V value) { return put(key, value, false); } @Override public V putIfAbsent(K key, V value) { return put(key, value, true); } /** * Adds a node to the list and the data store. If an existing node is found, * then its value is updated if allowed. * * @param key key with which the specified value is to be associated * @param value value to be associated with the specified key * @param onlyIfAbsent a write is performed only if the key is not already * associated with a value * @return the prior value in the data store or null if no mapping was found */ V put(K key, V value, boolean onlyIfAbsent) { checkNotNull(key); checkNotNull(value); final int weight = weigher.weightOf(key, value); final WeightedValue weightedValue = new WeightedValue(value, weight); final Node node = new Node(key, weightedValue); for (; ; ) { final Node prior = data.putIfAbsent(node.key, node); if (prior == null) { afterWrite(new AddTask(node, weight)); return null; } else if (onlyIfAbsent) { afterRead(prior); return prior.getValue(); } for (; ; ) { final WeightedValue oldWeightedValue = prior.get(); if (!oldWeightedValue.isAlive()) { break; } if (prior.compareAndSet(oldWeightedValue, weightedValue)) { final int weightedDifference = weight - oldWeightedValue.weight; if (weightedDifference == 0) { afterRead(prior); } else { afterWrite(new UpdateTask(prior, weightedDifference)); } return oldWeightedValue.value; } } } } @Override public V remove(Object key) { final Node node = data.remove(key); if (node == null) { return null; } makeRetired(node); afterWrite(new RemovalTask(node)); return node.getValue(); } @Override public boolean remove(Object key, Object value) { final Node node = data.get(key); if ((node == null) || (value == null)) { return false; } WeightedValue weightedValue = node.get(); for (; ; ) { if (weightedValue.contains(value)) { if (tryToRetire(node, weightedValue)) { if (data.remove(key, node)) { afterWrite(new RemovalTask(node)); return true; } } else { weightedValue = node.get(); if (weightedValue.isAlive()) { // retry as an intermediate update may have replaced the value with // an equal instance that has a different reference identity continue; } } } return false; } } @Override public V replace(K key, V value) { checkNotNull(key); checkNotNull(value); final int weight = weigher.weightOf(key, value); final WeightedValue weightedValue = new WeightedValue(value, weight); final Node node = data.get(key); if (node == null) { return null; } for (; ; ) { final WeightedValue oldWeightedValue = node.get(); if (!oldWeightedValue.isAlive()) { return null; } if (node.compareAndSet(oldWeightedValue, weightedValue)) { final int weightedDifference = weight - oldWeightedValue.weight; if (weightedDifference == 0) { afterRead(node); } else { afterWrite(new UpdateTask(node, weightedDifference)); } return oldWeightedValue.value; } } } @Override public boolean replace(K key, V oldValue, V newValue) { checkNotNull(key); checkNotNull(oldValue); checkNotNull(newValue); final int weight = weigher.weightOf(key, newValue); final WeightedValue newWeightedValue = new WeightedValue(newValue, weight); final Node node = data.get(key); if (node == null) { return false; } for (; ; ) { final WeightedValue weightedValue = node.get(); if (!weightedValue.isAlive() || !weightedValue.contains(oldValue)) { return false; } if (node.compareAndSet(weightedValue, newWeightedValue)) { final int weightedDifference = weight - weightedValue.weight; if (weightedDifference == 0) { afterRead(node); } else { afterWrite(new UpdateTask(node, weightedDifference)); } return true; } } } @Override public Set keySet() { final Set ks = keySet; return (ks == null) ? (keySet = new KeySet()) : ks; } /** * Returns a unmodifiable snapshot {@link Set} view of the keys contained in * this map. The set's iterator returns the keys whose order of iteration is * the ascending order in which its entries are considered eligible for * retention, from the least-likely to be retained to the most-likely. *

* Beware that, unlike in {@link #keySet()}, obtaining the set is NOT * a constant-time operation. Because of the asynchronous nature of the page * replacement policy, determining the retention ordering requires a traversal * of the keys. * * @return an ascending snapshot view of the keys in this map */ public Set ascendingKeySet() { return ascendingKeySetWithLimit(Integer.MAX_VALUE); } /** * Returns an unmodifiable snapshot {@link Set} view of the keys contained in * this map. The set's iterator returns the keys whose order of iteration is * the ascending order in which its entries are considered eligible for * retention, from the least-likely to be retained to the most-likely. *

* Beware that, unlike in {@link #keySet()}, obtaining the set is NOT * a constant-time operation. Because of the asynchronous nature of the page * replacement policy, determining the retention ordering requires a traversal * of the keys. * * @param limit the maximum size of the returned set * @return a ascending snapshot view of the keys in this map * @throws IllegalArgumentException if the limit is negative */ public Set ascendingKeySetWithLimit(int limit) { return orderedKeySet(true, limit); } /** * Returns an unmodifiable snapshot {@link Set} view of the keys contained in * this map. The set's iterator returns the keys whose order of iteration is * the descending order in which its entries are considered eligible for * retention, from the most-likely to be retained to the least-likely. *

* Beware that, unlike in {@link #keySet()}, obtaining the set is NOT * a constant-time operation. Because of the asynchronous nature of the page * replacement policy, determining the retention ordering requires a traversal * of the keys. * * @return a descending snapshot view of the keys in this map */ public Set descendingKeySet() { return descendingKeySetWithLimit(Integer.MAX_VALUE); } /** * Returns an unmodifiable snapshot {@link Set} view of the keys contained in * this map. The set's iterator returns the keys whose order of iteration is * the descending order in which its entries are considered eligible for * retention, from the most-likely to be retained to the least-likely. *

* Beware that, unlike in {@link #keySet()}, obtaining the set is NOT * a constant-time operation. Because of the asynchronous nature of the page * replacement policy, determining the retention ordering requires a traversal * of the keys. * * @param limit the maximum size of the returned set * @return a descending snapshot view of the keys in this map * @throws IllegalArgumentException if the limit is negative */ public Set descendingKeySetWithLimit(int limit) { return orderedKeySet(false, limit); } Set orderedKeySet(boolean ascending, int limit) { checkArgument(limit >= 0); evictionLock.lock(); try { drainBuffers(); final int initialCapacity = (weigher == Weighers.entrySingleton()) ? Math.min(limit, (int) weightedSize()) : 16; final Set keys = new LinkedHashSet(initialCapacity); final Iterator> iterator = ascending ? evictionDeque.iterator() : evictionDeque.descendingIterator(); while (iterator.hasNext() && (limit > keys.size())) { keys.add(iterator.next().key); } return unmodifiableSet(keys); } finally { evictionLock.unlock(); } } @Override public Collection values() { final Collection vs = values; return (vs == null) ? (values = new Values()) : vs; } @Override public Set> entrySet() { final Set> es = entrySet; return (es == null) ? (entrySet = new EntrySet()) : es; } /** * Returns an unmodifiable snapshot {@link Map} view of the mappings contained * in this map. The map's collections return the mappings whose order of * iteration is the ascending order in which its entries are considered * eligible for retention, from the least-likely to be retained to the * most-likely. *

* Beware that obtaining the mappings is NOT a constant-time * operation. Because of the asynchronous nature of the page replacement * policy, determining the retention ordering requires a traversal of the * entries. * * @return a ascending snapshot view of this map */ public Map ascendingMap() { return ascendingMapWithLimit(Integer.MAX_VALUE); } /** * Returns an unmodifiable snapshot {@link Map} view of the mappings contained * in this map. The map's collections return the mappings whose order of * iteration is the ascending order in which its entries are considered * eligible for retention, from the least-likely to be retained to the * most-likely. *

* Beware that obtaining the mappings is NOT a constant-time * operation. Because of the asynchronous nature of the page replacement * policy, determining the retention ordering requires a traversal of the * entries. * * @param limit the maximum size of the returned map * @return a ascending snapshot view of this map * @throws IllegalArgumentException if the limit is negative */ public Map ascendingMapWithLimit(int limit) { return orderedMap(true, limit); } /** * Returns an unmodifiable snapshot {@link Map} view of the mappings contained * in this map. The map's collections return the mappings whose order of * iteration is the descending order in which its entries are considered * eligible for retention, from the most-likely to be retained to the * least-likely. *

* Beware that obtaining the mappings is NOT a constant-time * operation. Because of the asynchronous nature of the page replacement * policy, determining the retention ordering requires a traversal of the * entries. * * @return a descending snapshot view of this map */ public Map descendingMap() { return descendingMapWithLimit(Integer.MAX_VALUE); } /** * Returns an unmodifiable snapshot {@link Map} view of the mappings contained * in this map. The map's collections return the mappings whose order of * iteration is the descending order in which its entries are considered * eligible for retention, from the most-likely to be retained to the * least-likely. *

* Beware that obtaining the mappings is NOT a constant-time * operation. Because of the asynchronous nature of the page replacement * policy, determining the retention ordering requires a traversal of the * entries. * * @param limit the maximum size of the returned map * @return a descending snapshot view of this map * @throws IllegalArgumentException if the limit is negative */ public Map descendingMapWithLimit(int limit) { return orderedMap(false, limit); } Map orderedMap(boolean ascending, int limit) { checkArgument(limit >= 0); evictionLock.lock(); try { drainBuffers(); final int initialCapacity = (weigher == Weighers.entrySingleton()) ? Math.min(limit, (int) weightedSize()) : 16; final Map map = new LinkedHashMap(initialCapacity); final Iterator> iterator = ascending ? evictionDeque.iterator() : evictionDeque.descendingIterator(); while (iterator.hasNext() && (limit > map.size())) { Node node = iterator.next(); map.put(node.key, node.getValue()); } return unmodifiableMap(map); } finally { evictionLock.unlock(); } } Object writeReplace() { return new SerializationProxy(this); } private void readObject(ObjectInputStream stream) throws InvalidObjectException { throw new InvalidObjectException("Proxy required"); } /** * The draining status of the buffers. */ public enum DrainStatus { /** * A drain is not taking place. */ IDLE { @Override boolean shouldDrainBuffers(boolean delayable) { return !delayable; } }, /** * A drain is required due to a pending write modification. */ REQUIRED { @Override boolean shouldDrainBuffers(boolean delayable) { return true; } }, /** * A drain is in progress. */ PROCESSING { @Override boolean shouldDrainBuffers(boolean delayable) { return false; } }; /** * Determines whether the buffers should be drained. * * @param delayable if a drain should be delayed until required * @return if a drain should be attempted */ abstract boolean shouldDrainBuffers(boolean delayable); } /** * A listener that ignores all notifications. */ enum DiscardingListener implements EvictionListener { INSTANCE; @Override public void onEviction(Object key, Object value) { } } /** * An element that is linked on the {@link Deque}. */ public interface Linked> { /** * Retrieves the previous element or null if either the element is * unlinked or the first element on the deque. * * @return previous */ T getPrevious(); /** * Sets the previous element or null if there is no link. * * @param prev prev */ void setPrevious(T prev); /** * Retrieves the next element or null if either the element is * unlinked or the last element on the deque. * * @return Next */ T getNext(); /** * Sets the next element or null if there is no link. * * @param next next */ void setNext(T next); } public interface EvictionListener { /** * A call-back notification that the entry was evicted. * * @param key the entry's key * @param value the entry's value */ void onEviction(K key, V value); } public interface Weigher { /** * Measures an object's weight to determine how many units of capacity that * the value consumes. A value must consume a minimum of one unit. * * @param value the object to weigh * @return the object's weight */ int weightOf(V value); } public interface EntryWeigher { /** * Measures an entry's weight to determine how many units of capacity that * the key and value consumes. An entry must consume a minimum of one unit. * * @param key the key to weigh * @param value the value to weigh * @return the entry's weight */ int weightOf(K key, V value); } /** * A value, its weight, and the entry's status. */ // @Immutable static final class WeightedValue { final int weight; final V value; WeightedValue(V value, int weight) { this.weight = weight; this.value = value; } boolean contains(Object o) { return (o == value) || value.equals(o); } /** * If the entry is available in the hash-table and page replacement policy. */ boolean isAlive() { return weight > 0; } /** * If the entry was removed from the hash-table and is awaiting removal from * the page replacement policy. */ boolean isRetired() { return weight < 0; } /** * If the entry was removed from the hash-table and the page replacement * policy. */ boolean isDead() { return weight == 0; } } /** * A node contains the key, the weighted value, and the linkage pointers on * the page-replacement algorithm's data structures. */ @SuppressWarnings("serial") static final class Node extends AtomicReference> implements Linked> { final K key; // @GuardedBy("evictionLock") Node prev; // @GuardedBy("evictionLock") Node next; /** * Creates a new, unlinked node. */ Node(K key, WeightedValue weightedValue) { super(weightedValue); this.key = key; } @Override // @GuardedBy("evictionLock") public Node getPrevious() { return prev; } @Override // @GuardedBy("evictionLock") public void setPrevious(Node prev) { this.prev = prev; } @Override // @GuardedBy("evictionLock") public Node getNext() { return next; } @Override // @GuardedBy("evictionLock") public void setNext(Node next) { this.next = next; } /** * Retrieves the value held by the current WeightedValue. */ V getValue() { return get().value; } } /** * A weigher that enforces that the weight falls within a valid range. */ static final class BoundedEntryWeigher implements EntryWeigher, Serializable { static final long serialVersionUID = 1; final EntryWeigher weigher; BoundedEntryWeigher(EntryWeigher weigher) { checkNotNull(weigher); this.weigher = weigher; } @Override public int weightOf(K key, V value) { int weight = weigher.weightOf(key, value); checkArgument(weight >= 1); return weight; } Object writeReplace() { return weigher; } } /** * A queue that discards all additions and is always empty. */ static final class DiscardingQueue extends AbstractQueue { @Override public boolean add(Object e) { return true; } @Override public boolean offer(Object e) { return true; } @Override public Object poll() { return null; } @Override public Object peek() { return null; } @Override public int size() { return 0; } @Override public Iterator iterator() { return emptyIterator(); } } /** * A proxy that is serialized instead of the map. The page-replacement * algorithm's data structures are not serialized so the deserialized * instance contains only the entries. This is acceptable as caches hold * transient data that is recomputable and serialization would tend to be * used as a fast warm-up process. */ static final class SerializationProxy implements Serializable { static final long serialVersionUID = 1; final EntryWeigher weigher; final EvictionListener listener; final int concurrencyLevel; final Map data; final long capacity; SerializationProxy(ConcurrentLinkedHashMap map) { concurrencyLevel = map.concurrencyLevel; data = new HashMap(map); capacity = map.capacity.get(); listener = map.listener; weigher = map.weigher; } Object readResolve() { ConcurrentLinkedHashMap map = new Builder() .concurrencyLevel(concurrencyLevel) .maximumWeightedCapacity(capacity) .listener(listener) .weigher(weigher) .build(); map.putAll(data); return map; } } /** * A builder that creates {@link ConcurrentLinkedHashMap} instances. It * provides a flexible approach for constructing customized instances with * a named parameter syntax. It can be used in the following manner: *
{@code
     * ConcurrentMap> graph = new Builder>()
     *     .maximumWeightedCapacity(5000)
     *     .weigher(Weighers.set())
     *     .build();
     * }
*/ public static final class Builder { static final int DEFAULT_CONCURRENCY_LEVEL = 16; static final int DEFAULT_INITIAL_CAPACITY = 16; EvictionListener listener; EntryWeigher weigher; int concurrencyLevel; int initialCapacity; long capacity; /** * null is FinalReference * {@link WeakReference} * {@link SoftReference} */ Class referenceType; @SuppressWarnings("unchecked") public Builder() { capacity = -1; weigher = Weighers.entrySingleton(); initialCapacity = DEFAULT_INITIAL_CAPACITY; concurrencyLevel = DEFAULT_CONCURRENCY_LEVEL; listener = (EvictionListener) DiscardingListener.INSTANCE; } public Builder referenceType(Class referenceType) { this.referenceType = referenceType; return this; } /** * Specifies the initial capacity of the hash table (default 16). * This is the number of key-value pairs that the hash table can hold * before a resize operation is required. * * @param initialCapacity the initial capacity used to size the hash table * to accommodate this many entries. * @return Builder * @throws IllegalArgumentException if the initialCapacity is negative */ public Builder initialCapacity(int initialCapacity) { checkArgument(initialCapacity >= 0); this.initialCapacity = initialCapacity; return this; } /** * Specifies the maximum weighted capacity to coerce the map to and may * exceed it temporarily. * * @param capacity the weighted threshold to bound the map by * @return Builder * @throws IllegalArgumentException if the maximumWeightedCapacity is * negative */ public Builder maximumWeightedCapacity(long capacity) { checkArgument(capacity >= 0); this.capacity = capacity; return this; } /** * Specifies the estimated number of concurrently updating threads. The * implementation performs internal sizing to try to accommodate this many * threads (default 16). * * @param concurrencyLevel the estimated number of concurrently updating * threads * @return Builder * @throws IllegalArgumentException if the concurrencyLevel is less than or * equal to zero */ public Builder concurrencyLevel(int concurrencyLevel) { checkArgument(concurrencyLevel > 0); this.concurrencyLevel = concurrencyLevel; return this; } /** * Specifies an optional listener that is registered for notification when * an entry is evicted. * * @param listener the object to forward evicted entries to * @return Builder * @throws NullPointerException if the listener is null */ public Builder listener(EvictionListener listener) { checkNotNull(listener); this.listener = listener; return this; } /** * Specifies an algorithm to determine how many the units of capacity a * value consumes. The default algorithm bounds the map by the number of * key-value pairs by giving each entry a weight of 1. * * @param weigher the algorithm to determine a value's weight * @return Builder * @throws NullPointerException if the weigher is null */ public Builder weigher(Weigher weigher) { this.weigher = (weigher == Weighers.singleton()) ? Weighers.entrySingleton() : new BoundedEntryWeigher(Weighers.asEntryWeigher(weigher)); return this; } /** * Specifies an algorithm to determine how many the units of capacity an * entry consumes. The default algorithm bounds the map by the number of * key-value pairs by giving each entry a weight of 1. * * @param weigher the algorithm to determine a entry's weight * @return Builder * @throws NullPointerException if the weigher is null */ public Builder weigher(EntryWeigher weigher) { this.weigher = (weigher == Weighers.entrySingleton()) ? Weighers.entrySingleton() : new BoundedEntryWeigher(weigher); return this; } /** * Creates a new {@link ConcurrentLinkedHashMap} instance. * * @return ConcurrentLinkedHashMap * @throws IllegalStateException if the maximum weighted capacity was * not set */ public ConcurrentLinkedHashMap build() { checkState(capacity >= 0); return new ConcurrentLinkedHashMap(this); } } /* ---------------- Serialization Support -------------- */ /** * Linked list implementation of the the JDK6 Deque interface where the link * pointers are tightly integrated with the element. Linked deques have no * capacity restrictions; they grow as necessary to support usage. They are not * thread-safe; in the absence of external synchronization, they do not support * concurrent access by multiple threads. Null elements are prohibited. *

* Most LinkedDeque operations run in constant time by assuming that * the {@link Linked} parameter is associated with the deque instance. Any usage * that violates this assumption will result in non-deterministic behavior. *

* The iterators returned by this class are not fail-fast: If * the deque is modified at any time after the iterator is created, the iterator * will be in an unknown state. Thus, in the face of concurrent modification, * the iterator risks arbitrary, non-deterministic behavior at an undetermined * time in the future. * * @param the type of elements held in this collection * @author [email protected] (Ben Manes) * @see * http://code.google.com/p/concurrentlinkedhashmap/ */ // @NotThreadSafe public final static class LinkedDeque> extends AbstractCollection { // This class provides a doubly-linked list that is optimized for the virtual // machine. The first and last elements are manipulated instead of a slightly // more convenient sentinel element to avoid the insertion of null checks with // NullPointerException throws in the byte code. The links to a removed // element are cleared to help a generational garbage collector if the // discarded elements inhabit more than one generation. /** * Pointer to first node. * Invariant: (first == null && last == null) || * (first.prev == null) */ E first; /** * Pointer to last node. * Invariant: (first == null && last == null) || * (last.next == null) */ E last; int size; /** * Links the element to the front of the deque so that it becomes the first * element. * * @param e the unlinked element */ void linkFirst(final E e) { final E f = first; first = e; if (f == null) { last = e; } else { f.setPrevious(e); e.setNext(f); } } /** * Links the element to the back of the deque so that it becomes the last * element. * * @param e the unlinked element */ void linkLast(final E e) { final E previousLast = last; last = e; if (previousLast == null) { first = e; } else { previousLast.setNext(e); e.setPrevious(previousLast); } } /** * Unlinks the non-null first element. */ E unlinkFirst() { final E f = first; final E next = f.getNext(); f.setNext(null); first = next; if (next == null) { last = null; } else { next.setPrevious(null); } return f; } /** * Unlinks the non-null last element. */ E unlinkLast() { final E l = last; final E prev = l.getPrevious(); l.setPrevious(null); last = prev; if (prev == null) { first = null; } else { prev.setNext(null); } return l; } /** * Unlinks the non-null element. */ void unlink(E e) { final E prev = e.getPrevious(); final E next = e.getNext(); if (prev == null) { first = next; } else { prev.setNext(next); e.setPrevious(null); } if (next == null) { last = prev; } else { next.setPrevious(prev); e.setNext(null); } } @Override public boolean isEmpty() { return (first == null); } void checkNotEmpty() { if (isEmpty()) { throw new NoSuchElementException(); } } @Override public int size() { return size; } @Override public void clear() { for (E e = first; e != null; ) { E next = e.getNext(); e.setPrevious(null); e.setNext(null); e = next; } first = last = null; size = 0; } @Override public boolean contains(Object o) { if (!(o instanceof Linked)) { return false; } Linked e = (Linked) o; return (e.getPrevious() != null) || (e.getNext() != null) || (e == first); } /** * Moves the element to the front of the deque so that it becomes the first * element. * * @param e the linked element */ public void moveToFront(E e) { if (e != first) { unlink(e); linkFirst(e); } } /** * Moves the element to the back of the deque so that it becomes the last * element. * * @param e the linked element */ public void moveToBack(E e) { if (e != last) { unlink(e); linkLast(e); } } public E peek() { return peekFirst(); } public E peekFirst() { return first; } public E peekLast() { return last; } public E getFirst() { checkNotEmpty(); return peekFirst(); } public E getLast() { checkNotEmpty(); return peekLast(); } public E element() { return getFirst(); } public boolean offer(E e) { return offerLast(e); } public boolean offerFirst(E e) { if (contains(e)) { return false; } size++; linkFirst(e); return true; } public boolean offerLast(E e) { if (contains(e)) { return false; } size++; linkLast(e); return true; } @Override public boolean add(E e) { return offerLast(e); } public void addFirst(E e) { if (!offerFirst(e)) { throw new IllegalArgumentException(); } } public void addLast(E e) { if (!offerLast(e)) { throw new IllegalArgumentException(); } } public E poll() { return pollFirst(); } public E pollFirst() { if (isEmpty()) { return null; } size--; return unlinkFirst(); } public E pollLast() { if (isEmpty()) { return null; } size--; return unlinkLast(); } public E remove() { return removeFirst(); } @Override @SuppressWarnings("unchecked") public boolean remove(Object o) { if (contains(o)) { size--; unlink((E) o); return true; } return false; } public E removeFirst() { checkNotEmpty(); return pollFirst(); } public boolean removeFirstOccurrence(Object o) { return remove(o); } public E removeLast() { checkNotEmpty(); return pollLast(); } public boolean removeLastOccurrence(Object o) { return remove(o); } @Override public boolean removeAll(Collection c) { boolean modified = false; for (Object o : c) { modified |= remove(o); } return modified; } public void push(E e) { addFirst(e); } public E pop() { return removeFirst(); } @Override public Iterator iterator() { return new AbstractLinkedIterator(first) { @Override E computeNext() { return cursor.getNext(); } }; } public Iterator descendingIterator() { return new AbstractLinkedIterator(last) { @Override E computeNext() { return cursor.getPrevious(); } }; } abstract class AbstractLinkedIterator implements Iterator { E cursor; /** * Creates an iterator that can can traverse the deque. * * @param start the initial element to begin traversal from */ AbstractLinkedIterator(E start) { cursor = start; } @Override public boolean hasNext() { return (cursor != null); } @Override public E next() { if (!hasNext()) { throw new NoSuchElementException(); } E e = cursor; cursor = computeNext(); return e; } @Override public void remove() { throw new UnsupportedOperationException(); } /** * Retrieves the next element to traverse to or null if there are * no more elements. */ abstract E computeNext(); } } /** * A common set of {@link Weigher} implementations. * * @author [email protected] (Ben Manes) * @see * http://code.google.com/p/concurrentlinkedhashmap/ */ public final static class Weighers { /** * A entry weigher backed by the specified weigher. The weight of the value * determines the weight of the entry. * * @param key * @param value * @param weigher the weigher to be "wrapped" in a entry weigher. * @return A entry weigher view of the specified weigher. */ public static EntryWeigher asEntryWeigher( final Weigher weigher) { return (weigher == singleton()) ? Weighers.entrySingleton() : new EntryWeigherView(weigher); } /** * A weigher where an entry has a weight of 1. A map bounded with * this weigher will evict when the number of key-value pairs exceeds the * capacity. * * @param key * @param value * @return A weigher where a value takes one unit of capacity. */ @SuppressWarnings({"cast", "unchecked"}) public static EntryWeigher entrySingleton() { return (EntryWeigher) SingletonEntryWeigher.INSTANCE; } /** * A weigher where a value has a weight of 1. A map bounded with * this weigher will evict when the number of key-value pairs exceeds the * capacity. * * @param value * @return A weigher where a value takes one unit of capacity. */ @SuppressWarnings({"cast", "unchecked"}) public static Weigher singleton() { return (Weigher) SingletonWeigher.INSTANCE; } /** * A weigher where the value is a byte array and its weight is the number of * bytes. A map bounded with this weigher will evict when the number of bytes * exceeds the capacity rather than the number of key-value pairs in the map. * This allows for restricting the capacity based on the memory-consumption * and is primarily for usage by dedicated caching servers that hold the * serialized data. *

* A value with a weight of 0 will be rejected by the map. If a value * with this weight can occur then the caller should eagerly evaluate the * value and treat it as a removal operation. Alternatively, a custom weigher * may be specified on the map to assign an empty value a positive weight. * * @return A weigher where each byte takes one unit of capacity. */ public static Weigher byteArray() { return ByteArrayWeigher.INSTANCE; } /** * A weigher where the value is a {@link Iterable} and its weight is the * number of elements. This weigher only should be used when the alternative * {@link #collection()} weigher cannot be, as evaluation takes O(n) time. A * map bounded with this weigher will evict when the total number of elements * exceeds the capacity rather than the number of key-value pairs in the map. *

* A value with a weight of 0 will be rejected by the map. If a value * with this weight can occur then the caller should eagerly evaluate the * value and treat it as a removal operation. Alternatively, a custom weigher * may be specified on the map to assign an empty value a positive weight. * * @param element * @return A weigher where each element takes one unit of capacity. */ @SuppressWarnings({"cast", "unchecked"}) public static Weigher> iterable() { return IterableWeigher.INSTANCE; } /** * A weigher where the value is a {@link Collection} and its weight is the * number of elements. A map bounded with this weigher will evict when the * total number of elements exceeds the capacity rather than the number of * key-value pairs in the map. *

* A value with a weight of 0 will be rejected by the map. If a value * with this weight can occur then the caller should eagerly evaluate the * value and treat it as a removal operation. Alternatively, a custom weigher * may be specified on the map to assign an empty value a positive weight. * * @param element * @return A weigher where each element takes one unit of capacity. */ @SuppressWarnings({"cast", "unchecked"}) public static Weigher> collection() { return CollectionWeigher.INSTANCE; } /** * A weigher where the value is a {@link List} and its weight is the number * of elements. A map bounded with this weigher will evict when the total * number of elements exceeds the capacity rather than the number of * key-value pairs in the map. *

* A value with a weight of 0 will be rejected by the map. If a value * with this weight can occur then the caller should eagerly evaluate the * value and treat it as a removal operation. Alternatively, a custom weigher * may be specified on the map to assign an empty value a positive weight. * * @param element * @return A weigher where each element takes one unit of capacity. */ @SuppressWarnings({"cast", "unchecked"}) public static Weigher> list() { return ListWeigher.INSTANCE; } /** * A weigher where the value is a {@link Set} and its weight is the number * of elements. A map bounded with this weigher will evict when the total * number of elements exceeds the capacity rather than the number of * key-value pairs in the map. *

* A value with a weight of 0 will be rejected by the map. If a value * with this weight can occur then the caller should eagerly evaluate the * value and treat it as a removal operation. Alternatively, a custom weigher * may be specified on the map to assign an empty value a positive weight. * * @param element * @return A weigher where each element takes one unit of capacity. */ @SuppressWarnings({"cast", "unchecked"}) public static Weigher> set() { return SetWeigher.INSTANCE; } /** * A weigher where the value is a {@link Map} and its weight is the number of * entries. A map bounded with this weigher will evict when the total number of * entries across all values exceeds the capacity rather than the number of * key-value pairs in the map. *

* A value with a weight of 0 will be rejected by the map. If a value * with this weight can occur then the caller should eagerly evaluate the * value and treat it as a removal operation. Alternatively, a custom weigher * may be specified on the map to assign an empty value a positive weight. * * @param KEY * @param VALUE * @return A weigher where each entry takes one unit of capacity. */ @SuppressWarnings({"cast", "unchecked"}) public static Weigher> map() { return MapWeigher.INSTANCE; } enum SingletonEntryWeigher implements EntryWeigher { INSTANCE; @Override public int weightOf(Object key, Object value) { return 1; } } enum SingletonWeigher implements Weigher { INSTANCE; @Override public int weightOf(Object value) { return 1; } } enum ByteArrayWeigher implements Weigher { INSTANCE; @Override public int weightOf(byte[] value) { return value.length; } } enum IterableWeigher implements Weigher> { INSTANCE; @Override public int weightOf(Iterable values) { if (values instanceof Collection) { return ((Collection) values).size(); } int size = 0; for (Iterator i = values.iterator(); i.hasNext(); ) { i.next(); size++; } return size; } } enum CollectionWeigher implements Weigher> { INSTANCE; @Override public int weightOf(Collection values) { return values.size(); } } enum ListWeigher implements Weigher> { INSTANCE; @Override public int weightOf(List values) { return values.size(); } } enum SetWeigher implements Weigher> { INSTANCE; @Override public int weightOf(Set values) { return values.size(); } } enum MapWeigher implements Weigher> { INSTANCE; @Override public int weightOf(Map values) { return values.size(); } } static final class EntryWeigherView implements EntryWeigher, Serializable { static final long serialVersionUID = 1; final Weigher weigher; EntryWeigherView(Weigher weigher) { checkNotNull(weigher); this.weigher = weigher; } @Override public int weightOf(K key, V value) { return weigher.weightOf(value); } } } /** * Adds the node to the page replacement policy. */ final class AddTask implements Runnable { final Node node; final int weight; AddTask(Node node, int weight) { this.weight = weight; this.node = node; } @Override // @GuardedBy("evictionLock") public void run() { weightedSize.lazySet(weightedSize.get() + weight); // ignore out-of-order write operations if (node.get().isAlive()) { evictionDeque.add(node); evict(); } } } /** * Removes a node from the page replacement policy. */ final class RemovalTask implements Runnable { final Node node; RemovalTask(Node node) { this.node = node; } @Override // @GuardedBy("evictionLock") public void run() { // add may not have been processed yet evictionDeque.remove(node); makeDead(node); } } /* ---------------- Builder -------------- */ /** * Updates the weighted size and evicts an entry on overflow. */ final class UpdateTask implements Runnable { final int weightDifference; final Node node; public UpdateTask(Node node, int weightDifference) { this.weightDifference = weightDifference; this.node = node; } @Override // @GuardedBy("evictionLock") public void run() { weightedSize.lazySet(weightedSize.get() + weightDifference); applyRead(node); evict(); } } /** * An adapter to safely externalize the keys. */ final class KeySet extends AbstractSet { final ConcurrentLinkedHashMap map = ConcurrentLinkedHashMap.this; @Override public int size() { return map.size(); } @Override public void clear() { map.clear(); } @Override public Iterator iterator() { return new KeyIterator(); } @Override public boolean contains(Object obj) { return containsKey(obj); } @Override public boolean remove(Object obj) { return (map.remove(obj) != null); } @Override public Object[] toArray() { return map.data.keySet().toArray(); } @Override public T[] toArray(T[] array) { return map.data.keySet().toArray(array); } } /** * An adapter to safely externalize the key iterator. */ final class KeyIterator implements Iterator { final Iterator iterator = data.keySet().iterator(); K current; @Override public boolean hasNext() { return iterator.hasNext(); } @Override public K next() { current = iterator.next(); return current; } @Override public void remove() { checkState(current != null); ConcurrentLinkedHashMap.this.remove(current); current = null; } } /** * An adapter to safely externalize the values. */ final class Values extends AbstractCollection { @Override public int size() { return ConcurrentLinkedHashMap.this.size(); } @Override public void clear() { ConcurrentLinkedHashMap.this.clear(); } @Override public Iterator iterator() { return new ValueIterator(); } @Override public boolean contains(Object o) { return containsValue(o); } } /** * An adapter to safely externalize the value iterator. */ final class ValueIterator implements Iterator { final Iterator> iterator = data.values().iterator(); Node current; @Override public boolean hasNext() { return iterator.hasNext(); } @Override public V next() { current = iterator.next(); return current.getValue(); } @Override public void remove() { checkState(current != null); ConcurrentLinkedHashMap.this.remove(current.key); current = null; } } /** * An adapter to safely externalize the entries. */ final class EntrySet extends AbstractSet> { final ConcurrentLinkedHashMap map = ConcurrentLinkedHashMap.this; @Override public int size() { return map.size(); } @Override public void clear() { map.clear(); } @Override public Iterator> iterator() { return new EntryIterator(); } @Override public boolean contains(Object obj) { if (!(obj instanceof Entry)) { return false; } Entry entry = (Entry) obj; Node node = map.data.get(entry.getKey()); return (node != null) && (node.getValue().equals(entry.getValue())); } @Override public boolean add(Entry entry) { return (map.putIfAbsent(entry.getKey(), entry.getValue()) == null); } @Override public boolean remove(Object obj) { if (!(obj instanceof Entry)) { return false; } Entry entry = (Entry) obj; return map.remove(entry.getKey(), entry.getValue()); } } /** * An adapter to safely externalize the entry iterator. */ final class EntryIterator implements Iterator> { final Iterator> iterator = data.values().iterator(); Node current; @Override public boolean hasNext() { return iterator.hasNext(); } @Override public Entry next() { current = iterator.next(); return new WriteThroughEntry(current); } @Override public void remove() { checkState(current != null); ConcurrentLinkedHashMap.this.remove(current.key); current = null; } } /** * An entry that allows updates to write through to the map. */ final class WriteThroughEntry extends SimpleEntry { static final long serialVersionUID = 1; WriteThroughEntry(Node node) { super(node.key, node.getValue()); } @Override public V setValue(V value) { put(getKey(), value); return super.setValue(value); } Object writeReplace() { return new SimpleEntry(this); } } }