javax.measure
diff --git a/src/main/java/com/fasterxml/jackson/databind/util/LRUMap.java b/src/main/java/com/fasterxml/jackson/databind/util/LRUMap.java
index cfd31aa498..2ce8fe3a09 100644
--- a/src/main/java/com/fasterxml/jackson/databind/util/LRUMap.java
+++ b/src/main/java/com/fasterxml/jackson/databind/util/LRUMap.java
@@ -1,7 +1,6 @@
package com.fasterxml.jackson.databind.util;
-import java.io.*;
-import java.util.concurrent.ConcurrentHashMap;
+import com.fasterxml.jackson.databind.util.internal.PrivateMaxEntriesMap;
/**
* Helper for simple bounded maps used for reusing lookup values.
@@ -10,44 +9,36 @@
* on assumption that all use cases are for caching where persistence
* does not make sense. The only thing serialized is the cache size of Map.
*
- * NOTE: since version 2.4.2, this is NOT an LRU-based at all; reason
- * being that it is not possible to use JDK components that do LRU _AND_ perform
- * well wrt synchronization on multi-core systems. So we choose efficient synchronization
- * over potentially more efficient handling of entries.
+ * NOTE: since Jackson 2.14, the implementation evicts the least recently used
+ * entry when max size is reached.
*
- * And yes, there are efficient LRU implementations such as
- * concurrentlinkedhashmap ;
- * but at this point we really try to keep external deps to minimum.
- * Plan from Jackson 2.12 is to focus more on pluggability as {@link LookupCache} and
- * let users, frameworks, provide their own cache implementations.
+ * Since Jackson 2.12, there has been pluggable {@link LookupCache} interface which
+ * allows users, frameworks, provide their own cache implementations.
*/
public class LRUMap
implements LookupCache, // since 2.12
java.io.Serializable
{
- private static final long serialVersionUID = 1L;
+ private static final long serialVersionUID = 2L;
- protected final transient int _maxEntries;
+ protected final int _initialEntries;
+ protected final int _maxEntries;
+ protected final transient PrivateMaxEntriesMap _map;
- protected final transient ConcurrentHashMap _map;
-
public LRUMap(int initialEntries, int maxEntries)
{
- // We'll use concurrency level of 4, seems reasonable
- _map = new ConcurrentHashMap(initialEntries, 0.8f, 4);
+ _initialEntries = initialEntries;
_maxEntries = maxEntries;
+ // We'll use concurrency level of 4, seems reasonable
+ _map = new PrivateMaxEntriesMap.Builder()
+ .initialCapacity(initialEntries)
+ .maximumCapacity(maxEntries)
+ .concurrencyLevel(4)
+ .build();
}
@Override
public V put(K key, V value) {
- if (_map.size() >= _maxEntries) {
- // double-locking, yes, but safe here; trying to avoid "clear storms"
- synchronized (this) {
- if (_map.size() >= _maxEntries) {
- clear();
- }
- }
- }
return _map.put(key, value);
}
@@ -56,21 +47,12 @@ public V put(K key, V value) {
*/
@Override
public V putIfAbsent(K key, V value) {
- // not 100% optimal semantically, but better from correctness (never exceeds
- // defined maximum) and close enough all in all:
- if (_map.size() >= _maxEntries) {
- synchronized (this) {
- if (_map.size() >= _maxEntries) {
- clear();
- }
- }
- }
return _map.putIfAbsent(key, value);
}
// NOTE: key is of type Object only to retain binary backwards-compatibility
@Override
- public V get(Object key) { return _map.get(key); }
+ public V get(Object key) { return _map.get(key); }
@Override
public void clear() { _map.clear(); }
@@ -84,23 +66,7 @@ public V putIfAbsent(K key, V value) {
/**********************************************************
*/
- /**
- * Ugly hack, to work through the requirement that _value is indeed final,
- * and that JDK serialization won't call ctor(s) if Serializable is implemented.
- *
- * @since 2.1
- */
- protected transient int _jdkSerializeMaxEntries;
-
- private void readObject(ObjectInputStream in) throws IOException {
- _jdkSerializeMaxEntries = in.readInt();
- }
-
- private void writeObject(ObjectOutputStream out) throws IOException {
- out.writeInt(_jdkSerializeMaxEntries);
- }
-
protected Object readResolve() {
- return new LRUMap(_jdkSerializeMaxEntries, _jdkSerializeMaxEntries);
+ return new LRUMap(_initialEntries, _maxEntries);
}
}
diff --git a/src/main/java/com/fasterxml/jackson/databind/util/internal/EvictionListener.java b/src/main/java/com/fasterxml/jackson/databind/util/internal/EvictionListener.java
new file mode 100644
index 0000000000..644f22dc74
--- /dev/null
+++ b/src/main/java/com/fasterxml/jackson/databind/util/internal/EvictionListener.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2010 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.fasterxml.jackson.databind.util.internal;
+
+/**
+ * A listener registered for notification when an entry is evicted. An instance
+ * may be called concurrently by multiple threads to process entries. An
+ * implementation should avoid performing blocking calls or synchronizing on
+ * shared resources.
+ *
+ * The listener is invoked by {@link PrivateMaxEntriesMap} 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 java.util.concurrent.ExecutorService}.
+ *
+ * @author ben.manes@gmail.com (Ben Manes)
+ * @see
+ * http://code.google.com/p/concurrentlinkedhashmap/
+ */
+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);
+}
diff --git a/src/main/java/com/fasterxml/jackson/databind/util/internal/LinkedDeque.java b/src/main/java/com/fasterxml/jackson/databind/util/internal/LinkedDeque.java
new file mode 100644
index 0000000000..0ffbd90887
--- /dev/null
+++ b/src/main/java/com/fasterxml/jackson/databind/util/internal/LinkedDeque.java
@@ -0,0 +1,460 @@
+/*
+ * Copyright 2011 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.fasterxml.jackson.databind.util.internal;
+
+import java.util.AbstractCollection;
+import java.util.Collection;
+import java.util.Deque;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+/**
+ * Linked list implementation of the {@link 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.
+ *
+ * @author ben.manes@gmail.com (Ben Manes)
+ * @param the type of elements held in this collection
+ * @see
+ * http://code.google.com/p/concurrentlinkedhashmap/
+ */
+final class LinkedDeque> extends AbstractCollection implements Deque {
+
+ // 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;
+
+ /**
+ * 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 l = last;
+ last = e;
+
+ if (l == null) {
+ first = e;
+ } else {
+ l.setNext(e);
+ e.setPrevious(l);
+ }
+ }
+
+ /** 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();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Beware that, unlike in most collections, this method is NOT a
+ * constant-time operation.
+ */
+ @Override
+ public int size() {
+ int size = 0;
+ for (E e = first; e != null; e = e.getNext()) {
+ 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;
+ }
+
+ @Override
+ public boolean contains(Object o) {
+ return (o instanceof Linked>) && contains((Linked>) o);
+ }
+
+ // A fast-path containment check
+ boolean contains(Linked> e) {
+ 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);
+ }
+ }
+
+ @Override
+ public E peek() {
+ return peekFirst();
+ }
+
+ @Override
+ public E peekFirst() {
+ return first;
+ }
+
+ @Override
+ public E peekLast() {
+ return last;
+ }
+
+ @Override
+ public E getFirst() {
+ checkNotEmpty();
+ return peekFirst();
+ }
+
+ @Override
+ public E getLast() {
+ checkNotEmpty();
+ return peekLast();
+ }
+
+ @Override
+ public E element() {
+ return getFirst();
+ }
+
+ @Override
+ public boolean offer(E e) {
+ return offerLast(e);
+ }
+
+ @Override
+ public boolean offerFirst(E e) {
+ if (contains(e)) {
+ return false;
+ }
+ linkFirst(e);
+ return true;
+ }
+
+ @Override
+ public boolean offerLast(E e) {
+ if (contains(e)) {
+ return false;
+ }
+ linkLast(e);
+ return true;
+ }
+
+ @Override
+ public boolean add(E e) {
+ return offerLast(e);
+ }
+
+
+ @Override
+ public void addFirst(E e) {
+ if (!offerFirst(e)) {
+ throw new IllegalArgumentException();
+ }
+ }
+
+ @Override
+ public void addLast(E e) {
+ if (!offerLast(e)) {
+ throw new IllegalArgumentException();
+ }
+ }
+
+ @Override
+ public E poll() {
+ return pollFirst();
+ }
+
+ @Override
+ public E pollFirst() {
+ return isEmpty() ? null : unlinkFirst();
+ }
+
+ @Override
+ public E pollLast() {
+ return isEmpty() ? null : unlinkLast();
+ }
+
+ @Override
+ public E remove() {
+ return removeFirst();
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public boolean remove(Object o) {
+ return (o instanceof Linked>) && remove((E) o);
+ }
+
+ // A fast-path removal
+ boolean remove(E e) {
+ if (contains(e)) {
+ unlink(e);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public E removeFirst() {
+ checkNotEmpty();
+ return pollFirst();
+ }
+
+ @Override
+ public boolean removeFirstOccurrence(Object o) {
+ return remove(o);
+ }
+
+ @Override
+ public E removeLast() {
+ checkNotEmpty();
+ return pollLast();
+ }
+
+ @Override
+ 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;
+ }
+
+ @Override
+ public void push(E e) {
+ addFirst(e);
+ }
+
+ @Override
+ public E pop() {
+ return removeFirst();
+ }
+
+ @Override
+ public Iterator iterator() {
+ return new AbstractLinkedIterator(first) {
+ @Override E computeNext() {
+ return cursor.getNext();
+ }
+ };
+ }
+
+ @Override
+ 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();
+ }
+}
+
+/**
+ * An element that is linked on the {@link Deque}.
+ */
+interface Linked> {
+
+ /**
+ * Retrieves the previous element or null if either the element is
+ * unlinked or the first element on the deque.
+ */
+ T getPrevious();
+
+ /** Sets the previous element or null if there is no link. */
+ void setPrevious(T prev);
+
+ /**
+ * Retrieves the next element or null if either the element is
+ * unlinked or the last element on the deque.
+ */
+ T getNext();
+
+ /** Sets the next element or null if there is no link. */
+ void setNext(T next);
+}
diff --git a/src/main/java/com/fasterxml/jackson/databind/util/internal/PrivateMaxEntriesMap.java b/src/main/java/com/fasterxml/jackson/databind/util/internal/PrivateMaxEntriesMap.java
new file mode 100644
index 0000000000..9058171dce
--- /dev/null
+++ b/src/main/java/com/fasterxml/jackson/databind/util/internal/PrivateMaxEntriesMap.java
@@ -0,0 +1,1523 @@
+/*
+ * Copyright 2010 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.fasterxml.jackson.databind.util.internal;
+
+import static com.fasterxml.jackson.databind.util.internal.PrivateMaxEntriesMap.DrainStatus.IDLE;
+import static com.fasterxml.jackson.databind.util.internal.PrivateMaxEntriesMap.DrainStatus.PROCESSING;
+import static com.fasterxml.jackson.databind.util.internal.PrivateMaxEntriesMap.DrainStatus.REQUIRED;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.unmodifiableMap;
+import static java.util.Collections.unmodifiableSet;
+
+import java.io.InvalidObjectException;
+import java.io.ObjectInputStream;
+import java.io.Serializable;
+import java.util.AbstractCollection;
+import java.util.AbstractMap;
+import java.util.AbstractQueue;
+import java.util.AbstractSet;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Queue;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * 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 entry size exceeds
+ * its maximum capacity threshold.
+ *
+ * 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 java.util.concurrent.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 java.util.Hashtable} but unlike {@link HashMap}, this class
+ * does not allow null to be used as a key or value. Unlike
+ * {@link java.util.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.
+ *
+ * @author ben.manes@gmail.com (Ben Manes)
+ * @param the type of keys maintained by this map
+ * @param the type of mapped values
+ * @see
+ * http://code.google.com/p/concurrentlinkedhashmap/
+ */
+//@ThreadSafe
+public final class PrivateMaxEntriesMap 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 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 int ceilingNextPowerOfTwo(int x) {
+ // From Hacker's Delight, Chapter 3, Harry S. Warren Jr.
+ return 1 << (Integer.SIZE - Integer.numberOfLeadingZeros(x - 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;
+
+ // These fields provide support for notifying a listener.
+ final Queue> pendingNotifications;
+ final EvictionListener listener;
+
+ transient Set keySet;
+ transient Collection values;
+ transient Set> entrySet;
+
+ /**
+ * Creates an instance based on the builder's configuration.
+ */
+ @SuppressWarnings({"unchecked", "cast"})
+ private PrivateMaxEntriesMap(Builder builder) {
+ // The data store and its maximum capacity
+ concurrencyLevel = builder.concurrencyLevel;
+ capacity = new AtomicLong(Math.min(builder.capacity, MAXIMUM_CAPACITY));
+ data = new ConcurrentHashMap>(builder.initialCapacity, 0.75f, concurrencyLevel);
+
+ // The eviction support
+ evictionLock = new ReentrantLock();
+ weightedSize = new AtomicLong();
+ evictionDeque = new LinkedDeque>();
+ writeBuffer = new ConcurrentLinkedQueue();
+ drainStatus = new AtomicReference(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>();
+ }
+
+ /** 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();
+ }
+ }
+
+ /** Ensures that the state expression is true. */
+ static void checkState(boolean expression) {
+ if (!expression) {
+ throw new IllegalStateException();
+ }
+ }
+
+ /* ---------------- Eviction Support -------------- */
+
+ /**
+ * Retrieves the maximum capacity of the map.
+ *
+ * @return the maximum capacity
+ */
+ public long capacity() {
+ return capacity.get();
+ }
+
+ /**
+ * Sets the maximum capacity of the map and eagerly evicts entries
+ * until it shrinks to the appropriate size.
+ *
+ * @param capacity the maximum 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();
+ }
+
+ /** 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;
+ }
+
+ /**
+ * 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();
+ }
+ }
+
+ /**
+ * 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(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(PROCESSING);
+ drainBuffers();
+ } finally {
+ drainStatus.compareAndSet(PROCESSING, 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());
+ }
+ }
+
+ /** 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);
+ }
+ }
+
+ /** Updates the weighted size and evicts an entry on overflow. */
+ final class UpdateTask implements Runnable {
+ final int weightDifference;
+ final Node node;
+
+ 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();
+ }
+ }
+
+ /* ---------------- Concurrent Map Support -------------- */
+
+ @Override
+ public boolean isEmpty() {
+ return data.isEmpty();
+ }
+
+ @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 = 1;
+ 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 = 1;
+ 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 = 1;
+ 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 = Math.min(limit, (int) weightedSize());
+ 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 = Math.min(limit, (int) weightedSize());
+ 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();
+ }
+ }
+
+ /** The draining status of the buffers. */
+ 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 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;
+ }
+ }
+
+ /** An adapter to safely externalize the keys. */
+ final class KeySet extends AbstractSet {
+ final PrivateMaxEntriesMap map = PrivateMaxEntriesMap.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);
+ PrivateMaxEntriesMap.this.remove(current);
+ current = null;
+ }
+ }
+
+ /** An adapter to safely externalize the values. */
+ final class Values extends AbstractCollection {
+
+ @Override
+ public int size() {
+ return PrivateMaxEntriesMap.this.size();
+ }
+
+ @Override
+ public void clear() {
+ PrivateMaxEntriesMap.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);
+ PrivateMaxEntriesMap.this.remove(current.key);
+ current = null;
+ }
+ }
+
+ /** An adapter to safely externalize the entries. */
+ final class EntrySet extends AbstractSet> {
+ final PrivateMaxEntriesMap map = PrivateMaxEntriesMap.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) {
+ throw new UnsupportedOperationException("ConcurrentLinkedHashMap does not allow add to be called on entrySet()");
+ }
+
+ @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);
+ PrivateMaxEntriesMap.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);
+ }
+ }
+
+ /** 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 emptyList().iterator(); }
+ }
+
+ /** A listener that ignores all notifications. */
+ enum DiscardingListener implements EvictionListener {
+ INSTANCE;
+
+ @Override public void onEviction(Object key, Object value) {}
+ }
+
+ /* ---------------- Serialization Support -------------- */
+
+ static final long serialVersionUID = 1;
+
+ Object writeReplace() {
+ return new SerializationProxy(this);
+ }
+
+ private void readObject(ObjectInputStream stream) throws InvalidObjectException {
+ throw new InvalidObjectException("Proxy required");
+ }
+
+ /**
+ * 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 {
+ final EvictionListener listener;
+ final int concurrencyLevel;
+ final Map data;
+ final long capacity;
+
+ SerializationProxy(PrivateMaxEntriesMap map) {
+ concurrencyLevel = map.concurrencyLevel;
+ data = new HashMap(map);
+ capacity = map.capacity.get();
+ listener = map.listener;
+ }
+
+ Object readResolve() {
+ PrivateMaxEntriesMap map = new Builder()
+ .maximumCapacity(capacity)
+ .build();
+ map.putAll(data);
+ return map;
+ }
+
+ static final long serialVersionUID = 1;
+ }
+
+ /* ---------------- Builder -------------- */
+
+ /**
+ * A builder that creates {@link PrivateMaxEntriesMap} 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>()
+ * .maximumCapacity(5000)
+ * .build();
+ * }
+ */
+ public static final class Builder {
+ static final int DEFAULT_CONCURRENCY_LEVEL = 16;
+ static final int DEFAULT_INITIAL_CAPACITY = 16;
+
+ EvictionListener listener;
+
+ int concurrencyLevel;
+ int initialCapacity;
+ long capacity;
+
+ @SuppressWarnings("unchecked")
+ public Builder() {
+ capacity = -1;
+ initialCapacity = DEFAULT_INITIAL_CAPACITY;
+ concurrencyLevel = DEFAULT_CONCURRENCY_LEVEL;
+ listener = (EvictionListener) DiscardingListener.INSTANCE;
+ }
+
+ /**
+ * 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.
+ * @throws IllegalArgumentException if the initialCapacity is negative
+ */
+ public Builder initialCapacity(int initialCapacity) {
+ checkArgument(initialCapacity >= 0);
+ this.initialCapacity = initialCapacity;
+ return this;
+ }
+
+ /**
+ * Specifies the maximum capacity to coerce the map to and may
+ * exceed it temporarily.
+ *
+ * @param capacity the threshold to bound the map by
+ * @throws IllegalArgumentException if the maximumCapacity is negative
+ */
+ public Builder maximumCapacity(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
+ * @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
+ * @throws NullPointerException if the listener is null
+ */
+ public Builder listener(EvictionListener listener) {
+ checkNotNull(listener);
+ this.listener = listener;
+ return this;
+ }
+
+ /**
+ * Creates a new {@link PrivateMaxEntriesMap} instance.
+ *
+ * @throws IllegalStateException if the maximum capacity was
+ * not set
+ */
+ public PrivateMaxEntriesMap build() {
+ checkState(capacity >= 0);
+ return new PrivateMaxEntriesMap(this);
+ }
+ }
+}
diff --git a/src/main/java/com/fasterxml/jackson/databind/util/internal/package-info.java b/src/main/java/com/fasterxml/jackson/databind/util/internal/package-info.java
new file mode 100644
index 0000000000..f7de34a700
--- /dev/null
+++ b/src/main/java/com/fasterxml/jackson/databind/util/internal/package-info.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2011 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * This package contains an implementation of a bounded
+ * {@link java.util.concurrent.ConcurrentMap} data structure.
+ *
+ * This package is intended only for use internally by Jackson libraries and has
+ * missing features compared to the full
+ * http://code.google.com/p/concurrentlinkedhashmap/ implementation.
+ *
+ * {@link com.fasterxml.jackson.databind.util.internal.Weigher} is a simple interface
+ * for determining how many units of capacity an entry consumes. Depending on
+ * which concrete Weigher class is used, an entry may consume a different amount
+ * of space within the cache. The
+ * {@link com.fasterxml.jackson.databind.util.internal.Weighers} class provides
+ * utility methods for obtaining the most common kinds of implementations.
+ *
+ * {@link com.fasterxml.jackson.databind.util.internal.EvictionListener} provides the
+ * ability to be notified when an entry is evicted from the map. An eviction
+ * occurs when the entry was automatically removed due to the map exceeding a
+ * capacity threshold. It is not called when an entry was explicitly removed.
+ *
+ * The {@link com.fasterxml.jackson.databind.util.internal.PrivateMaxEntriesMap}
+ * class supplies an efficient, scalable, thread-safe, bounded map. As with the
+ * Java Collections Framework the "Concurrent" prefix is used to
+ * indicate that the map is not governed by a single exclusion lock.
+ *
+ * @see
+ * http://code.google.com/p/concurrentlinkedhashmap/
+ */
+package com.fasterxml.jackson.databind.util.internal;
diff --git a/src/test/java/com/fasterxml/jackson/databind/TestJDKSerialization.java b/src/test/java/com/fasterxml/jackson/databind/TestJDKSerialization.java
index 01e7072b4d..bd3e81420e 100644
--- a/src/test/java/com/fasterxml/jackson/databind/TestJDKSerialization.java
+++ b/src/test/java/com/fasterxml/jackson/databind/TestJDKSerialization.java
@@ -8,7 +8,6 @@
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.databind.type.TypeFactory;
-import com.fasterxml.jackson.databind.util.LRUMap;
/**
* Tests to verify that most core Jackson components can be serialized
@@ -193,28 +192,13 @@ public void testTypeFactory() throws Exception
assertEquals(JavaType.class, t.getRawClass());
}
- public void testLRUMap() throws Exception
- {
- LRUMap map = new LRUMap(32, 32);
- map.put("a", 1);
-
- byte[] bytes = jdkSerialize(map);
- LRUMap result = jdkDeserialize(bytes);
- // transient implementation, will be read as empty
- assertEquals(0, result.size());
-
- // but should be possible to re-populate
- result.put("a", 2);
- assertEquals(1, result.size());
- }
-
/*
/**********************************************************
/* Helper methods
/**********************************************************
*/
- protected byte[] jdkSerialize(Object o) throws IOException
+ public static byte[] jdkSerialize(Object o) throws IOException
{
ByteArrayOutputStream bytes = new ByteArrayOutputStream(2000);
ObjectOutputStream obOut = new ObjectOutputStream(bytes);
@@ -224,7 +208,7 @@ protected byte[] jdkSerialize(Object o) throws IOException
}
@SuppressWarnings("unchecked")
- protected T jdkDeserialize(byte[] raw) throws IOException
+ public static T jdkDeserialize(byte[] raw) throws IOException
{
ObjectInputStream objIn = new ObjectInputStream(new ByteArrayInputStream(raw));
try {
diff --git a/src/test/java/com/fasterxml/jackson/databind/util/LRUMapTest.java b/src/test/java/com/fasterxml/jackson/databind/util/LRUMapTest.java
new file mode 100644
index 0000000000..c2261a13c0
--- /dev/null
+++ b/src/test/java/com/fasterxml/jackson/databind/util/LRUMapTest.java
@@ -0,0 +1,68 @@
+package com.fasterxml.jackson.databind.util;
+
+import com.fasterxml.jackson.databind.BaseTest;
+
+import static com.fasterxml.jackson.databind.TestJDKSerialization.jdkDeserialize;
+import static com.fasterxml.jackson.databind.TestJDKSerialization.jdkSerialize;
+
+public class LRUMapTest extends BaseTest {
+
+ public void testPutGet() {
+ LRUMap m = new LRUMap<>(5, 5);
+
+ assertEquals(0, m.size());
+ m.put("k1", 100);
+ assertEquals(1, m.size());
+ assertNull(m.get("nosuchkey"));
+ assertEquals(Integer.valueOf(100), m.get("k1"));
+
+ m.put("k2", 200);
+ assertEquals(2, m.size());
+ assertEquals(Integer.valueOf(200), m.get("k2"));
+ }
+
+ public void testEviction() {
+ LRUMap m = new LRUMap<>(5, 5);
+
+ assertEquals(0, m.size());
+ m.put("k1", 100);
+ assertEquals(1, m.size());
+ m.put("k2", 101);
+ assertEquals(2, m.size());
+ m.put("k3", 102);
+ assertEquals(3, m.size());
+ m.put("k4", 103);
+ assertEquals(4, m.size());
+ m.put("k5", 104);
+ assertEquals(5, m.size());
+ m.put("k6", 105);
+ assertEquals(5, m.size());
+ m.put("k7", 106);
+ assertEquals(5, m.size());
+ m.put("k8", 107);
+ assertEquals(5, m.size());
+
+ assertNull(m.get("k3"));
+ assertEquals(Integer.valueOf(105), m.get("k6"));
+ }
+
+ public void testJDKSerialization() throws Exception
+ {
+ final int maxEntries = 32;
+ LRUMap map = new LRUMap(16, maxEntries);
+ map.put("a", 1);
+ assertEquals(1, map.size());
+
+ byte[] bytes = jdkSerialize(map);
+ LRUMap result = jdkDeserialize(bytes);
+ // transient implementation, will be read as empty
+ assertNull(result.get("a"));
+ assertEquals(0, result.size());
+ assertEquals(maxEntries, result._map.capacity());
+
+ // but should be possible to re-populate
+ assertNull(result.put("a", 2));
+ assertEquals(Integer.valueOf(2), result.get("a"));
+ assertEquals(1, result.size());
+ }
+}
diff --git a/src/test/java/com/fasterxml/jackson/databind/util/internal/CLHMTestlibTests.java b/src/test/java/com/fasterxml/jackson/databind/util/internal/CLHMTestlibTests.java
new file mode 100644
index 0000000000..6349aa3186
--- /dev/null
+++ b/src/test/java/com/fasterxml/jackson/databind/util/internal/CLHMTestlibTests.java
@@ -0,0 +1,21 @@
+package com.fasterxml.jackson.databind.util.internal;
+
+import junit.framework.Test;
+import junit.framework.TestCase;
+import junit.framework.TestSuite;
+
+public final class CLHMTestlibTests extends TestCase {
+
+ public static Test suite() {
+ TestSuite suite = new TestSuite();
+ addCLHMViewTests(suite);
+ return suite;
+ }
+
+ private static void addCLHMViewTests(TestSuite suite) {
+ suite.addTest(MapTestFactory.suite("CLHMView", MapTestFactory.synchronousGenerator(
+ () -> new PrivateMaxEntriesMap.Builder()
+ .maximumCapacity(10).build()))
+ );
+ }
+}
diff --git a/src/test/java/com/fasterxml/jackson/databind/util/internal/MapTestFactory.java b/src/test/java/com/fasterxml/jackson/databind/util/internal/MapTestFactory.java
new file mode 100644
index 0000000000..d0b23f8bcf
--- /dev/null
+++ b/src/test/java/com/fasterxml/jackson/databind/util/internal/MapTestFactory.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2015 Ben Manes. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.fasterxml.jackson.databind.util.internal;
+
+import java.util.Map;
+import java.util.function.Supplier;
+
+import com.google.common.collect.testing.ConcurrentMapTestSuiteBuilder;
+import com.google.common.collect.testing.TestMapGenerator;
+import com.google.common.collect.testing.TestStringMapGenerator;
+import com.google.common.collect.testing.features.CollectionFeature;
+import com.google.common.collect.testing.features.CollectionSize;
+import com.google.common.collect.testing.features.MapFeature;
+
+import junit.framework.Test;
+
+/**
+ * A JUnit test suite factory for the map tests from Guava's testlib.
+ *
+ * @author ben.manes@gmail.com (Ben Manes)
+ */
+final class MapTestFactory {
+
+ private MapTestFactory() {}
+
+ /**
+ * Returns a test suite.
+ *
+ * @param name the name of the cache type under test
+ * @param generator the map generator
+ * @return a suite of tests
+ */
+ public static Test suite(String name, TestMapGenerator, ?> generator) {
+ return ConcurrentMapTestSuiteBuilder
+ .using(generator)
+ .named(name)
+ .withFeatures(
+ CollectionSize.ANY,
+ MapFeature.GENERAL_PURPOSE,
+ MapFeature.ALLOWS_NULL_ENTRY_QUERIES,
+ CollectionFeature.SUPPORTS_ITERATOR_REMOVE)
+ .createTestSuite();
+ }
+
+ /** Returns a map generator for synchronous values. */
+ public static TestStringMapGenerator synchronousGenerator(
+ Supplier> supplier) {
+ return new TestStringMapGenerator() {
+ @Override protected Map create(Map.Entry[] entries) {
+ Map map = supplier.get();
+ for (Map.Entry entry : entries) {
+ map.put(entry.getKey(), entry.getValue());
+ }
+ return map;
+ }
+ };
+ }
+}
diff --git a/src/test/java/com/fasterxml/jackson/databind/util/internal/PrivateMaxEntriesMapStressTest.java b/src/test/java/com/fasterxml/jackson/databind/util/internal/PrivateMaxEntriesMapStressTest.java
new file mode 100644
index 0000000000..1d0d0cbcde
--- /dev/null
+++ b/src/test/java/com/fasterxml/jackson/databind/util/internal/PrivateMaxEntriesMapStressTest.java
@@ -0,0 +1,80 @@
+package com.fasterxml.jackson.databind.util.internal;
+
+import org.junit.Test;
+
+import java.time.Duration;
+import java.time.temporal.ChronoUnit;
+import java.util.Map;
+import java.util.Random;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertEquals;
+
+public class PrivateMaxEntriesMapStressTest {
+
+ //increase these to increase the stress
+ private static int iterations = 100000;
+ private static int threads = 20;
+ private static int waitSeconds = 60;
+
+ @Test
+ public void testManyEntries() throws Exception {
+ final int maxEntries = 30;
+ final int maxKey = 100;
+ final Random rnd = new Random();
+ final PrivateMaxEntriesMap clhm =
+ new PrivateMaxEntriesMap.Builder().maximumCapacity(maxEntries).build();
+ final Map map = new ConcurrentHashMap<>();
+ final ExecutorService executor = Executors.newFixedThreadPool(threads);
+ try {
+ for (int i = 0; i < maxKey; i++) {
+ final Integer key = Integer.valueOf(i);
+ executor.submit(() -> {
+ UUID uuid = UUID.randomUUID();
+ synchronized (key) {
+ clhm.put(key, uuid);
+ map.put(key, uuid);
+ }
+ });
+ }
+ for (int i = 0; i < iterations; i++) {
+ executor.submit(() -> {
+ Integer key = Integer.valueOf(rnd.nextInt(maxKey));
+ UUID uuid = UUID.randomUUID();
+ synchronized (key) {
+ clhm.put(key, uuid);
+ map.put(key, uuid);
+ }
+ });
+ }
+ } finally {
+ executor.shutdown();
+ }
+ executor.awaitTermination(waitSeconds, TimeUnit.SECONDS);
+
+ final long endTime = System.nanoTime() + Duration.of(waitSeconds, ChronoUnit.SECONDS).toNanos();
+ boolean assertsFailing = true;
+ while(assertsFailing) {
+ clhm.drainBuffers();
+ try {
+ assertEquals(clhm.size(), maxEntries);
+ for (int i = 0; i < maxKey; i++) {
+ UUID uuid = clhm.get(i);
+ if (uuid != null) {
+ assertEquals(map.get(i), clhm.get(i));
+ }
+ }
+ assertsFailing = false;
+ } catch (Throwable t) {
+ if (System.nanoTime() > endTime) {
+ throw t;
+ }
+ Thread.sleep(100);
+ }
+ }
+ }
+}
diff --git a/src/test/java/com/fasterxml/jackson/databind/util/internal/PrivateMaxEntriesMapTest.java b/src/test/java/com/fasterxml/jackson/databind/util/internal/PrivateMaxEntriesMapTest.java
new file mode 100644
index 0000000000..eccb9e86d3
--- /dev/null
+++ b/src/test/java/com/fasterxml/jackson/databind/util/internal/PrivateMaxEntriesMapTest.java
@@ -0,0 +1,72 @@
+/*****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ ****************************************************************/
+package com.fasterxml.jackson.databind.util.internal;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+//copied from https://github.com/apache/cayenne/blob/b156addac1c8e4079fa88e977fee609210c5da69/cayenne-server/src/test/java/org/apache/cayenne/util/concurrentlinkedhashmap/ConcurrentLinkedHashMapTest.java
+public class PrivateMaxEntriesMapTest {
+
+ @Test
+ public void testPutGet() {
+ PrivateMaxEntriesMap m = new PrivateMaxEntriesMap.Builder()
+ .maximumCapacity(10).build();
+
+ assertEquals(0, m.size());
+ m.put("k1", 100);
+ assertEquals(1, m.size());
+ assertNull(m.get("nosuchkey"));
+ assertEquals(100, m.get("k1"));
+
+ m.put("k2", 200);
+ assertEquals(2, m.size());
+ assertEquals(200, m.get("k2"));
+ }
+
+ @Test
+ public void testLRU() {
+ PrivateMaxEntriesMap m = new PrivateMaxEntriesMap.Builder()
+ .maximumCapacity(5).build();
+
+ assertEquals(0, m.size());
+ m.put("k1", 100);
+ assertEquals(1, m.size());
+ m.put("k2", 101);
+ assertEquals(2, m.size());
+ m.put("k3", 102);
+ assertEquals(3, m.size());
+ m.put("k4", 103);
+ assertEquals(4, m.size());
+ m.put("k5", 104);
+ assertEquals(5, m.size());
+ m.put("k6", 105);
+ assertEquals(5, m.size());
+ m.put("k7", 106);
+ assertEquals(5, m.size());
+ m.put("k8", 107);
+ assertEquals(5, m.size());
+
+ m.remove("k6");
+ assertEquals(4, m.size());
+
+ }
+}