From c93c6a2b77166f4fb5394a2e9dc7a6249c60acba Mon Sep 17 00:00:00 2001 From: Looly Date: Mon, 3 Apr 2023 13:17:55 +0800 Subject: [PATCH] fix code --- .../hutool/core/collection/CollUtil.java | 1 + .../MemorySafeLinkedBlockingQueue.java | 1 + .../{ => queue}/BoundedPriorityQueue.java | 2 +- .../CheckedLinkedBlockingQueue.java | 2 +- .../hutool/core/collection/queue/Linked.java | 53 + .../core/collection/queue/LinkedDeque.java | 440 +++++ .../core/collection/queue/package-info.java | 16 + .../concurrent/ConcurrentLinkedHashMap.java | 1693 +++++++++++++++++ .../core/map/concurrent/EntryWeigher.java | 39 + .../hutool/core/map/concurrent/Weigher.java | 37 + .../hutool/core/map/concurrent/Weighers.java | 288 +++ .../core/map/concurrent/package-info.java | 41 + .../core/classloader/ClassLoaderUtilTest.java | 2 +- .../hutool/core/convert/ConvertTest.java | 2 +- .../core/lang/caller/CallerUtilTest.java | 2 +- .../hutool/core/reflect/MethodUtilTest.java | 6 +- .../hutool/core/util/ClassUtilTest.java | 6 +- .../engine/jetbrick/JetbrickEngine.java | 2 +- 18 files changed, 2621 insertions(+), 12 deletions(-) rename hutool-core/src/main/java/org/dromara/hutool/core/collection/{ => queue}/BoundedPriorityQueue.java (98%) rename hutool-core/src/main/java/org/dromara/hutool/core/collection/{ => queue}/CheckedLinkedBlockingQueue.java (97%) create mode 100755 hutool-core/src/main/java/org/dromara/hutool/core/collection/queue/Linked.java create mode 100755 hutool-core/src/main/java/org/dromara/hutool/core/collection/queue/LinkedDeque.java create mode 100755 hutool-core/src/main/java/org/dromara/hutool/core/collection/queue/package-info.java create mode 100755 hutool-core/src/main/java/org/dromara/hutool/core/map/concurrent/ConcurrentLinkedHashMap.java create mode 100755 hutool-core/src/main/java/org/dromara/hutool/core/map/concurrent/EntryWeigher.java create mode 100755 hutool-core/src/main/java/org/dromara/hutool/core/map/concurrent/Weigher.java create mode 100755 hutool-core/src/main/java/org/dromara/hutool/core/map/concurrent/Weighers.java create mode 100755 hutool-core/src/main/java/org/dromara/hutool/core/map/concurrent/package-info.java diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/collection/CollUtil.java b/hutool-core/src/main/java/org/dromara/hutool/core/collection/CollUtil.java index c90645f65..6351d9f7b 100644 --- a/hutool-core/src/main/java/org/dromara/hutool/core/collection/CollUtil.java +++ b/hutool-core/src/main/java/org/dromara/hutool/core/collection/CollUtil.java @@ -16,6 +16,7 @@ import org.dromara.hutool.core.bean.BeanUtil; import org.dromara.hutool.core.codec.hash.Hash32; import org.dromara.hutool.core.collection.iter.IterUtil; import org.dromara.hutool.core.collection.iter.IteratorEnumeration; +import org.dromara.hutool.core.collection.queue.BoundedPriorityQueue; import org.dromara.hutool.core.comparator.CompareUtil; import org.dromara.hutool.core.comparator.PinyinComparator; import org.dromara.hutool.core.comparator.PropertyComparator; diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/collection/MemorySafeLinkedBlockingQueue.java b/hutool-core/src/main/java/org/dromara/hutool/core/collection/MemorySafeLinkedBlockingQueue.java index b13437f27..9386ca924 100644 --- a/hutool-core/src/main/java/org/dromara/hutool/core/collection/MemorySafeLinkedBlockingQueue.java +++ b/hutool-core/src/main/java/org/dromara/hutool/core/collection/MemorySafeLinkedBlockingQueue.java @@ -11,6 +11,7 @@ */ package org.dromara.hutool.core.collection; +import org.dromara.hutool.core.collection.queue.CheckedLinkedBlockingQueue; import org.dromara.hutool.core.thread.SimpleScheduler; import org.dromara.hutool.core.util.RuntimeUtil; diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/collection/BoundedPriorityQueue.java b/hutool-core/src/main/java/org/dromara/hutool/core/collection/queue/BoundedPriorityQueue.java similarity index 98% rename from hutool-core/src/main/java/org/dromara/hutool/core/collection/BoundedPriorityQueue.java rename to hutool-core/src/main/java/org/dromara/hutool/core/collection/queue/BoundedPriorityQueue.java index f3b127571..841a101b5 100644 --- a/hutool-core/src/main/java/org/dromara/hutool/core/collection/BoundedPriorityQueue.java +++ b/hutool-core/src/main/java/org/dromara/hutool/core/collection/queue/BoundedPriorityQueue.java @@ -10,7 +10,7 @@ * See the Mulan PSL v2 for more details. */ -package org.dromara.hutool.core.collection; +package org.dromara.hutool.core.collection.queue; import java.util.ArrayList; import java.util.Arrays; diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/collection/CheckedLinkedBlockingQueue.java b/hutool-core/src/main/java/org/dromara/hutool/core/collection/queue/CheckedLinkedBlockingQueue.java similarity index 97% rename from hutool-core/src/main/java/org/dromara/hutool/core/collection/CheckedLinkedBlockingQueue.java rename to hutool-core/src/main/java/org/dromara/hutool/core/collection/queue/CheckedLinkedBlockingQueue.java index 6e8623244..50fd4bf49 100644 --- a/hutool-core/src/main/java/org/dromara/hutool/core/collection/CheckedLinkedBlockingQueue.java +++ b/hutool-core/src/main/java/org/dromara/hutool/core/collection/queue/CheckedLinkedBlockingQueue.java @@ -10,7 +10,7 @@ * See the Mulan PSL v2 for more details. */ -package org.dromara.hutool.core.collection; +package org.dromara.hutool.core.collection.queue; import java.util.Collection; import java.util.concurrent.LinkedBlockingQueue; diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/collection/queue/Linked.java b/hutool-core/src/main/java/org/dromara/hutool/core/collection/queue/Linked.java new file mode 100755 index 000000000..e21ab7e07 --- /dev/null +++ b/hutool-core/src/main/java/org/dromara/hutool/core/collection/queue/Linked.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2023 looly(loolly@aliyun.com) + * Hutool is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +package org.dromara.hutool.core.collection.queue; + +import java.util.Deque; + +/** + * An element that is linked on the {@link Deque}. + * + * @param 值类型 + */ +public interface Linked> { + + /** + * Retrieves the previous element or null if either the element is + * unlinked or the first element on the deque. + * + * @return 前一个值 + */ + T getPrevious(); + + /** + * Sets the previous element or null if there is no link. + * + * @param 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 下一个值 + */ + T getNext(); + + /** + * Sets the next element or null if there is no link. + * + * @param next 下一个值 + */ + void setNext(T next); +} diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/collection/queue/LinkedDeque.java b/hutool-core/src/main/java/org/dromara/hutool/core/collection/queue/LinkedDeque.java new file mode 100755 index 000000000..0501830c7 --- /dev/null +++ b/hutool-core/src/main/java/org/dromara/hutool/core/collection/queue/LinkedDeque.java @@ -0,0 +1,440 @@ +/* + * 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 org.dromara.hutool.core.collection.queue; + +import java.util.*; + +/** + * 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. + * + * @param the type of elements held in this collection + * @author ben.manes@gmail.com (Ben Manes) + * @see + * http://code.google.com/p/concurrentlinkedhashmap/ + */ +public 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(final 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; ) { + final E next = e.getNext(); + e.setPrevious(null); + e.setNext(null); + e = next; + } + first = last = null; + } + + @Override + public boolean contains(final Object o) { + return (o instanceof Linked) && contains((Linked) o); + } + + // A fast-path containment check + boolean contains(final 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(final 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(final 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(final E e) { + return offerLast(e); + } + + @Override + public boolean offerFirst(final E e) { + if (contains(e)) { + return false; + } + linkFirst(e); + return true; + } + + @Override + public boolean offerLast(final E e) { + if (contains(e)) { + return false; + } + linkLast(e); + return true; + } + + @Override + public boolean add(final E e) { + return offerLast(e); + } + + + @Override + public void addFirst(final E e) { + if (!offerFirst(e)) { + throw new IllegalArgumentException(); + } + } + + @Override + public void addLast(final 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(final Object o) { + return (o instanceof Linked) && remove((E) o); + } + + // A fast-path removal + boolean remove(final E e) { + if (contains(e)) { + unlink(e); + return true; + } + return false; + } + + @Override + public E removeFirst() { + checkNotEmpty(); + return pollFirst(); + } + + @Override + public boolean removeFirstOccurrence(final Object o) { + return remove(o); + } + + @Override + public E removeLast() { + checkNotEmpty(); + return pollLast(); + } + + @Override + public boolean removeLastOccurrence(final Object o) { + return remove(o); + } + + @Override + public boolean removeAll(final Collection c) { + boolean modified = false; + for (final Object o : c) { + modified |= remove(o); + } + return modified; + } + + @Override + public void push(final 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(final E start) { + cursor = start; + } + + @Override + public boolean hasNext() { + return (cursor != null); + } + + @Override + public E next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + final 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(); + } +} diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/collection/queue/package-info.java b/hutool-core/src/main/java/org/dromara/hutool/core/collection/queue/package-info.java new file mode 100755 index 000000000..edadc4f82 --- /dev/null +++ b/hutool-core/src/main/java/org/dromara/hutool/core/collection/queue/package-info.java @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2023 looly(loolly@aliyun.com) + * Hutool is licensed under Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +/** + * 队列{@link java.util.Queue}相关封装 + */ +package org.dromara.hutool.core.collection.queue; diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/map/concurrent/ConcurrentLinkedHashMap.java b/hutool-core/src/main/java/org/dromara/hutool/core/map/concurrent/ConcurrentLinkedHashMap.java new file mode 100755 index 000000000..f2fcd0ed1 --- /dev/null +++ b/hutool-core/src/main/java/org/dromara/hutool/core/map/concurrent/ConcurrentLinkedHashMap.java @@ -0,0 +1,1693 @@ +/* + * 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 org.dromara.hutool.core.map.concurrent; + +import org.dromara.hutool.core.collection.queue.Linked; +import org.dromara.hutool.core.collection.queue.LinkedDeque; +import org.dromara.hutool.core.map.SafeConcurrentHashMap; + +import java.io.InvalidObjectException; +import java.io.ObjectInputStream; +import java.io.Serializable; +import java.util.*; +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; +import java.util.function.BiConsumer; + +/** + * 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 EntryWeigher} + * determines how many units of capacity that an entry 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 BiConsumer} 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 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 ben.manes@gmail.com (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(); + + @SuppressWarnings("SameParameterValue") + static int ceilingNextPowerOfTwo(final 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 + final long[] readBufferReadCount; + final LinkedDeque> evictionDeque; + + final AtomicLong weightedSize; + 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 BiConsumer listener; + + transient Set keySet; + transient Collection values; + transient Set> entrySet; + + /** + * Creates an instance based on the builder's configuration. + */ + @SuppressWarnings({"unchecked", "cast"}) + private ConcurrentLinkedHashMap(final Builder builder) { + // The data store and its maximum capacity + concurrencyLevel = builder.concurrencyLevel; + capacity = new AtomicLong(Math.min(builder.capacity, MAXIMUM_CAPACITY)); + data = new SafeConcurrentHashMap<>(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<>(); + } + + /** + * Ensures that the object is not null. + */ + static void checkNotNull(final Object o) { + if (o == null) { + throw new NullPointerException(); + } + } + + /** + * Ensures that the argument expression is true. + */ + static void checkArgument(final boolean expression) { + if (!expression) { + throw new IllegalArgumentException(); + } + } + + /** + * Ensures that the state expression is true. + */ + static void checkState(final boolean expression) { + if (!expression) { + throw new IllegalStateException(); + } + } + + /* ---------------- Eviction Support -------------- */ + + /** + * 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(final 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. + */ + 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. + */ + 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(final 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(final int bufferIndex, final 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(final int bufferIndex, final 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(final 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. + */ + void drainBuffers() { + drainReadBuffers(); + drainWriteBuffer(); + } + + /** + * Drains the read buffers, each up to an amortized threshold. + */ + 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. + */ + void drainReadBuffer(final 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. + */ + + void applyRead(final 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. + */ + + 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(final Node node, final 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(final 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 + */ + + void makeDead(final Node node) { + for (; ; ) { + final WeightedValue current = node.get(); + final 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.accept(node.key, node.getValue()); + } + } + + /** + * Adds the node to the page replacement policy. + */ + final class AddTask implements Runnable { + final Node node; + final int weight; + + AddTask(final Node node, final int weight) { + this.weight = weight; + this.node = node; + } + + @Override + + 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(final Node node) { + this.node = node; + } + + @Override + + 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; + + public UpdateTask(final Node node, final int weightDifference) { + this.weightDifference = weightDifference; + this.node = node; + } + + @Override + + 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 (final AtomicReference>[] buffer : readBuffers) { + for (final 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(final Object key) { + return data.containsKey(key); + } + + @Override + public boolean containsValue(final Object value) { + checkNotNull(value); + + for (final Node node : data.values()) { + if (node.getValue().equals(value)) { + return true; + } + } + return false; + } + + @Override + public V get(final 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(final K key) { + final Node node = data.get(key); + return (node == null) ? null : node.getValue(); + } + + @Override + public V put(final K key, final V value) { + return put(key, value, false); + } + + @Override + public V putIfAbsent(final K key, final 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(final K key, final V value, final 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(final Object key) { + final Node node = data.remove(key); + if (node == null) { + return null; + } + + makeRetired(node); + afterWrite(new RemovalTask(node)); + return node.getValue(); + } + + @SuppressWarnings("unchecked") + @Override + public boolean remove(final Object key, final Object value) { + final Node node = data.get((K) 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(final K key, final 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(final K key, final V oldValue, final 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(final 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(final int limit) { + return orderedKeySet(false, limit); + } + + Set orderedKeySet(final boolean ascending, final 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 Collections.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(final 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(final int limit) { + return orderedMap(false, limit); + } + + Map orderedMap(final boolean ascending, final 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())) { + final Node node = iterator.next(); + map.put(node.key, node.getValue()); + } + return Collections.unmodifiableMap(map); + } finally { + evictionLock.unlock(); + } + } + + /** + * The draining status of the buffers. + */ + enum DrainStatus { + + /** + * A drain is not taking place. + */ + IDLE { + @Override + boolean shouldDrainBuffers(final boolean delayable) { + return !delayable; + } + }, + + /** + * A drain is required due to a pending write modification. + */ + REQUIRED { + @Override + boolean shouldDrainBuffers(final boolean delayable) { + return true; + } + }, + + /** + * A drain is in progress. + */ + PROCESSING { + @Override + boolean shouldDrainBuffers(final 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. + */ + static final class WeightedValue { + final int weight; + final V value; + + WeightedValue(final V value, final int weight) { + this.weight = weight; + this.value = value; + } + + boolean contains(final 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; + + Node prev; + + Node next; + + /** + * Creates a new, unlinked node. + */ + Node(final K key, final WeightedValue weightedValue) { + super(weightedValue); + this.key = key; + } + + @Override + public Node getPrevious() { + return prev; + } + + @Override + public void setPrevious(final Node prev) { + this.prev = prev; + } + + @Override + public Node getNext() { + return next; + } + + @Override + public void setNext(final 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 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(final Object obj) { + return containsKey(obj); + } + + @Override + public boolean remove(final Object obj) { + return (map.remove(obj) != null); + } + + @Override + public Object[] toArray() { + return map.data.keySet().toArray(); + } + + @Override + public T[] toArray(final 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(final 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(); + } + + @SuppressWarnings("unchecked") + @Override + public boolean contains(final Object obj) { + if (false == (obj instanceof Entry)) { + return false; + } + final Entry> entry = (Entry>) obj; + final Node node = map.data.get(entry.getKey()); + return (node != null) && (node.getValue().equals(entry.getValue())); + } + + @Override + public boolean add(final Entry entry) { + return (map.putIfAbsent(entry.getKey(), entry.getValue()) == null); + } + + @SuppressWarnings("unchecked") + @Override + public boolean remove(final Object obj) { + if (!(obj instanceof Entry)) { + return false; + } + final 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 { + private static final long serialVersionUID = 1; + + WriteThroughEntry(final Node node) { + super(node.key, node.getValue()); + } + + @Override + public V setValue(final V value) { + put(getKey(), value); + return super.setValue(value); + } + + Object writeReplace() { + return new SimpleEntry<>(this); + } + } + + /** + * A weigher that enforces that the weight falls within a valid range. + */ + static final class BoundedEntryWeigher implements EntryWeigher, Serializable { + private static final long serialVersionUID = 1; + final EntryWeigher weigher; + + BoundedEntryWeigher(final EntryWeigher weigher) { + checkNotNull(weigher); + this.weigher = weigher; + } + + @Override + public int weightOf(final K key, final V value) { + final 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(final Object e) { + return true; + } + + @Override + public boolean offer(final 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 Collections.emptyIterator(); + } + } + + /** + * A listener that ignores all notifications. + */ + enum DiscardingListener implements BiConsumer { + INSTANCE; + + @Override + public void accept(final Object key, final Object value) { + } + } + + /* ---------------- Serialization Support -------------- */ + + private static final long serialVersionUID = 1; + + Object writeReplace() { + return new SerializationProxy<>(this); + } + + private void readObject(final 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 EntryWeigher weigher; + final BiConsumer listener; + final int concurrencyLevel; + final Map data; + final long capacity; + + SerializationProxy(final ConcurrentLinkedHashMap map) { + concurrencyLevel = map.concurrencyLevel; + data = new HashMap<>(map); + capacity = map.capacity.get(); + listener = map.listener; + weigher = map.weigher; + } + + Object readResolve() { + final ConcurrentLinkedHashMap map = new Builder() + .concurrencyLevel(concurrencyLevel) + .maximumWeightedCapacity(capacity) + .listener(listener) + .weigher(weigher) + .build(); + map.putAll(data); + return map; + } + + private static final long serialVersionUID = 1; + } + + /* ---------------- Builder -------------- */ + + /** + * 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; + + BiConsumer listener; + EntryWeigher weigher; + + int concurrencyLevel; + int initialCapacity; + long capacity; + + /** + * 构造 + */ + @SuppressWarnings("unchecked") + public Builder() { + capacity = -1; + weigher = Weighers.entrySingleton(); + initialCapacity = DEFAULT_INITIAL_CAPACITY; + concurrencyLevel = DEFAULT_CONCURRENCY_LEVEL; + listener = (BiConsumer) 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. + * @return this + * @throws IllegalArgumentException if the initialCapacity is negative + */ + public Builder initialCapacity(final 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 this + * @throws IllegalArgumentException if the maximumWeightedCapacity is + * negative + */ + public Builder maximumWeightedCapacity(final 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 this + * @throws IllegalArgumentException if the concurrencyLevel is less than or + * equal to zero + */ + public Builder concurrencyLevel(final 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 this + * @throws NullPointerException if the listener is null + */ + public Builder listener(final BiConsumer 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 this + * @throws NullPointerException if the weigher is null + */ + public Builder weigher(final 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 this + * @throws NullPointerException if the weigher is null + */ + public Builder weigher(final 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); + } + } +} diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/map/concurrent/EntryWeigher.java b/hutool-core/src/main/java/org/dromara/hutool/core/map/concurrent/EntryWeigher.java new file mode 100755 index 000000000..18d6eab2d --- /dev/null +++ b/hutool-core/src/main/java/org/dromara/hutool/core/map/concurrent/EntryWeigher.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012 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 org.dromara.hutool.core.map.concurrent; + +/** + * A class that can determine the weight of an entry. The total weight threshold + * is used to determine when an eviction is required. + * + * @param 键类型 + * @param 值类型 + * @author ben.manes@gmail.com (Ben Manes) + * @see + * http://code.google.com/p/concurrentlinkedhashmap/ + */ +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); +} diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/map/concurrent/Weigher.java b/hutool-core/src/main/java/org/dromara/hutool/core/map/concurrent/Weigher.java new file mode 100755 index 000000000..d2b28e3e6 --- /dev/null +++ b/hutool-core/src/main/java/org/dromara/hutool/core/map/concurrent/Weigher.java @@ -0,0 +1,37 @@ +/* + * 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 org.dromara.hutool.core.map.concurrent; + +/** + * A class that can determine the weight of a value. The total weight threshold + * is used to determine when an eviction is required. + * + * @param 值类型 + * @author ben.manes@gmail.com (Ben Manes) + * @see + * http://code.google.com/p/concurrentlinkedhashmap/ + */ +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); +} diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/map/concurrent/Weighers.java b/hutool-core/src/main/java/org/dromara/hutool/core/map/concurrent/Weighers.java new file mode 100755 index 000000000..45ad245b9 --- /dev/null +++ b/hutool-core/src/main/java/org/dromara/hutool/core/map/concurrent/Weighers.java @@ -0,0 +1,288 @@ +/* + * 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 org.dromara.hutool.core.map.concurrent; + +import org.dromara.hutool.core.collection.iter.IterUtil; +import org.dromara.hutool.core.lang.Assert; + +import java.io.Serializable; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * A common set of {@link Weigher} and {@link EntryWeigher} implementations. + * + * @author ben.manes@gmail.com (Ben Manes) + * @see + * http://code.google.com/p/concurrentlinkedhashmap/ + */ +public final class Weighers { + + private Weighers() { + throw new AssertionError(); + } + + /** + * A entry weigher backed by the specified weigher. The weight of the value + * determines the weight of the entry. + * + * @param weigher the weigher to be "wrapped" in a entry weigher. + * @param 键类型 + * @param 值类型 + * @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 键类型 + * @param 值类型 + * @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 值类型 + * @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 元素类型 + * @return A weigher where each element takes one unit of capacity. + */ + @SuppressWarnings({"cast", "unchecked"}) + public static Weigher> iterable() { + return (Weigher>) (Weigher) 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 元素类型 + * @return A weigher where each element takes one unit of capacity. + */ + @SuppressWarnings({"cast", "unchecked"}) + public static Weigher> collection() { + return (Weigher>) (Weigher) 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 元素类型 + * @return A weigher where each element takes one unit of capacity. + */ + @SuppressWarnings({"cast"}) + 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 元素类型 + * @return A weigher where each element takes one unit of capacity. + */ + @SuppressWarnings({"cast"}) + 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 键类型 + * @param 值类型 + * @return A weigher where each entry takes one unit of capacity. + */ + @SuppressWarnings({"cast"}) + public static Weigher> map() { + return MapWeigher.INSTANCE; + } + + static final class EntryWeigherView implements EntryWeigher, Serializable { + private static final long serialVersionUID = 1; + final Weigher weigher; + + EntryWeigherView(final Weigher weigher) { + Assert.notNull(weigher); + this.weigher = weigher; + } + + @Override + public int weightOf(final K key, final V value) { + return weigher.weightOf(value); + } + } + + enum SingletonEntryWeigher implements EntryWeigher { + INSTANCE; + + @Override + public int weightOf(final Object key, final Object value) { + return 1; + } + } + + enum SingletonWeigher implements Weigher { + INSTANCE; + + @Override + public int weightOf(final Object value) { + return 1; + } + } + + enum ByteArrayWeigher implements Weigher { + INSTANCE; + + @Override + public int weightOf(final byte[] value) { + return value.length; + } + } + + enum IterableWeigher implements Weigher> { + INSTANCE; + + @Override + public int weightOf(final Iterable values) { + if (values instanceof Collection) { + return ((Collection) values).size(); + } + return IterUtil.size(values); + } + } + + enum CollectionWeigher implements Weigher> { + INSTANCE; + + @Override + public int weightOf(final Collection values) { + return values.size(); + } + } + + enum ListWeigher implements Weigher> { + INSTANCE; + + @Override + public int weightOf(final List values) { + return values.size(); + } + } + + enum SetWeigher implements Weigher> { + INSTANCE; + + @Override + public int weightOf(final Set values) { + return values.size(); + } + } + + enum MapWeigher implements Weigher> { + INSTANCE; + + @Override + public int weightOf(final Map values) { + return values.size(); + } + } +} diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/map/concurrent/package-info.java b/hutool-core/src/main/java/org/dromara/hutool/core/map/concurrent/package-info.java new file mode 100755 index 000000000..9ab59a9f0 --- /dev/null +++ b/hutool-core/src/main/java/org/dromara/hutool/core/map/concurrent/package-info.java @@ -0,0 +1,41 @@ +/* + * 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. + *

+ * {@link com.googlecode.concurrentlinkedhashmap.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.googlecode.concurrentlinkedhashmap.Weighers} class provides + * utility methods for obtaining the most common kinds of implementations. + *

+ * {@link com.googlecode.concurrentlinkedhashmap.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.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap} + * 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 org.dromara.hutool.core.map.concurrent; diff --git a/hutool-core/src/test/java/org/dromara/hutool/core/classloader/ClassLoaderUtilTest.java b/hutool-core/src/test/java/org/dromara/hutool/core/classloader/ClassLoaderUtilTest.java index ca3fa5c06..cc9853a84 100644 --- a/hutool-core/src/test/java/org/dromara/hutool/core/classloader/ClassLoaderUtilTest.java +++ b/hutool-core/src/test/java/org/dromara/hutool/core/classloader/ClassLoaderUtilTest.java @@ -8,7 +8,7 @@ public class ClassLoaderUtilTest { @Test public void isPresentTest() { - final boolean present = ClassLoaderUtil.isPresent("classloader.org.dromara.hutool.core.ClassLoaderUtil"); + final boolean present = ClassLoaderUtil.isPresent("org.dromara.hutool.core.classloader.ClassLoaderUtil"); Assertions.assertTrue(present); } diff --git a/hutool-core/src/test/java/org/dromara/hutool/core/convert/ConvertTest.java b/hutool-core/src/test/java/org/dromara/hutool/core/convert/ConvertTest.java index 150a92257..100a4f738 100644 --- a/hutool-core/src/test/java/org/dromara/hutool/core/convert/ConvertTest.java +++ b/hutool-core/src/test/java/org/dromara/hutool/core/convert/ConvertTest.java @@ -311,7 +311,7 @@ public class ConvertTest { @Test public void toClassTest(){ - final Class convert = Convert.convert(Class.class, "convert.org.dromara.hutool.core.ConvertTest.Product"); + final Class convert = Convert.convert(Class.class, "org.dromara.hutool.core.convert.ConvertTest.Product"); Assertions.assertSame(Product.class, convert); } diff --git a/hutool-core/src/test/java/org/dromara/hutool/core/lang/caller/CallerUtilTest.java b/hutool-core/src/test/java/org/dromara/hutool/core/lang/caller/CallerUtilTest.java index bf56ad75e..8b146b907 100644 --- a/hutool-core/src/test/java/org/dromara/hutool/core/lang/caller/CallerUtilTest.java +++ b/hutool-core/src/test/java/org/dromara/hutool/core/lang/caller/CallerUtilTest.java @@ -11,6 +11,6 @@ public class CallerUtilTest { Assertions.assertEquals("getCallerMethodNameTest", callerMethodName); final String fullCallerMethodName = CallerUtil.getCallerMethodName(true); - Assertions.assertEquals("caller.lang.org.dromara.hutool.core.CallerUtilTest.getCallerMethodNameTest", fullCallerMethodName); + Assertions.assertEquals("org.dromara.hutool.core.lang.caller.CallerUtilTest.getCallerMethodNameTest", fullCallerMethodName); } } diff --git a/hutool-core/src/test/java/org/dromara/hutool/core/reflect/MethodUtilTest.java b/hutool-core/src/test/java/org/dromara/hutool/core/reflect/MethodUtilTest.java index dc063b9ba..f333e6509 100644 --- a/hutool-core/src/test/java/org/dromara/hutool/core/reflect/MethodUtilTest.java +++ b/hutool-core/src/test/java/org/dromara/hutool/core/reflect/MethodUtilTest.java @@ -149,11 +149,11 @@ public class MethodUtilTest { Assertions.assertEquals(3, methods.length); // getA属于本类 - Assertions.assertEquals("public void reflect.org.dromara.hutool.core.ReflectUtilTest$C2.getA()", methods[0].toString()); + Assertions.assertEquals("public void org.dromara.hutool.core.reflect.ReflectUtilTest$C2.getA()", methods[0].toString()); // getB属于父类 - Assertions.assertEquals("public void reflect.org.dromara.hutool.core.ReflectUtilTest$C1.getB()", methods[1].toString()); + Assertions.assertEquals("public void org.dromara.hutool.core.reflect.ReflectUtilTest$C1.getB()", methods[1].toString()); // getC属于接口中的默认方法 - Assertions.assertEquals("public default void reflect.org.dromara.hutool.core.ReflectUtilTest$TestInterface1.getC()", methods[2].toString()); + Assertions.assertEquals("public default void org.dromara.hutool.core.reflect.ReflectUtilTest$TestInterface1.getC()", methods[2].toString()); } @Test diff --git a/hutool-core/src/test/java/org/dromara/hutool/core/util/ClassUtilTest.java b/hutool-core/src/test/java/org/dromara/hutool/core/util/ClassUtilTest.java index ffcb67efd..921a7168a 100644 --- a/hutool-core/src/test/java/org/dromara/hutool/core/util/ClassUtilTest.java +++ b/hutool-core/src/test/java/org/dromara/hutool/core/util/ClassUtilTest.java @@ -17,7 +17,7 @@ public class ClassUtilTest { @Test public void getClassNameTest() { final String className = ClassUtil.getClassName(ClassUtil.class, false); - Assertions.assertEquals("reflect.org.dromara.hutool.core.ClassUtil", className); + Assertions.assertEquals("org.dromara.hutool.core.reflect.ClassUtil", className); final String simpleClassName = ClassUtil.getClassName(ClassUtil.class, true); Assertions.assertEquals("ClassUtil", simpleClassName); @@ -31,9 +31,9 @@ public class ClassUtilTest { @Test public void getShortClassNameTest() { - final String className = "text.org.dromara.hutool.core.StrUtil"; + final String className = "org.dromara.hutool.core.text.StrUtil"; final String result = ClassUtil.getShortClassName(className); - Assertions.assertEquals("c.h.c.t.StrUtil", result); + Assertions.assertEquals("o.d.h.c.t.StrUtil", result); } @Test diff --git a/hutool-extra/src/main/java/org/dromara/hutool/extra/template/engine/jetbrick/JetbrickEngine.java b/hutool-extra/src/main/java/org/dromara/hutool/extra/template/engine/jetbrick/JetbrickEngine.java index 50a65c75e..49aa5da16 100644 --- a/hutool-extra/src/main/java/org/dromara/hutool/extra/template/engine/jetbrick/JetbrickEngine.java +++ b/hutool-extra/src/main/java/org/dromara/hutool/extra/template/engine/jetbrick/JetbrickEngine.java @@ -119,7 +119,7 @@ public class JetbrickEngine implements TemplateEngine { props.setProperty("$loader.root", config.getPath()); break; case STRING: - props.setProperty("$loader", "loader.jetbrick.engine.template.org.dromara.hutool.extra.StringResourceLoader"); + props.setProperty("$loader", "org.dromara.hutool.extra.template.engine.jetbrick.loader.StringResourceLoader"); props.setProperty("$loader.charset", config.getCharsetStr()); break; default: