mirror of
https://github.com/PaperMC/Paper.git
synced 2024-11-18 00:25:39 +01:00
01a13871de
Patch documentation to come Issues with the old system that are fixed now: - World generation does not scale with cpu cores effectively. - Relies on the main thread for scheduling and maintaining chunk state, dropping chunk load/generate rates at lower tps. - Unreliable prioritisation of chunk gen/load calls that block the main thread. - Shutdown logic is utterly unreliable, as it has to wait for all chunks to unload - is it guaranteed that the chunk system is in a state on shutdown that it can reliably do this? Watchdog shutdown also typically failed due to thread checks, which is now resolved. - Saving of data is not unified (i.e can save chunk data without saving entity data, poses problems for desync if shutdown is really abnormal. - Entities are not loaded with chunks. This caused quite a bit of headache for Chunk#getEntities API, but now the new chunk system loads entities with chunks so that they are ready whenever the chunk loads in. Effectively brings the behavior back to 1.16 era, but still storing entities in their own separate regionfiles. The above list is not complete. The patch documentation will complete it. New chunk system hard relies on starlight and dataconverter, and most importantly the new concurrent utilities in ConcurrentUtil. Some of the old async chunk i/o interface (i.e the old file io thread reroutes _some_ calls to the new file io thread) is kept for plugin compat reasons. It will be removed in the next major version of minecraft. The old legacy chunk system patches have been moved to the removed folder in case we need them again.
7019 lines
241 KiB
Diff
7019 lines
241 KiB
Diff
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
|
From: Spottedleaf <Spottedleaf@users.noreply.github.com>
|
|
Date: Sun, 23 Jan 2022 22:58:11 -0800
|
|
Subject: [PATCH] ConcurrentUtil
|
|
|
|
|
|
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/collection/MultiThreadedQueue.java b/src/main/java/ca/spottedleaf/concurrentutil/collection/MultiThreadedQueue.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..f4415f782b32fed25da98e44b172f717c4d46e34
|
|
--- /dev/null
|
|
+++ b/src/main/java/ca/spottedleaf/concurrentutil/collection/MultiThreadedQueue.java
|
|
@@ -0,0 +1,1402 @@
|
|
+package ca.spottedleaf.concurrentutil.collection;
|
|
+
|
|
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
|
|
+import ca.spottedleaf.concurrentutil.util.Validate;
|
|
+import java.lang.invoke.VarHandle;
|
|
+import java.util.ArrayList;
|
|
+import java.util.Collection;
|
|
+import java.util.Iterator;
|
|
+import java.util.List;
|
|
+import java.util.NoSuchElementException;
|
|
+import java.util.Queue;
|
|
+import java.util.Spliterator;
|
|
+import java.util.Spliterators;
|
|
+import java.util.function.Consumer;
|
|
+import java.util.function.IntFunction;
|
|
+import java.util.function.Predicate;
|
|
+
|
|
+/**
|
|
+ * MT-Safe linked first in first out ordered queue.
|
|
+ *
|
|
+ * This queue should out-perform {@link java.util.concurrent.ConcurrentLinkedQueue} in high-contention reads/writes, and is
|
|
+ * not any slower in lower contention reads/writes.
|
|
+ * <p>
|
|
+ * Note that this queue breaks the specification laid out by {@link Collection}, see {@link #preventAdds()} and {@link Collection#add(Object)}.
|
|
+ * </p>
|
|
+ * <p><b>
|
|
+ * This queue will only unlink linked nodes through the {@link #peek()} and {@link #poll()} methods, and this is only if
|
|
+ * they are at the head of the queue.
|
|
+ * </b></p>
|
|
+ * @param <E> Type of element in this queue.
|
|
+ */
|
|
+public class MultiThreadedQueue<E> implements Queue<E> {
|
|
+
|
|
+ protected volatile LinkedNode<E> head; /* Always non-null, high chance of being the actual head */
|
|
+
|
|
+ protected volatile LinkedNode<E> tail; /* Always non-null, high chance of being the actual tail */
|
|
+
|
|
+ /* Note that it is possible to reach head from tail. */
|
|
+
|
|
+ /* IMPL NOTE: Leave hashCode and equals to their defaults */
|
|
+
|
|
+ protected static final VarHandle HEAD_HANDLE = ConcurrentUtil.getVarHandle(MultiThreadedQueue.class, "head", LinkedNode.class);
|
|
+ protected static final VarHandle TAIL_HANDLE = ConcurrentUtil.getVarHandle(MultiThreadedQueue.class, "tail", LinkedNode.class);
|
|
+
|
|
+ /* head */
|
|
+
|
|
+ protected final void setHeadPlain(final LinkedNode<E> newHead) {
|
|
+ HEAD_HANDLE.set(this, newHead);
|
|
+ }
|
|
+
|
|
+ protected final void setHeadOpaque(final LinkedNode<E> newHead) {
|
|
+ HEAD_HANDLE.setOpaque(this, newHead);
|
|
+ }
|
|
+
|
|
+ @SuppressWarnings("unchecked")
|
|
+ protected final LinkedNode<E> getHeadPlain() {
|
|
+ return (LinkedNode<E>)HEAD_HANDLE.get(this);
|
|
+ }
|
|
+
|
|
+ @SuppressWarnings("unchecked")
|
|
+ protected final LinkedNode<E> getHeadOpaque() {
|
|
+ return (LinkedNode<E>)HEAD_HANDLE.getOpaque(this);
|
|
+ }
|
|
+
|
|
+ @SuppressWarnings("unchecked")
|
|
+ protected final LinkedNode<E> getHeadAcquire() {
|
|
+ return (LinkedNode<E>)HEAD_HANDLE.getAcquire(this);
|
|
+ }
|
|
+
|
|
+ /* tail */
|
|
+
|
|
+ protected final void setTailPlain(final LinkedNode<E> newTail) {
|
|
+ TAIL_HANDLE.set(this, newTail);
|
|
+ }
|
|
+
|
|
+ protected final void setTailOpaque(final LinkedNode<E> newTail) {
|
|
+ TAIL_HANDLE.setOpaque(this, newTail);
|
|
+ }
|
|
+
|
|
+ @SuppressWarnings("unchecked")
|
|
+ protected final LinkedNode<E> getTailPlain() {
|
|
+ return (LinkedNode<E>)TAIL_HANDLE.get(this);
|
|
+ }
|
|
+
|
|
+ @SuppressWarnings("unchecked")
|
|
+ protected final LinkedNode<E> getTailOpaque() {
|
|
+ return (LinkedNode<E>)TAIL_HANDLE.getOpaque(this);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Constructs a {@code MultiThreadedQueue}, initially empty.
|
|
+ * <p>
|
|
+ * The returned object may not be published without synchronization.
|
|
+ * </p>
|
|
+ */
|
|
+ public MultiThreadedQueue() {
|
|
+ final LinkedNode<E> value = new LinkedNode<>(null, null);
|
|
+ this.setHeadPlain(value);
|
|
+ this.setTailPlain(value);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Constructs a {@code MultiThreadedQueue}, initially containing all elements in the specified {@code collection}.
|
|
+ * <p>
|
|
+ * The returned object may not be published without synchronization.
|
|
+ * </p>
|
|
+ * @param collection The specified collection.
|
|
+ * @throws NullPointerException If {@code collection} is {@code null} or contains {@code null} elements.
|
|
+ */
|
|
+ public MultiThreadedQueue(final Iterable<? extends E> collection) {
|
|
+ final Iterator<? extends E> elements = collection.iterator();
|
|
+
|
|
+ if (!elements.hasNext()) {
|
|
+ final LinkedNode<E> value = new LinkedNode<>(null, null);
|
|
+ this.setHeadPlain(value);
|
|
+ this.setTailPlain(value);
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ final LinkedNode<E> head = new LinkedNode<>(Validate.notNull(elements.next(), "Null element"), null);
|
|
+ LinkedNode<E> tail = head;
|
|
+
|
|
+ while (elements.hasNext()) {
|
|
+ final LinkedNode<E> next = new LinkedNode<>(Validate.notNull(elements.next(), "Null element"), null);
|
|
+ tail.setNextPlain(next);
|
|
+ tail = next;
|
|
+ }
|
|
+
|
|
+ this.setHeadPlain(head);
|
|
+ this.setTailPlain(tail);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public E remove() throws NoSuchElementException {
|
|
+ final E ret = this.poll();
|
|
+
|
|
+ if (ret == null) {
|
|
+ throw new NoSuchElementException();
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ * <p>
|
|
+ * Contrary to the specification of {@link Collection#add}, this method will fail to add the element to this queue
|
|
+ * and return {@code false} if this queue is add-blocked.
|
|
+ * </p>
|
|
+ */
|
|
+ @Override
|
|
+ public boolean add(final E element) {
|
|
+ return this.offer(element);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Adds the specified element to the tail of this queue. If this queue is currently add-locked, then the queue is
|
|
+ * released from that lock and this element is added. The unlock operation and addition of the specified
|
|
+ * element is atomic.
|
|
+ * @param element The specified element.
|
|
+ * @return {@code true} if this queue previously allowed additions
|
|
+ */
|
|
+ public boolean forceAdd(final E element) {
|
|
+ final LinkedNode<E> node = new LinkedNode<>(element, null);
|
|
+
|
|
+ return !this.forceAppendList(node, node);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public E element() throws NoSuchElementException {
|
|
+ final E ret = this.peek();
|
|
+
|
|
+ if (ret == null) {
|
|
+ throw new NoSuchElementException();
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ * <p>
|
|
+ * This method may also return {@code false} to indicate an element was not added if this queue is add-blocked.
|
|
+ * </p>
|
|
+ */
|
|
+ @Override
|
|
+ public boolean offer(final E element) {
|
|
+ Validate.notNull(element, "Null element");
|
|
+
|
|
+ final LinkedNode<E> node = new LinkedNode<>(element, null);
|
|
+
|
|
+ return this.appendList(node, node);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public E peek() {
|
|
+ for (LinkedNode<E> head = this.getHeadOpaque(), curr = head;;) {
|
|
+ final LinkedNode<E> next = curr.getNextVolatile();
|
|
+ final E element = curr.getElementPlain(); /* Likely in sync */
|
|
+
|
|
+ if (element != null) {
|
|
+ if (this.getHeadOpaque() == head && curr != head) {
|
|
+ this.setHeadOpaque(curr);
|
|
+ }
|
|
+ return element;
|
|
+ }
|
|
+
|
|
+ if (next == null || curr == next) {
|
|
+ return null;
|
|
+ }
|
|
+ curr = next;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public E poll() {
|
|
+ return this.removeHead();
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Retrieves and removes the head of this queue if it matches the specified predicate. If this queue is empty
|
|
+ * or the head does not match the predicate, this function returns {@code null}.
|
|
+ * <p>
|
|
+ * The predicate may be invoked multiple or no times in this call.
|
|
+ * </p>
|
|
+ * @param predicate The specified predicate.
|
|
+ * @return The head if it matches the predicate, or {@code null} if it did not or this queue is empty.
|
|
+ */
|
|
+ public E pollIf(final Predicate<E> predicate) {
|
|
+ return this.removeHead(Validate.notNull(predicate, "Null predicate"));
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public void clear() {
|
|
+ //noinspection StatementWithEmptyBody
|
|
+ while (this.poll() != null);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Prevents elements from being added to this queue. Once this is called, any attempt to add to this queue will fail.
|
|
+ * <p>
|
|
+ * This function is MT-Safe.
|
|
+ * </p>
|
|
+ * @return {@code true} if the queue was modified to prevent additions, {@code false} if it already prevented additions.
|
|
+ */
|
|
+ public boolean preventAdds() {
|
|
+ final LinkedNode<E> deadEnd = new LinkedNode<>(null, null);
|
|
+ deadEnd.setNextPlain(deadEnd);
|
|
+
|
|
+ if (!this.appendList(deadEnd, deadEnd)) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ this.setTailPlain(deadEnd); /* (try to) Ensure tail is set for the following #allowAdds call */
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Allows elements to be added to this queue once again. Note that this function has undefined behaviour if
|
|
+ * {@link #preventAdds()} is not called beforehand. The benefit of this function over {@link #tryAllowAdds()}
|
|
+ * is that this function might perform better.
|
|
+ * <p>
|
|
+ * This function is not MT-Safe.
|
|
+ * </p>
|
|
+ */
|
|
+ public void allowAdds() {
|
|
+ LinkedNode<E> tail = this.getTailPlain();
|
|
+
|
|
+ /* We need to find the tail given the cas on tail isn't atomic (nor volatile) in this.appendList */
|
|
+ /* Thus it is possible for an outdated tail to be set */
|
|
+ while (tail != (tail = tail.getNextPlain())) {}
|
|
+
|
|
+ tail.setNextVolatile(null);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Tries to allow elements to be added to this queue. Returns {@code true} if the queue was previous add-locked,
|
|
+ * {@code false} otherwise.
|
|
+ * <p>
|
|
+ * This function is MT-Safe, however it should not be used with {@link #allowAdds()}.
|
|
+ * </p>
|
|
+ * @return {@code true} if the queue was previously add-locked, {@code false} otherwise.
|
|
+ */
|
|
+ public boolean tryAllowAdds() {
|
|
+ LinkedNode<E> tail = this.getTailPlain();
|
|
+
|
|
+ for (int failures = 0;;) {
|
|
+ /* We need to find the tail given the cas on tail isn't atomic (nor volatile) in this.appendList */
|
|
+ /* Thus it is possible for an outdated tail to be set */
|
|
+ while (tail != (tail = tail.getNextAcquire())) {
|
|
+ if (tail == null) {
|
|
+ return false;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+
|
|
+ if (tail == (tail = tail.compareExchangeNextVolatile(tail, null))) {
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ if (tail == null) {
|
|
+ return false;
|
|
+ }
|
|
+ ++failures;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Atomically adds the specified element to this queue or allows additions to the queue. If additions
|
|
+ * are not allowed, the element is not added.
|
|
+ * <p>
|
|
+ * This function is MT-Safe.
|
|
+ * </p>
|
|
+ * @param element The specified element.
|
|
+ * @return {@code true} if the queue now allows additions, {@code false} if the element was added.
|
|
+ */
|
|
+ public boolean addOrAllowAdds(final E element) {
|
|
+ Validate.notNull(element, "Null element");
|
|
+ int failures = 0;
|
|
+
|
|
+ final LinkedNode<E> append = new LinkedNode<>(element, null);
|
|
+
|
|
+ for (LinkedNode<E> currTail = this.getTailOpaque(), curr = currTail;;) {
|
|
+ /* It has been experimentally shown that placing the read before the backoff results in significantly greater performance */
|
|
+ /* It is likely due to a cache miss caused by another write to the next field */
|
|
+ final LinkedNode<E> next = curr.getNextVolatile();
|
|
+
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+
|
|
+ if (next == null) {
|
|
+ final LinkedNode<E> compared = curr.compareExchangeNextVolatile(null, append);
|
|
+
|
|
+ if (compared == null) {
|
|
+ /* Added */
|
|
+ /* Avoid CASing on tail more than we need to */
|
|
+ /* CAS to avoid setting an out-of-date tail */
|
|
+ if (this.getTailOpaque() == currTail) {
|
|
+ this.setTailOpaque(append);
|
|
+ }
|
|
+ return false; // we added
|
|
+ }
|
|
+
|
|
+ ++failures;
|
|
+ curr = compared;
|
|
+ continue;
|
|
+ } else if (next == curr) {
|
|
+ final LinkedNode<E> compared = curr.compareExchangeNextVolatile(curr, null);
|
|
+
|
|
+ if (compared == curr) {
|
|
+ return true; // we let additions through
|
|
+ }
|
|
+
|
|
+ ++failures;
|
|
+
|
|
+ if (compared != null) {
|
|
+ curr = compared;
|
|
+ }
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ if (curr == currTail) {
|
|
+ /* Tail is likely not up-to-date */
|
|
+ curr = next;
|
|
+ } else {
|
|
+ /* Try to update to tail */
|
|
+ if (currTail == (currTail = this.getTailOpaque())) {
|
|
+ curr = next;
|
|
+ } else {
|
|
+ curr = currTail;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Atomically removes the head from this queue if it exists, otherwise prevents additions to this queue if no
|
|
+ * head is removed.
|
|
+ * <p>
|
|
+ * This function is MT-Safe.
|
|
+ * </p>
|
|
+ * If the queue is already add-blocked and empty then no operation is performed.
|
|
+ * @return {@code null} if the queue is now add-blocked or was previously add-blocked, else returns
|
|
+ * an non-null value which was the previous head of queue.
|
|
+ */
|
|
+ public E pollOrBlockAdds() {
|
|
+ int failures = 0;
|
|
+ for (LinkedNode<E> head = this.getHeadOpaque(), curr = head;;) {
|
|
+ final E currentVal = curr.getElementVolatile();
|
|
+ final LinkedNode<E> next = curr.getNextOpaque();
|
|
+
|
|
+ if (next == curr) {
|
|
+ return null; /* Additions are already blocked */
|
|
+ }
|
|
+
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+
|
|
+ if (currentVal != null) {
|
|
+ if (curr.getAndSetElementVolatile(null) == null) {
|
|
+ ++failures;
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ /* "CAS" to avoid setting an out-of-date head */
|
|
+ if (this.getHeadOpaque() == head) {
|
|
+ this.setHeadOpaque(next != null ? next : curr);
|
|
+ }
|
|
+
|
|
+ return currentVal;
|
|
+ }
|
|
+
|
|
+ if (next == null) {
|
|
+ /* Try to update stale head */
|
|
+ if (curr != head && this.getHeadOpaque() == head) {
|
|
+ this.setHeadOpaque(curr);
|
|
+ }
|
|
+
|
|
+ final LinkedNode<E> compared = curr.compareExchangeNextVolatile(null, curr);
|
|
+
|
|
+ if (compared != null) {
|
|
+ // failed to block additions
|
|
+ curr = compared;
|
|
+ ++failures;
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ return null; /* We blocked additions */
|
|
+ }
|
|
+
|
|
+ if (head == curr) {
|
|
+ /* head is likely not up-to-date */
|
|
+ curr = next;
|
|
+ } else {
|
|
+ /* Try to update to head */
|
|
+ if (head == (head = this.getHeadOpaque())) {
|
|
+ curr = next;
|
|
+ } else {
|
|
+ curr = head;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public boolean remove(final Object object) {
|
|
+ Validate.notNull(object, "Null object to remove");
|
|
+
|
|
+ for (LinkedNode<E> curr = this.getHeadOpaque();;) {
|
|
+ final LinkedNode<E> next = curr.getNextVolatile();
|
|
+ final E element = curr.getElementPlain(); /* Likely in sync */
|
|
+
|
|
+ if (element != null) {
|
|
+ if ((element == object || element.equals(object)) && curr.getAndSetElementVolatile(null) == element) {
|
|
+ return true;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (next == curr || next == null) {
|
|
+ break;
|
|
+ }
|
|
+ curr = next;
|
|
+ }
|
|
+
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public boolean removeIf(final Predicate<? super E> filter) {
|
|
+ Validate.notNull(filter, "Null filter");
|
|
+
|
|
+ boolean ret = false;
|
|
+
|
|
+ for (LinkedNode<E> curr = this.getHeadOpaque();;) {
|
|
+ final LinkedNode<E> next = curr.getNextVolatile();
|
|
+ final E element = curr.getElementPlain(); /* Likely in sync */
|
|
+
|
|
+ if (element != null) {
|
|
+ ret |= filter.test(element) && curr.getAndSetElementVolatile(null) == element;
|
|
+ }
|
|
+
|
|
+ if (next == null || next == curr) {
|
|
+ break;
|
|
+ }
|
|
+ curr = next;
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public boolean removeAll(final Collection<?> collection) {
|
|
+ Validate.notNull(collection, "Null collection");
|
|
+
|
|
+ boolean ret = false;
|
|
+
|
|
+ /* Volatile is required to synchronize with the write to the first element */
|
|
+ for (LinkedNode<E> curr = this.getHeadOpaque();;) {
|
|
+ final LinkedNode<E> next = curr.getNextVolatile();
|
|
+ final E element = curr.getElementPlain(); /* Likely in sync */
|
|
+
|
|
+ if (element != null) {
|
|
+ ret |= collection.contains(element) && curr.getAndSetElementVolatile(null) == element;
|
|
+ }
|
|
+
|
|
+ if (next == null || next == curr) {
|
|
+ break;
|
|
+ }
|
|
+ curr = next;
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public boolean retainAll(final Collection<?> collection) {
|
|
+ Validate.notNull(collection, "Null collection");
|
|
+
|
|
+ boolean ret = false;
|
|
+
|
|
+ for (LinkedNode<E> curr = this.getHeadOpaque();;) {
|
|
+ final LinkedNode<E> next = curr.getNextVolatile();
|
|
+ final E element = curr.getElementPlain(); /* Likely in sync */
|
|
+
|
|
+ if (element != null) {
|
|
+ ret |= !collection.contains(element) && curr.getAndSetElementVolatile(null) == element;
|
|
+ }
|
|
+
|
|
+ if (next == null || next == curr) {
|
|
+ break;
|
|
+ }
|
|
+ curr = next;
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public Object[] toArray() {
|
|
+ final List<E> ret = new ArrayList<>();
|
|
+
|
|
+ for (LinkedNode<E> curr = this.getHeadOpaque();;) {
|
|
+ final LinkedNode<E> next = curr.getNextVolatile();
|
|
+ final E element = curr.getElementPlain(); /* Likely in sync */
|
|
+
|
|
+ if (element != null) {
|
|
+ ret.add(element);
|
|
+ }
|
|
+
|
|
+ if (next == null || next == curr) {
|
|
+ break;
|
|
+ }
|
|
+ curr = next;
|
|
+ }
|
|
+
|
|
+ return ret.toArray();
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public <T> T[] toArray(final T[] array) {
|
|
+ final List<T> ret = new ArrayList<>();
|
|
+
|
|
+ for (LinkedNode<E> curr = this.getHeadOpaque();;) {
|
|
+ final LinkedNode<E> next = curr.getNextVolatile();
|
|
+ final E element = curr.getElementPlain(); /* Likely in sync */
|
|
+
|
|
+ if (element != null) {
|
|
+ //noinspection unchecked
|
|
+ ret.add((T)element);
|
|
+ }
|
|
+
|
|
+ if (next == null || next == curr) {
|
|
+ break;
|
|
+ }
|
|
+ curr = next;
|
|
+ }
|
|
+
|
|
+ return ret.toArray(array);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public <T> T[] toArray(final IntFunction<T[]> generator) {
|
|
+ Validate.notNull(generator, "Null generator");
|
|
+
|
|
+ final List<T> ret = new ArrayList<>();
|
|
+
|
|
+ for (LinkedNode<E> curr = this.getHeadOpaque();;) {
|
|
+ final LinkedNode<E> next = curr.getNextVolatile();
|
|
+ final E element = curr.getElementPlain(); /* Likely in sync */
|
|
+
|
|
+ if (element != null) {
|
|
+ //noinspection unchecked
|
|
+ ret.add((T)element);
|
|
+ }
|
|
+
|
|
+ if (next == null || next == curr) {
|
|
+ break;
|
|
+ }
|
|
+ curr = next;
|
|
+ }
|
|
+
|
|
+ return ret.toArray(generator);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public String toString() {
|
|
+ final StringBuilder builder = new StringBuilder();
|
|
+
|
|
+ builder.append("MultiThreadedQueue: {elements: {");
|
|
+
|
|
+ int deadEntries = 0;
|
|
+ int totalEntries = 0;
|
|
+ int aliveEntries = 0;
|
|
+
|
|
+ boolean addLocked = false;
|
|
+
|
|
+ for (LinkedNode<E> curr = this.getHeadOpaque();; ++totalEntries) {
|
|
+ final LinkedNode<E> next = curr.getNextVolatile();
|
|
+ final E element = curr.getElementPlain(); /* Likely in sync */
|
|
+
|
|
+ if (element == null) {
|
|
+ ++deadEntries;
|
|
+ } else {
|
|
+ ++aliveEntries;
|
|
+ }
|
|
+
|
|
+ if (totalEntries != 0) {
|
|
+ builder.append(", ");
|
|
+ }
|
|
+
|
|
+ builder.append(totalEntries).append(": \"").append(element).append('"');
|
|
+
|
|
+ if (next == null) {
|
|
+ break;
|
|
+ }
|
|
+ if (curr == next) {
|
|
+ addLocked = true;
|
|
+ break;
|
|
+ }
|
|
+ curr = next;
|
|
+ }
|
|
+
|
|
+ builder.append("}, total_entries: \"").append(totalEntries).append("\", alive_entries: \"").append(aliveEntries)
|
|
+ .append("\", dead_entries:").append(deadEntries).append("\", add_locked: \"").append(addLocked)
|
|
+ .append("\"}");
|
|
+
|
|
+ return builder.toString();
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Adds all elements from the specified collection to this queue. The addition is atomic.
|
|
+ * @param collection The specified collection.
|
|
+ * @return {@code true} if all elements were added successfully, or {@code false} if this queue is add-blocked, or
|
|
+ * {@code false} if the specified collection contains no elements.
|
|
+ */
|
|
+ @Override
|
|
+ public boolean addAll(final Collection<? extends E> collection) {
|
|
+ return this.addAll((Iterable<? extends E>)collection);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Adds all elements from the specified iterable object to this queue. The addition is atomic.
|
|
+ * @param iterable The specified iterable object.
|
|
+ * @return {@code true} if all elements were added successfully, or {@code false} if this queue is add-blocked, or
|
|
+ * {@code false} if the specified iterable contains no elements.
|
|
+ */
|
|
+ public boolean addAll(final Iterable<? extends E> iterable) {
|
|
+ Validate.notNull(iterable, "Null iterable");
|
|
+
|
|
+ final Iterator<? extends E> elements = iterable.iterator();
|
|
+ if (!elements.hasNext()) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ /* Build a list of nodes to append */
|
|
+ /* This is an much faster due to the fact that zero additional synchronization is performed */
|
|
+
|
|
+ final LinkedNode<E> head = new LinkedNode<>(Validate.notNull(elements.next(), "Null element"), null);
|
|
+ LinkedNode<E> tail = head;
|
|
+
|
|
+ while (elements.hasNext()) {
|
|
+ final LinkedNode<E> next = new LinkedNode<>(Validate.notNull(elements.next(), "Null element"), null);
|
|
+ tail.setNextPlain(next);
|
|
+ tail = next;
|
|
+ }
|
|
+
|
|
+ return this.appendList(head, tail);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Adds all of the elements from the specified array to this queue.
|
|
+ * @param items The specified array.
|
|
+ * @return {@code true} if all elements were added successfully, or {@code false} if this queue is add-blocked, or
|
|
+ * {@code false} if the specified array has a length of 0.
|
|
+ */
|
|
+ public boolean addAll(final E[] items) {
|
|
+ return this.addAll(items, 0, items.length);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Adds all of the elements from the specified array to this queue.
|
|
+ * @param items The specified array.
|
|
+ * @param off The offset in the array.
|
|
+ * @param len The number of items.
|
|
+ * @return {@code true} if all elements were added successfully, or {@code false} if this queue is add-blocked, or
|
|
+ * {@code false} if the specified array has a length of 0.
|
|
+ */
|
|
+ public boolean addAll(final E[] items, final int off, final int len) {
|
|
+ Validate.notNull(items, "Items may not be null");
|
|
+ Validate.arrayBounds(off, len, items.length, "Items array indices out of bounds");
|
|
+
|
|
+ if (len == 0) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ final LinkedNode<E> head = new LinkedNode<>(Validate.notNull(items[off], "Null element"), null);
|
|
+ LinkedNode<E> tail = head;
|
|
+
|
|
+ for (int i = 1; i < len; ++i) {
|
|
+ final LinkedNode<E> next = new LinkedNode<>(Validate.notNull(items[off + i], "Null element"), null);
|
|
+ tail.setNextPlain(next);
|
|
+ tail = next;
|
|
+ }
|
|
+
|
|
+ return this.appendList(head, tail);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public boolean containsAll(final Collection<?> collection) {
|
|
+ Validate.notNull(collection, "Null collection");
|
|
+
|
|
+ for (final Object element : collection) {
|
|
+ if (!this.contains(element)) {
|
|
+ return false;
|
|
+ }
|
|
+ }
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public Iterator<E> iterator() {
|
|
+ return new LinkedIterator<>(this.getHeadOpaque());
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ * <p>
|
|
+ * Note that this function is computed non-atomically and in O(n) time. The value returned may not be representative of
|
|
+ * the queue in its current state.
|
|
+ * </p>
|
|
+ */
|
|
+ @Override
|
|
+ public int size() {
|
|
+ int size = 0;
|
|
+
|
|
+ /* Volatile is required to synchronize with the write to the first element */
|
|
+ for (LinkedNode<E> curr = this.getHeadOpaque();;) {
|
|
+ final LinkedNode<E> next = curr.getNextVolatile();
|
|
+ final E element = curr.getElementPlain(); /* Likely in sync */
|
|
+
|
|
+ if (element != null) {
|
|
+ ++size;
|
|
+ }
|
|
+
|
|
+ if (next == null || next == curr) {
|
|
+ break;
|
|
+ }
|
|
+ curr = next;
|
|
+ }
|
|
+
|
|
+ return size;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public boolean isEmpty() {
|
|
+ return this.peek() == null;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public boolean contains(final Object object) {
|
|
+ Validate.notNull(object, "Null object");
|
|
+
|
|
+ for (LinkedNode<E> curr = this.getHeadOpaque();;) {
|
|
+ final LinkedNode<E> next = curr.getNextVolatile();
|
|
+ final E element = curr.getElementPlain(); /* Likely in sync */
|
|
+
|
|
+ if (element != null && (element == object || element.equals(object))) {
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ if (next == null || next == curr) {
|
|
+ break;
|
|
+ }
|
|
+ curr = next;
|
|
+ }
|
|
+
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Finds the first element in this queue that matches the predicate.
|
|
+ * @param predicate The predicate to test elements against.
|
|
+ * @return The first element that matched the predicate, {@code null} if none matched.
|
|
+ */
|
|
+ public E find(final Predicate<E> predicate) {
|
|
+ Validate.notNull(predicate, "Null predicate");
|
|
+
|
|
+ for (LinkedNode<E> curr = this.getHeadOpaque();;) {
|
|
+ final LinkedNode<E> next = curr.getNextVolatile();
|
|
+ final E element = curr.getElementPlain(); /* Likely in sync */
|
|
+
|
|
+ if (element != null && predicate.test(element)) {
|
|
+ return element;
|
|
+ }
|
|
+
|
|
+ if (next == null || next == curr) {
|
|
+ break;
|
|
+ }
|
|
+ curr = next;
|
|
+ }
|
|
+
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public void forEach(final Consumer<? super E> action) {
|
|
+ Validate.notNull(action, "Null action");
|
|
+
|
|
+ for (LinkedNode<E> curr = this.getHeadOpaque();;) {
|
|
+ final LinkedNode<E> next = curr.getNextVolatile();
|
|
+ final E element = curr.getElementPlain(); /* Likely in sync */
|
|
+
|
|
+ if (element != null) {
|
|
+ action.accept(element);
|
|
+ }
|
|
+
|
|
+ if (next == null || next == curr) {
|
|
+ break;
|
|
+ }
|
|
+ curr = next;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // return true if normal addition, false if the queue previously disallowed additions
|
|
+ protected final boolean forceAppendList(final LinkedNode<E> head, final LinkedNode<E> tail) {
|
|
+ int failures = 0;
|
|
+
|
|
+ for (LinkedNode<E> currTail = this.getTailOpaque(), curr = currTail;;) {
|
|
+ /* It has been experimentally shown that placing the read before the backoff results in significantly greater performance */
|
|
+ /* It is likely due to a cache miss caused by another write to the next field */
|
|
+ final LinkedNode<E> next = curr.getNextVolatile();
|
|
+
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+
|
|
+ if (next == null || next == curr) {
|
|
+ final LinkedNode<E> compared = curr.compareExchangeNextVolatile(next, head);
|
|
+
|
|
+ if (compared == next) {
|
|
+ /* Added */
|
|
+ /* Avoid CASing on tail more than we need to */
|
|
+ /* "CAS" to avoid setting an out-of-date tail */
|
|
+ if (this.getTailOpaque() == currTail) {
|
|
+ this.setTailOpaque(tail);
|
|
+ }
|
|
+ return next != curr;
|
|
+ }
|
|
+
|
|
+ ++failures;
|
|
+ curr = compared;
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ if (curr == currTail) {
|
|
+ /* Tail is likely not up-to-date */
|
|
+ curr = next;
|
|
+ } else {
|
|
+ /* Try to update to tail */
|
|
+ if (currTail == (currTail = this.getTailOpaque())) {
|
|
+ curr = next;
|
|
+ } else {
|
|
+ curr = currTail;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // return true if successful, false otherwise
|
|
+ protected final boolean appendList(final LinkedNode<E> head, final LinkedNode<E> tail) {
|
|
+ int failures = 0;
|
|
+
|
|
+ for (LinkedNode<E> currTail = this.getTailOpaque(), curr = currTail;;) {
|
|
+ /* It has been experimentally shown that placing the read before the backoff results in significantly greater performance */
|
|
+ /* It is likely due to a cache miss caused by another write to the next field */
|
|
+ final LinkedNode<E> next = curr.getNextVolatile();
|
|
+
|
|
+ if (next == curr) {
|
|
+ /* Additions are stopped */
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+
|
|
+ if (next == null) {
|
|
+ final LinkedNode<E> compared = curr.compareExchangeNextVolatile(null, head);
|
|
+
|
|
+ if (compared == null) {
|
|
+ /* Added */
|
|
+ /* Avoid CASing on tail more than we need to */
|
|
+ /* CAS to avoid setting an out-of-date tail */
|
|
+ if (this.getTailOpaque() == currTail) {
|
|
+ this.setTailOpaque(tail);
|
|
+ }
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ ++failures;
|
|
+ curr = compared;
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ if (curr == currTail) {
|
|
+ /* Tail is likely not up-to-date */
|
|
+ curr = next;
|
|
+ } else {
|
|
+ /* Try to update to tail */
|
|
+ if (currTail == (currTail = this.getTailOpaque())) {
|
|
+ curr = next;
|
|
+ } else {
|
|
+ curr = currTail;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ protected final E removeHead(final Predicate<E> predicate) {
|
|
+ int failures = 0;
|
|
+ for (LinkedNode<E> head = this.getHeadOpaque(), curr = head;;) {
|
|
+ // volatile here synchronizes-with writes to element
|
|
+ final LinkedNode<E> next = curr.getNextVolatile();
|
|
+ final E currentVal = curr.getElementPlain();
|
|
+
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+
|
|
+ if (currentVal != null) {
|
|
+ if (!predicate.test(currentVal)) {
|
|
+ /* Try to update stale head */
|
|
+ if (curr != head && this.getHeadOpaque() == head) {
|
|
+ this.setHeadOpaque(curr);
|
|
+ }
|
|
+ return null;
|
|
+ }
|
|
+ if (curr.getAndSetElementVolatile(null) == null) {
|
|
+ /* Failed to get head */
|
|
+ if (curr == (curr = next) || next == null) {
|
|
+ return null;
|
|
+ }
|
|
+ ++failures;
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ /* "CAS" to avoid setting an out-of-date head */
|
|
+ if (this.getHeadOpaque() == head) {
|
|
+ this.setHeadOpaque(next != null ? next : curr);
|
|
+ }
|
|
+
|
|
+ return currentVal;
|
|
+ }
|
|
+
|
|
+ if (curr == next || next == null) {
|
|
+ /* Try to update stale head */
|
|
+ if (curr != head && this.getHeadOpaque() == head) {
|
|
+ this.setHeadOpaque(curr);
|
|
+ }
|
|
+ return null; /* End of queue */
|
|
+ }
|
|
+
|
|
+ if (head == curr) {
|
|
+ /* head is likely not up-to-date */
|
|
+ curr = next;
|
|
+ } else {
|
|
+ /* Try to update to head */
|
|
+ if (head == (head = this.getHeadOpaque())) {
|
|
+ curr = next;
|
|
+ } else {
|
|
+ curr = head;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ protected final E removeHead() {
|
|
+ int failures = 0;
|
|
+ for (LinkedNode<E> head = this.getHeadOpaque(), curr = head;;) {
|
|
+ final LinkedNode<E> next = curr.getNextVolatile();
|
|
+ final E currentVal = curr.getElementPlain();
|
|
+
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+
|
|
+ if (currentVal != null) {
|
|
+ if (curr.getAndSetElementVolatile(null) == null) {
|
|
+ /* Failed to get head */
|
|
+ if (curr == (curr = next) || next == null) {
|
|
+ return null;
|
|
+ }
|
|
+ ++failures;
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ /* "CAS" to avoid setting an out-of-date head */
|
|
+ if (this.getHeadOpaque() == head) {
|
|
+ this.setHeadOpaque(next != null ? next : curr);
|
|
+ }
|
|
+
|
|
+ return currentVal;
|
|
+ }
|
|
+
|
|
+ if (curr == next || next == null) {
|
|
+ /* Try to update stale head */
|
|
+ if (curr != head && this.getHeadOpaque() == head) {
|
|
+ this.setHeadOpaque(curr);
|
|
+ }
|
|
+ return null; /* End of queue */
|
|
+ }
|
|
+
|
|
+ if (head == curr) {
|
|
+ /* head is likely not up-to-date */
|
|
+ curr = next;
|
|
+ } else {
|
|
+ /* Try to update to head */
|
|
+ if (head == (head = this.getHeadOpaque())) {
|
|
+ curr = next;
|
|
+ } else {
|
|
+ curr = head;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Empties the queue into the specified consumer. This function is optimized for single-threaded reads, and should
|
|
+ * be faster than a loop on {@link #poll()}.
|
|
+ * <p>
|
|
+ * This function is not MT-Safe. This function cannot be called with other read operations ({@link #peek()}, {@link #poll()},
|
|
+ * {@link #clear()}, etc).
|
|
+ * Write operations are safe to be called concurrently.
|
|
+ * </p>
|
|
+ * @param consumer The consumer to accept the elements.
|
|
+ * @return The total number of elements drained.
|
|
+ */
|
|
+ public int drain(final Consumer<E> consumer) {
|
|
+ return this.drain(consumer, false, ConcurrentUtil::rethrow);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Empties the queue into the specified consumer. This function is optimized for single-threaded reads, and should
|
|
+ * be faster than a loop on {@link #poll()}.
|
|
+ * <p>
|
|
+ * If {@code preventAdds} is {@code true}, then after this function returns the queue is guaranteed to be empty and
|
|
+ * additions to the queue will fail.
|
|
+ * </p>
|
|
+ * <p>
|
|
+ * This function is not MT-Safe. This function cannot be called with other read operations ({@link #peek()}, {@link #poll()},
|
|
+ * {@link #clear()}, etc).
|
|
+ * Write operations are safe to be called concurrently.
|
|
+ * </p>
|
|
+ * @param consumer The consumer to accept the elements.
|
|
+ * @param preventAdds Whether to prevent additions to this queue after draining.
|
|
+ * @return The total number of elements drained.
|
|
+ */
|
|
+ public int drain(final Consumer<E> consumer, final boolean preventAdds) {
|
|
+ return this.drain(consumer, preventAdds, ConcurrentUtil::rethrow);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Empties the queue into the specified consumer. This function is optimized for single-threaded reads, and should
|
|
+ * be faster than a loop on {@link #poll()}.
|
|
+ * <p>
|
|
+ * If {@code preventAdds} is {@code true}, then after this function returns the queue is guaranteed to be empty and
|
|
+ * additions to the queue will fail.
|
|
+ * </p>
|
|
+ * <p>
|
|
+ * This function is not MT-Safe. This function cannot be called with other read operations ({@link #peek()}, {@link #poll()},
|
|
+ * {@link #clear()}, {@link #remove(Object)} etc).
|
|
+ * Only write operations are safe to be called concurrently.
|
|
+ * </p>
|
|
+ * @param consumer The consumer to accept the elements.
|
|
+ * @param preventAdds Whether to prevent additions to this queue after draining.
|
|
+ * @param exceptionHandler Invoked when the consumer raises an exception.
|
|
+ * @return The total number of elements drained.
|
|
+ */
|
|
+ public int drain(final Consumer<E> consumer, final boolean preventAdds, final Consumer<Throwable> exceptionHandler) {
|
|
+ Validate.notNull(consumer, "Null consumer");
|
|
+ Validate.notNull(exceptionHandler, "Null exception handler");
|
|
+
|
|
+ /* This function assumes proper synchronization is made to ensure drain and no other read function are called concurrently */
|
|
+ /* This allows plain write usages instead of opaque or higher */
|
|
+ int total = 0;
|
|
+
|
|
+ final LinkedNode<E> head = this.getHeadAcquire(); /* Required to synchronize with the write to the first element field */
|
|
+ LinkedNode<E> curr = head;
|
|
+
|
|
+ for (;;) {
|
|
+ /* Volatile acquires with the write to the element field */
|
|
+ final E currentVal = curr.getElementPlain();
|
|
+ LinkedNode<E> next = curr.getNextVolatile();
|
|
+
|
|
+ if (next == curr) {
|
|
+ /* Add-locked nodes always have a null value */
|
|
+ break;
|
|
+ }
|
|
+
|
|
+ if (currentVal == null) {
|
|
+ if (next == null) {
|
|
+ if (preventAdds && (next = curr.compareExchangeNextVolatile(null, curr)) != null) {
|
|
+ // failed to prevent adds, continue
|
|
+ curr = next;
|
|
+ continue;
|
|
+ } else {
|
|
+ // we're done here
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+ curr = next;
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ try {
|
|
+ consumer.accept(currentVal);
|
|
+ } catch (final Exception ex) {
|
|
+ this.setHeadOpaque(next != null ? next : curr); /* Avoid perf penalty (of reiterating) if the exception handler decides to re-throw */
|
|
+ curr.setElementOpaque(null); /* set here, we might re-throw */
|
|
+
|
|
+ exceptionHandler.accept(ex);
|
|
+ }
|
|
+
|
|
+ curr.setElementOpaque(null);
|
|
+
|
|
+ ++total;
|
|
+
|
|
+ if (next == null) {
|
|
+ if (preventAdds && (next = curr.compareExchangeNextVolatile(null, curr)) != null) {
|
|
+ /* Retry with next value */
|
|
+ curr = next;
|
|
+ continue;
|
|
+ }
|
|
+ break;
|
|
+ }
|
|
+
|
|
+ curr = next;
|
|
+ }
|
|
+ if (curr != head) {
|
|
+ this.setHeadOpaque(curr); /* While this may be a plain write, eventually publish it for methods such as find. */
|
|
+ }
|
|
+ return total;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public Spliterator<E> spliterator() { // TODO implement
|
|
+ return Spliterators.spliterator(this, Spliterator.CONCURRENT |
|
|
+ Spliterator.NONNULL | Spliterator.ORDERED);
|
|
+ }
|
|
+
|
|
+ protected static final class LinkedNode<E> {
|
|
+
|
|
+ protected volatile Object element;
|
|
+ protected volatile LinkedNode<E> next;
|
|
+
|
|
+ protected static final VarHandle ELEMENT_HANDLE = ConcurrentUtil.getVarHandle(LinkedNode.class, "element", Object.class);
|
|
+ protected static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(LinkedNode.class, "next", LinkedNode.class);
|
|
+
|
|
+ protected LinkedNode(final Object element, final LinkedNode<E> next) {
|
|
+ ELEMENT_HANDLE.set(this, element);
|
|
+ NEXT_HANDLE.set(this, next);
|
|
+ }
|
|
+
|
|
+ /* element */
|
|
+
|
|
+ @SuppressWarnings("unchecked")
|
|
+ protected final E getElementPlain() {
|
|
+ return (E)ELEMENT_HANDLE.get(this);
|
|
+ }
|
|
+
|
|
+ @SuppressWarnings("unchecked")
|
|
+ protected final E getElementVolatile() {
|
|
+ return (E)ELEMENT_HANDLE.getVolatile(this);
|
|
+ }
|
|
+
|
|
+ protected final void setElementPlain(final E update) {
|
|
+ ELEMENT_HANDLE.set(this, (Object)update);
|
|
+ }
|
|
+
|
|
+ protected final void setElementOpaque(final E update) {
|
|
+ ELEMENT_HANDLE.setOpaque(this, (Object)update);
|
|
+ }
|
|
+
|
|
+ protected final void setElementVolatile(final E update) {
|
|
+ ELEMENT_HANDLE.setVolatile(this, (Object)update);
|
|
+ }
|
|
+
|
|
+ @SuppressWarnings("unchecked")
|
|
+ protected final E getAndSetElementVolatile(final E update) {
|
|
+ return (E)ELEMENT_HANDLE.getAndSet(this, update);
|
|
+ }
|
|
+
|
|
+ @SuppressWarnings("unchecked")
|
|
+ protected final E compareExchangeElementVolatile(final E expect, final E update) {
|
|
+ return (E)ELEMENT_HANDLE.compareAndExchange(this, expect, update);
|
|
+ }
|
|
+
|
|
+ /* next */
|
|
+
|
|
+ @SuppressWarnings("unchecked")
|
|
+ protected final LinkedNode<E> getNextPlain() {
|
|
+ return (LinkedNode<E>)NEXT_HANDLE.get(this);
|
|
+ }
|
|
+
|
|
+ @SuppressWarnings("unchecked")
|
|
+ protected final LinkedNode<E> getNextOpaque() {
|
|
+ return (LinkedNode<E>)NEXT_HANDLE.getOpaque(this);
|
|
+ }
|
|
+
|
|
+ @SuppressWarnings("unchecked")
|
|
+ protected final LinkedNode<E> getNextAcquire() {
|
|
+ return (LinkedNode<E>)NEXT_HANDLE.getAcquire(this);
|
|
+ }
|
|
+
|
|
+ @SuppressWarnings("unchecked")
|
|
+ protected final LinkedNode<E> getNextVolatile() {
|
|
+ return (LinkedNode<E>)NEXT_HANDLE.getVolatile(this);
|
|
+ }
|
|
+
|
|
+ protected final void setNextPlain(final LinkedNode<E> next) {
|
|
+ NEXT_HANDLE.set(this, next);
|
|
+ }
|
|
+
|
|
+ protected final void setNextVolatile(final LinkedNode<E> next) {
|
|
+ NEXT_HANDLE.setVolatile(this, next);
|
|
+ }
|
|
+
|
|
+ @SuppressWarnings("unchecked")
|
|
+ protected final LinkedNode<E> compareExchangeNextVolatile(final LinkedNode<E> expect, final LinkedNode<E> set) {
|
|
+ return (LinkedNode<E>)NEXT_HANDLE.compareAndExchange(this, expect, set);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ protected static final class LinkedIterator<E> implements Iterator<E> {
|
|
+
|
|
+ protected LinkedNode<E> curr; /* last returned by next() */
|
|
+ protected LinkedNode<E> next; /* next to return from next() */
|
|
+ protected E nextElement; /* cached to avoid a race condition with removing or polling */
|
|
+
|
|
+ protected LinkedIterator(final LinkedNode<E> start) {
|
|
+ /* setup nextElement and next */
|
|
+ for (LinkedNode<E> curr = start;;) {
|
|
+ final LinkedNode<E> next = curr.getNextVolatile();
|
|
+
|
|
+ final E element = curr.getElementPlain();
|
|
+
|
|
+ if (element != null) {
|
|
+ this.nextElement = element;
|
|
+ this.next = curr;
|
|
+ break;
|
|
+ }
|
|
+
|
|
+ if (next == null || next == curr) {
|
|
+ break;
|
|
+ }
|
|
+ curr = next;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ protected final void findNext() {
|
|
+ /* only called if this.nextElement != null, which means this.next != null */
|
|
+ for (LinkedNode<E> curr = this.next;;) {
|
|
+ final LinkedNode<E> next = curr.getNextVolatile();
|
|
+
|
|
+ if (next == null || next == curr) {
|
|
+ break;
|
|
+ }
|
|
+
|
|
+ final E element = next.getElementPlain();
|
|
+
|
|
+ if (element != null) {
|
|
+ this.nextElement = element;
|
|
+ this.curr = this.next; /* this.next will be the value returned from next(), set this.curr for remove() */
|
|
+ this.next = next;
|
|
+ return;
|
|
+ }
|
|
+ curr = next;
|
|
+ }
|
|
+
|
|
+ /* out of nodes to iterate */
|
|
+ /* keep curr for remove() calls */
|
|
+ this.next = null;
|
|
+ this.nextElement = null;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public boolean hasNext() {
|
|
+ return this.nextElement != null;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public E next() {
|
|
+ final E element = this.nextElement;
|
|
+
|
|
+ if (element == null) {
|
|
+ throw new NoSuchElementException();
|
|
+ }
|
|
+
|
|
+ this.findNext();
|
|
+
|
|
+ return element;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public void remove() {
|
|
+ if (this.curr == null) {
|
|
+ throw new IllegalStateException();
|
|
+ }
|
|
+
|
|
+ this.curr.setElementVolatile(null);
|
|
+ this.curr = null;
|
|
+ }
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/collection/SRSWLinkedQueue.java b/src/main/java/ca/spottedleaf/concurrentutil/collection/SRSWLinkedQueue.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..597659f38aa816646dcda4ca39c002b6d9f9a792
|
|
--- /dev/null
|
|
+++ b/src/main/java/ca/spottedleaf/concurrentutil/collection/SRSWLinkedQueue.java
|
|
@@ -0,0 +1,148 @@
|
|
+package ca.spottedleaf.concurrentutil.collection;
|
|
+
|
|
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
|
|
+import ca.spottedleaf.concurrentutil.util.Validate;
|
|
+import java.lang.invoke.VarHandle;
|
|
+import java.util.ConcurrentModificationException;
|
|
+
|
|
+/**
|
|
+ * Single reader thread single writer thread queue. The reader side of the queue is ordered by acquire semantics,
|
|
+ * and the writer side of the queue is ordered by release semantics.
|
|
+ */
|
|
+// TODO test
|
|
+public class SRSWLinkedQueue<E> {
|
|
+
|
|
+ // always non-null
|
|
+ protected LinkedNode<E> head;
|
|
+
|
|
+ // always non-null
|
|
+ protected LinkedNode<E> tail;
|
|
+
|
|
+ /* IMPL NOTE: Leave hashCode and equals to their defaults */
|
|
+
|
|
+ public SRSWLinkedQueue() {
|
|
+ final LinkedNode<E> dummy = new LinkedNode<>(null, null);
|
|
+ this.head = this.tail = dummy;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Must be the reader thread.
|
|
+ *
|
|
+ * <p>
|
|
+ * Returns, without removing, the first element of this queue.
|
|
+ * </p>
|
|
+ * @return Returns, without removing, the first element of this queue.
|
|
+ */
|
|
+ public E peekFirst() {
|
|
+ LinkedNode<E> head = this.head;
|
|
+ E ret = head.getElementPlain();
|
|
+ if (ret == null) {
|
|
+ head = head.getNextAcquire();
|
|
+ if (head == null) {
|
|
+ // empty
|
|
+ return null;
|
|
+ }
|
|
+ // update head reference for next poll() call
|
|
+ this.head = head;
|
|
+ // guaranteed to be non-null
|
|
+ ret = head.getElementPlain();
|
|
+ if (ret == null) {
|
|
+ throw new ConcurrentModificationException("Multiple reader threads");
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Must be the reader thread.
|
|
+ *
|
|
+ * <p>
|
|
+ * Returns and removes the first element of this queue.
|
|
+ * </p>
|
|
+ * @return Returns and removes the first element of this queue.
|
|
+ */
|
|
+ public E poll() {
|
|
+ LinkedNode<E> head = this.head;
|
|
+ E ret = head.getElementPlain();
|
|
+ if (ret == null) {
|
|
+ head = head.getNextAcquire();
|
|
+ if (head == null) {
|
|
+ // empty
|
|
+ return null;
|
|
+ }
|
|
+ // guaranteed to be non-null
|
|
+ ret = head.getElementPlain();
|
|
+ if (ret == null) {
|
|
+ throw new ConcurrentModificationException("Multiple reader threads");
|
|
+ }
|
|
+ }
|
|
+
|
|
+ head.setElementPlain(null);
|
|
+ LinkedNode<E> next = head.getNextAcquire();
|
|
+ this.head = next == null ? head : next;
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Must be the writer thread.
|
|
+ *
|
|
+ * <p>
|
|
+ * Adds the element to the end of the queue.
|
|
+ * </p>
|
|
+ *
|
|
+ * @throws NullPointerException If the provided element is null
|
|
+ */
|
|
+ public void addLast(final E element) {
|
|
+ Validate.notNull(element, "Provided element cannot be null");
|
|
+ final LinkedNode<E> append = new LinkedNode<>(element, null);
|
|
+
|
|
+ this.tail.setNextRelease(append);
|
|
+ this.tail = append;
|
|
+ }
|
|
+
|
|
+ protected static final class LinkedNode<E> {
|
|
+
|
|
+ protected volatile Object element;
|
|
+ protected volatile LinkedNode<E> next;
|
|
+
|
|
+ protected static final VarHandle ELEMENT_HANDLE = ConcurrentUtil.getVarHandle(LinkedNode.class, "element", Object.class);
|
|
+ protected static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(LinkedNode.class, "next", LinkedNode.class);
|
|
+
|
|
+ protected LinkedNode(final Object element, final LinkedNode<E> next) {
|
|
+ ELEMENT_HANDLE.set(this, element);
|
|
+ NEXT_HANDLE.set(this, next);
|
|
+ }
|
|
+
|
|
+ /* element */
|
|
+
|
|
+ @SuppressWarnings("unchecked")
|
|
+ protected final E getElementPlain() {
|
|
+ return (E)ELEMENT_HANDLE.get(this);
|
|
+ }
|
|
+
|
|
+ protected final void setElementPlain(final E update) {
|
|
+ ELEMENT_HANDLE.set(this, (Object)update);
|
|
+ }
|
|
+ /* next */
|
|
+
|
|
+ @SuppressWarnings("unchecked")
|
|
+ protected final LinkedNode<E> getNextPlain() {
|
|
+ return (LinkedNode<E>)NEXT_HANDLE.get(this);
|
|
+ }
|
|
+
|
|
+ @SuppressWarnings("unchecked")
|
|
+ protected final LinkedNode<E> getNextAcquire() {
|
|
+ return (LinkedNode<E>)NEXT_HANDLE.getAcquire(this);
|
|
+ }
|
|
+
|
|
+ protected final void setNextPlain(final LinkedNode<E> next) {
|
|
+ NEXT_HANDLE.set(this, next);
|
|
+ }
|
|
+
|
|
+ protected final void setNextRelease(final LinkedNode<E> next) {
|
|
+ NEXT_HANDLE.setRelease(this, next);
|
|
+ }
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/completable/Completable.java b/src/main/java/ca/spottedleaf/concurrentutil/completable/Completable.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..a1ad3308f9c3545a604b635896259a1cd3382b2a
|
|
--- /dev/null
|
|
+++ b/src/main/java/ca/spottedleaf/concurrentutil/completable/Completable.java
|
|
@@ -0,0 +1,98 @@
|
|
+package ca.spottedleaf.concurrentutil.completable;
|
|
+
|
|
+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
|
|
+import ca.spottedleaf.concurrentutil.executor.Cancellable;
|
|
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
|
|
+import com.mojang.logging.LogUtils;
|
|
+import org.slf4j.Logger;
|
|
+import java.util.function.BiConsumer;
|
|
+
|
|
+public final class Completable<T> {
|
|
+
|
|
+ private static final Logger LOGGER = LogUtils.getLogger();
|
|
+
|
|
+ private final MultiThreadedQueue<BiConsumer<T, Throwable>> waiters = new MultiThreadedQueue<>();
|
|
+ private T result;
|
|
+ private Throwable throwable;
|
|
+ private volatile boolean completed;
|
|
+
|
|
+ public boolean isCompleted() {
|
|
+ return this.completed;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Note: Can only use after calling {@link #addAsynchronousWaiter(BiConsumer)}, as this function performs zero
|
|
+ * synchronisation
|
|
+ */
|
|
+ public T getResult() {
|
|
+ return this.result;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Note: Can only use after calling {@link #addAsynchronousWaiter(BiConsumer)}, as this function performs zero
|
|
+ * synchronisation
|
|
+ */
|
|
+ public Throwable getThrowable() {
|
|
+ return this.throwable;
|
|
+ }
|
|
+
|
|
+ public Cancellable addAsynchronousWaiter(final BiConsumer<T, Throwable> consumer) {
|
|
+ if (this.waiters.add(consumer)) {
|
|
+ return new CancellableImpl(consumer);
|
|
+ }
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ private void completeAllWaiters(final T result, final Throwable throwable) {
|
|
+ this.completed = true;
|
|
+ BiConsumer<T, Throwable> waiter;
|
|
+ while ((waiter = this.waiters.pollOrBlockAdds()) != null) {
|
|
+ this.completeWaiter(waiter, result, throwable);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private void completeWaiter(final BiConsumer<T, Throwable> consumer, final T result, final Throwable throwable) {
|
|
+ try {
|
|
+ consumer.accept(result, throwable);
|
|
+ } catch (final ThreadDeath death) {
|
|
+ throw death;
|
|
+ } catch (final Throwable throwable2) {
|
|
+ LOGGER.error("Failed to complete callback " + ConcurrentUtil.genericToString(consumer), throwable2);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public Cancellable addWaiter(final BiConsumer<T, Throwable> consumer) {
|
|
+ if (this.waiters.add(consumer)) {
|
|
+ return new CancellableImpl(consumer);
|
|
+ }
|
|
+ this.completeWaiter(consumer, this.result, this.throwable);
|
|
+ return new CancellableImpl(consumer);
|
|
+ }
|
|
+
|
|
+ public void complete(final T result) {
|
|
+ this.result = result;
|
|
+ this.completeAllWaiters(result, null);
|
|
+ }
|
|
+
|
|
+ public void completeWithThrowable(final Throwable throwable) {
|
|
+ if (throwable == null) {
|
|
+ throw new NullPointerException("Throwable cannot be null");
|
|
+ }
|
|
+ this.throwable = throwable;
|
|
+ this.completeAllWaiters(null, throwable);
|
|
+ }
|
|
+
|
|
+ private final class CancellableImpl implements Cancellable {
|
|
+
|
|
+ private final BiConsumer<T, Throwable> waiter;
|
|
+
|
|
+ private CancellableImpl(final BiConsumer<T, Throwable> waiter) {
|
|
+ this.waiter = waiter;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean cancel() {
|
|
+ return Completable.this.waiters.remove(this.waiter);
|
|
+ }
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/BaseExecutor.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/BaseExecutor.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..8c452b0988da4725762d543f6bee09915c328ae6
|
|
--- /dev/null
|
|
+++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/BaseExecutor.java
|
|
@@ -0,0 +1,198 @@
|
|
+package ca.spottedleaf.concurrentutil.executor;
|
|
+
|
|
+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
|
|
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
|
|
+import java.util.function.BooleanSupplier;
|
|
+
|
|
+public interface BaseExecutor {
|
|
+
|
|
+ /**
|
|
+ * Returns whether every task scheduled to this queue has been removed and executed or cancelled. If no tasks have been queued,
|
|
+ * returns {@code true}.
|
|
+ *
|
|
+ * @return {@code true} if all tasks that have been queued have finished executing or no tasks have been queued, {@code false} otherwise.
|
|
+ */
|
|
+ public default boolean haveAllTasksExecuted() {
|
|
+ // order is important
|
|
+ // if new tasks are scheduled between the reading of these variables, scheduled is guaranteed to be higher -
|
|
+ // so our check fails, and we try again
|
|
+ final long completed = this.getTotalTasksExecuted();
|
|
+ final long scheduled = this.getTotalTasksScheduled();
|
|
+
|
|
+ return completed == scheduled;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Returns the number of tasks that have been scheduled or execute or are pending to be scheduled.
|
|
+ */
|
|
+ public long getTotalTasksScheduled();
|
|
+
|
|
+ /**
|
|
+ * Returns the number of tasks that have fully been executed.
|
|
+ */
|
|
+ public long getTotalTasksExecuted();
|
|
+
|
|
+
|
|
+ /**
|
|
+ * Waits until this queue has had all of its tasks executed (NOT removed). See {@link #haveAllTasksExecuted()}
|
|
+ * <p>
|
|
+ * This call is most effective after a {@link #shutdown()} call, as the shutdown call guarantees no tasks can
|
|
+ * be executed and the waitUntilAllExecuted call makes sure the queue is empty. Effectively, using shutdown then using
|
|
+ * waitUntilAllExecuted ensures this queue is empty - and most importantly, will remain empty.
|
|
+ * </p>
|
|
+ * <p>
|
|
+ * This method is not guaranteed to be immediately responsive to queue state, so calls may take significantly more
|
|
+ * time than expected. Effectively, do not rely on this call being fast - even if there are few tasks scheduled.
|
|
+ * </p>
|
|
+ * <p>
|
|
+ * Note: Interruptions to the the current thread have no effect. Interrupt status is also not affected by this cal.
|
|
+ * </p>
|
|
+ *
|
|
+ * @throws IllegalStateException If the current thread is not allowed to wait
|
|
+ */
|
|
+ public default void waitUntilAllExecuted() throws IllegalStateException {
|
|
+ long failures = 1L; // start at 0.25ms
|
|
+
|
|
+ while (!this.haveAllTasksExecuted()) {
|
|
+ Thread.yield();
|
|
+ failures = ConcurrentUtil.linearLongBackoff(failures, 250_000L, 5_000_000L); // 500us, 5ms
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Executes the next available task.
|
|
+ * <p>
|
|
+ * If there is a task with priority {@link PrioritisedExecutor.Priority#BLOCKING} available, then that such task is executed.
|
|
+ * </p>
|
|
+ * <p>
|
|
+ * If there is a task with priority {@link PrioritisedExecutor.Priority#IDLE} available then that task is only executed
|
|
+ * when there are no other tasks available with a higher priority.
|
|
+ * </p>
|
|
+ * <p>
|
|
+ * If there are no tasks that have priority {@link PrioritisedExecutor.Priority#BLOCKING} or {@link PrioritisedExecutor.Priority#IDLE}, then
|
|
+ * this function will be biased to execute tasks that have higher priorities.
|
|
+ * </p>
|
|
+ *
|
|
+ * @return {@code true} if a task was executed, {@code false} otherwise
|
|
+ * @throws IllegalStateException If the current thread is not allowed to execute a task
|
|
+ */
|
|
+ public boolean executeTask() throws IllegalStateException;
|
|
+
|
|
+ /**
|
|
+ * Executes all queued tasks.
|
|
+ *
|
|
+ * @return {@code true} if a task was executed, {@code false} otherwise
|
|
+ * @throws IllegalStateException If the current thread is not allowed to execute a task
|
|
+ */
|
|
+ public default boolean executeAll() {
|
|
+ if (!this.executeTask()) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ while (this.executeTask());
|
|
+
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Waits and executes tasks until the condition returns {@code true}.
|
|
+ * <p>
|
|
+ * WARNING: This function is <i>not</i> suitable for waiting until a deadline!
|
|
+ * Use {@link #executeUntil(long)} or {@link #executeConditionally(BooleanSupplier, long)} instead.
|
|
+ * </p>
|
|
+ */
|
|
+ public default void executeConditionally(final BooleanSupplier condition) {
|
|
+ long failures = 0;
|
|
+ while (!condition.getAsBoolean()) {
|
|
+ if (this.executeTask()) {
|
|
+ failures = failures >>> 2;
|
|
+ } else {
|
|
+ failures = ConcurrentUtil.linearLongBackoff(failures, 100_000L, 10_000_000L); // 100us, 10ms
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Waits and executes tasks until the condition returns {@code true} or {@code System.nanoTime() >= deadline}.
|
|
+ */
|
|
+ public default void executeConditionally(final BooleanSupplier condition, final long deadline) {
|
|
+ long failures = 0;
|
|
+ // double check deadline; we don't know how expensive the condition is
|
|
+ while ((System.nanoTime() < deadline) && !condition.getAsBoolean() && (System.nanoTime() < deadline)) {
|
|
+ if (this.executeTask()) {
|
|
+ failures = failures >>> 2;
|
|
+ } else {
|
|
+ failures = ConcurrentUtil.linearLongBackoffDeadline(failures, 100_000L, 10_000_000L, deadline); // 100us, 10ms
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Waits and executes tasks until {@code System.nanoTime() >= deadline}.
|
|
+ */
|
|
+ public default void executeUntil(final long deadline) {
|
|
+ long failures = 0;
|
|
+ while (System.nanoTime() < deadline) {
|
|
+ if (this.executeTask()) {
|
|
+ failures = failures >>> 2;
|
|
+ } else {
|
|
+ failures = ConcurrentUtil.linearLongBackoffDeadline(failures, 100_000L, 10_000_000L, deadline); // 100us, 10ms
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Prevent further additions to this queue. Attempts to add after this call has completed (potentially during) will
|
|
+ * result in {@link IllegalStateException} being thrown.
|
|
+ * <p>
|
|
+ * This operation is atomic with respect to other shutdown calls
|
|
+ * </p>
|
|
+ * <p>
|
|
+ * After this call has completed, regardless of return value, this queue will be shutdown.
|
|
+ * </p>
|
|
+ *
|
|
+ * @return {@code true} if the queue was shutdown, {@code false} if it has shut down already
|
|
+ * @throws UnsupportedOperationException If this queue does not support shutdown
|
|
+ */
|
|
+ public default boolean shutdown() throws UnsupportedOperationException {
|
|
+ throw new UnsupportedOperationException();
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Returns whether this queue has shut down. Effectively, whether new tasks will be rejected - this method
|
|
+ * does not indicate whether all of the tasks scheduled have been executed.
|
|
+ * @return Returns whether this queue has shut down.
|
|
+ */
|
|
+ public default boolean isShutdown() {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ public static interface BaseTask extends Cancellable {
|
|
+
|
|
+ /**
|
|
+ * Causes a lazily queued task to become queued or executed
|
|
+ *
|
|
+ * @throws IllegalStateException If the backing queue has shutdown
|
|
+ * @return {@code true} If the task was queued, {@code false} if the task was already queued/cancelled/executed
|
|
+ */
|
|
+ public boolean queue();
|
|
+
|
|
+ /**
|
|
+ * Forces this task to be marked as completed.
|
|
+ *
|
|
+ * @return {@code true} if the task was cancelled, {@code false} if the task has already completed or is being completed.
|
|
+ */
|
|
+ @Override
|
|
+ public boolean cancel();
|
|
+
|
|
+ /**
|
|
+ * Executes this task. This will also mark the task as completing.
|
|
+ * <p>
|
|
+ * Exceptions thrown from the runnable will be rethrown.
|
|
+ * </p>
|
|
+ *
|
|
+ * @return {@code true} if this task was executed, {@code false} if it was already marked as completed.
|
|
+ */
|
|
+ public boolean execute();
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/Cancellable.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/Cancellable.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..11449056361bb6c5a055f543cdd135c4113757c6
|
|
--- /dev/null
|
|
+++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/Cancellable.java
|
|
@@ -0,0 +1,14 @@
|
|
+package ca.spottedleaf.concurrentutil.executor;
|
|
+
|
|
+/**
|
|
+ * Interface specifying that something can be cancelled.
|
|
+ */
|
|
+public interface Cancellable {
|
|
+
|
|
+ /**
|
|
+ * Tries to cancel this task. If the task is in a stage that is too late to be cancelled, then this function
|
|
+ * will return {@code false}. If the task is already cancelled, then this function returns {@code false}. Only
|
|
+ * when this function successfully stops this task from being completed will it return {@code true}.
|
|
+ */
|
|
+ public boolean cancel();
|
|
+}
|
|
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/DelayedPrioritisedTask.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/DelayedPrioritisedTask.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..3ce10053d4ec51855ad7012abb5d97df1c0e557a
|
|
--- /dev/null
|
|
+++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/DelayedPrioritisedTask.java
|
|
@@ -0,0 +1,170 @@
|
|
+package ca.spottedleaf.concurrentutil.executor.standard;
|
|
+
|
|
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
|
|
+import java.lang.invoke.VarHandle;
|
|
+
|
|
+public class DelayedPrioritisedTask {
|
|
+
|
|
+ protected volatile int priority;
|
|
+ protected static final VarHandle PRIORITY_HANDLE = ConcurrentUtil.getVarHandle(DelayedPrioritisedTask.class, "priority", int.class);
|
|
+
|
|
+ protected static final int PRIORITY_SET = Integer.MIN_VALUE >>> 0;
|
|
+
|
|
+ protected final int getPriorityVolatile() {
|
|
+ return (int)PRIORITY_HANDLE.getVolatile((DelayedPrioritisedTask)this);
|
|
+ }
|
|
+
|
|
+ protected final int compareAndExchangePriorityVolatile(final int expect, final int update) {
|
|
+ return (int)PRIORITY_HANDLE.compareAndExchange((DelayedPrioritisedTask)this, (int)expect, (int)update);
|
|
+ }
|
|
+
|
|
+ protected final int getAndOrPriorityVolatile(final int val) {
|
|
+ return (int)PRIORITY_HANDLE.getAndBitwiseOr((DelayedPrioritisedTask)this, (int)val);
|
|
+ }
|
|
+
|
|
+ protected final void setPriorityPlain(final int val) {
|
|
+ PRIORITY_HANDLE.set((DelayedPrioritisedTask)this, (int)val);
|
|
+ }
|
|
+
|
|
+ protected volatile PrioritisedExecutor.PrioritisedTask task;
|
|
+ protected static final VarHandle TASK_HANDLE = ConcurrentUtil.getVarHandle(DelayedPrioritisedTask.class, "task", PrioritisedExecutor.PrioritisedTask.class);
|
|
+
|
|
+ protected PrioritisedExecutor.PrioritisedTask getTaskPlain() {
|
|
+ return (PrioritisedExecutor.PrioritisedTask)TASK_HANDLE.get((DelayedPrioritisedTask)this);
|
|
+ }
|
|
+
|
|
+ protected PrioritisedExecutor.PrioritisedTask getTaskVolatile() {
|
|
+ return (PrioritisedExecutor.PrioritisedTask)TASK_HANDLE.getVolatile((DelayedPrioritisedTask)this);
|
|
+ }
|
|
+
|
|
+ protected final PrioritisedExecutor.PrioritisedTask compareAndExchangeTaskVolatile(final PrioritisedExecutor.PrioritisedTask expect, final PrioritisedExecutor.PrioritisedTask update) {
|
|
+ return (PrioritisedExecutor.PrioritisedTask)TASK_HANDLE.compareAndExchange((DelayedPrioritisedTask)this, (PrioritisedExecutor.PrioritisedTask)expect, (PrioritisedExecutor.PrioritisedTask)update);
|
|
+ }
|
|
+
|
|
+ public DelayedPrioritisedTask(final PrioritisedExecutor.Priority priority) {
|
|
+ this.setPriorityPlain(priority.priority);
|
|
+ }
|
|
+
|
|
+ // only public for debugging
|
|
+ public int getPriorityInternal() {
|
|
+ return this.getPriorityVolatile();
|
|
+ }
|
|
+
|
|
+ public PrioritisedExecutor.PrioritisedTask getTask() {
|
|
+ return this.getTaskVolatile();
|
|
+ }
|
|
+
|
|
+ public void setTask(final PrioritisedExecutor.PrioritisedTask task) {
|
|
+ int priority = this.getPriorityVolatile();
|
|
+
|
|
+ if (this.compareAndExchangeTaskVolatile(null, task) != null) {
|
|
+ throw new IllegalStateException("setTask() called twice");
|
|
+ }
|
|
+
|
|
+ int failures = 0;
|
|
+ for (;;) {
|
|
+ task.setPriority(PrioritisedExecutor.Priority.getPriority(priority));
|
|
+
|
|
+ if (priority == (priority = this.compareAndExchangePriorityVolatile(priority, priority | PRIORITY_SET))) {
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ ++failures;
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public PrioritisedExecutor.Priority getPriority() {
|
|
+ final int priority = this.getPriorityVolatile();
|
|
+ if ((priority & PRIORITY_SET) != 0) {
|
|
+ return this.task.getPriority();
|
|
+ }
|
|
+
|
|
+ return PrioritisedExecutor.Priority.getPriority(priority);
|
|
+ }
|
|
+
|
|
+ public void raisePriority(final PrioritisedExecutor.Priority priority) {
|
|
+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
|
|
+ throw new IllegalArgumentException("Invalid priority " + priority);
|
|
+ }
|
|
+
|
|
+ int failures = 0;
|
|
+ for (int curr = this.getPriorityVolatile();;) {
|
|
+ if ((curr & PRIORITY_SET) != 0) {
|
|
+ this.getTaskPlain().raisePriority(priority);
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ if (!priority.isLowerPriority(curr)) {
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) {
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ // failed, retry
|
|
+
|
|
+ ++failures;
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public void setPriority(final PrioritisedExecutor.Priority priority) {
|
|
+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
|
|
+ throw new IllegalArgumentException("Invalid priority " + priority);
|
|
+ }
|
|
+
|
|
+ int failures = 0;
|
|
+ for (int curr = this.getPriorityVolatile();;) {
|
|
+ if ((curr & PRIORITY_SET) != 0) {
|
|
+ this.getTaskPlain().setPriority(priority);
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) {
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ // failed, retry
|
|
+
|
|
+ ++failures;
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public void lowerPriority(final PrioritisedExecutor.Priority priority) {
|
|
+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
|
|
+ throw new IllegalArgumentException("Invalid priority " + priority);
|
|
+ }
|
|
+
|
|
+ int failures = 0;
|
|
+ for (int curr = this.getPriorityVolatile();;) {
|
|
+ if ((curr & PRIORITY_SET) != 0) {
|
|
+ this.getTaskPlain().lowerPriority(priority);
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ if (!priority.isHigherPriority(curr)) {
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) {
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ // failed, retry
|
|
+
|
|
+ ++failures;
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedExecutor.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedExecutor.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..e5d8ff730ba9d83efc2d80782de313a718bf55b3
|
|
--- /dev/null
|
|
+++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedExecutor.java
|
|
@@ -0,0 +1,246 @@
|
|
+package ca.spottedleaf.concurrentutil.executor.standard;
|
|
+
|
|
+import ca.spottedleaf.concurrentutil.executor.BaseExecutor;
|
|
+
|
|
+public interface PrioritisedExecutor extends BaseExecutor {
|
|
+
|
|
+ public static enum Priority {
|
|
+
|
|
+ /**
|
|
+ * Priority value indicating the task has completed or is being completed.
|
|
+ * This priority cannot be used to schedule tasks.
|
|
+ */
|
|
+ COMPLETING(-1),
|
|
+
|
|
+ /**
|
|
+ * Absolute highest priority, should only be used for when a task is blocking a time-critical thread.
|
|
+ */
|
|
+ BLOCKING(),
|
|
+
|
|
+ /**
|
|
+ * Should only be used for urgent but not time-critical tasks.
|
|
+ */
|
|
+ HIGHEST(),
|
|
+
|
|
+ /**
|
|
+ * Two priorities above normal.
|
|
+ */
|
|
+ HIGHER(),
|
|
+
|
|
+ /**
|
|
+ * One priority above normal.
|
|
+ */
|
|
+ HIGH(),
|
|
+
|
|
+ /**
|
|
+ * Default priority.
|
|
+ */
|
|
+ NORMAL(),
|
|
+
|
|
+ /**
|
|
+ * One priority below normal.
|
|
+ */
|
|
+ LOW(),
|
|
+
|
|
+ /**
|
|
+ * Two priorities below normal.
|
|
+ */
|
|
+ LOWER(),
|
|
+
|
|
+ /**
|
|
+ * Use for tasks that should eventually execute, but are not needed to.
|
|
+ */
|
|
+ LOWEST(),
|
|
+
|
|
+ /**
|
|
+ * Use for tasks that can be delayed indefinitely.
|
|
+ */
|
|
+ IDLE();
|
|
+
|
|
+ // returns whether the priority can be scheduled
|
|
+ public static boolean isValidPriority(final Priority priority) {
|
|
+ return priority != null && priority != Priority.COMPLETING;
|
|
+ }
|
|
+
|
|
+ // returns the higher priority of the two
|
|
+ public static PrioritisedExecutor.Priority max(final Priority p1, final Priority p2) {
|
|
+ return p1.isHigherOrEqualPriority(p2) ? p1 : p2;
|
|
+ }
|
|
+
|
|
+ // returns the lower priroity of the two
|
|
+ public static PrioritisedExecutor.Priority min(final Priority p1, final Priority p2) {
|
|
+ return p1.isLowerOrEqualPriority(p2) ? p1 : p2;
|
|
+ }
|
|
+
|
|
+ public boolean isHigherOrEqualPriority(final Priority than) {
|
|
+ return this.priority <= than.priority;
|
|
+ }
|
|
+
|
|
+ public boolean isHigherPriority(final Priority than) {
|
|
+ return this.priority < than.priority;
|
|
+ }
|
|
+
|
|
+ public boolean isLowerOrEqualPriority(final Priority than) {
|
|
+ return this.priority >= than.priority;
|
|
+ }
|
|
+
|
|
+ public boolean isLowerPriority(final Priority than) {
|
|
+ return this.priority > than.priority;
|
|
+ }
|
|
+
|
|
+ public boolean isHigherOrEqualPriority(final int than) {
|
|
+ return this.priority <= than;
|
|
+ }
|
|
+
|
|
+ public boolean isHigherPriority(final int than) {
|
|
+ return this.priority < than;
|
|
+ }
|
|
+
|
|
+ public boolean isLowerOrEqualPriority(final int than) {
|
|
+ return this.priority >= than;
|
|
+ }
|
|
+
|
|
+ public boolean isLowerPriority(final int than) {
|
|
+ return this.priority > than;
|
|
+ }
|
|
+
|
|
+ public static boolean isHigherOrEqualPriority(final int priority, final int than) {
|
|
+ return priority <= than;
|
|
+ }
|
|
+
|
|
+ public static boolean isHigherPriority(final int priority, final int than) {
|
|
+ return priority < than;
|
|
+ }
|
|
+
|
|
+ public static boolean isLowerOrEqualPriority(final int priority, final int than) {
|
|
+ return priority >= than;
|
|
+ }
|
|
+
|
|
+ public static boolean isLowerPriority(final int priority, final int than) {
|
|
+ return priority > than;
|
|
+ }
|
|
+
|
|
+ static final PrioritisedExecutor.Priority[] PRIORITIES = PrioritisedExecutor.Priority.values();
|
|
+
|
|
+ /** includes special priorities */
|
|
+ public static final int TOTAL_PRIORITIES = PRIORITIES.length;
|
|
+
|
|
+ public static final int TOTAL_SCHEDULABLE_PRIORITIES = TOTAL_PRIORITIES - 1;
|
|
+
|
|
+ public static PrioritisedExecutor.Priority getPriority(final int priority) {
|
|
+ return PRIORITIES[priority + 1];
|
|
+ }
|
|
+
|
|
+ private static int priorityCounter;
|
|
+
|
|
+ private static int nextCounter() {
|
|
+ return priorityCounter++;
|
|
+ }
|
|
+
|
|
+ public final int priority;
|
|
+
|
|
+ Priority() {
|
|
+ this(nextCounter());
|
|
+ }
|
|
+
|
|
+ Priority(final int priority) {
|
|
+ this.priority = priority;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Queues or executes a task at {@link Priority#NORMAL} priority.
|
|
+ * @param task The task to run.
|
|
+ *
|
|
+ * @throws IllegalStateException If this queue has shutdown.
|
|
+ * @throws NullPointerException If the task is null
|
|
+ * @return {@code null} if the current thread immediately executed the task, else returns the prioritised task
|
|
+ * associated with the parameter
|
|
+ */
|
|
+ public default PrioritisedTask queueRunnable(final Runnable task) {
|
|
+ return this.queueRunnable(task, PrioritisedExecutor.Priority.NORMAL);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Queues or executes a task.
|
|
+ *
|
|
+ * @param task The task to run.
|
|
+ * @param priority The priority for the task.
|
|
+ *
|
|
+ * @throws IllegalStateException If this queue has shutdown.
|
|
+ * @throws NullPointerException If the task is null
|
|
+ * @throws IllegalArgumentException If the priority is invalid.
|
|
+ * @return {@code null} if the current thread immediately executed the task, else returns the prioritised task
|
|
+ * associated with the parameter
|
|
+ */
|
|
+ public PrioritisedTask queueRunnable(final Runnable task, final PrioritisedExecutor.Priority priority);
|
|
+
|
|
+ /**
|
|
+ * Creates, but does not execute or queue the task. The task must later be queued via {@link BaseExecutor.BaseTask#queue()}.
|
|
+ *
|
|
+ * @param task The task to run.
|
|
+ *
|
|
+ * @throws IllegalStateException If this queue has shutdown.
|
|
+ * @throws NullPointerException If the task is null
|
|
+ * @throws IllegalArgumentException If the priority is invalid.
|
|
+ * @throws UnsupportedOperationException If this executor does not support lazily queueing tasks
|
|
+ * @return The prioritised task associated with the parameters
|
|
+ */
|
|
+ public default PrioritisedExecutor.PrioritisedTask createTask(final Runnable task) {
|
|
+ return this.createTask(task, PrioritisedExecutor.Priority.NORMAL);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Creates, but does not execute or queue the task. The task must later be queued via {@link BaseExecutor.BaseTask#queue()}.
|
|
+ *
|
|
+ * @param task The task to run.
|
|
+ * @param priority The priority for the task.
|
|
+ *
|
|
+ * @throws IllegalStateException If this queue has shutdown.
|
|
+ * @throws NullPointerException If the task is null
|
|
+ * @throws IllegalArgumentException If the priority is invalid.
|
|
+ * @throws UnsupportedOperationException If this executor does not support lazily queueing tasks
|
|
+ * @return The prioritised task associated with the parameters
|
|
+ */
|
|
+ public PrioritisedExecutor.PrioritisedTask createTask(final Runnable task, final PrioritisedExecutor.Priority priority);
|
|
+
|
|
+ public static interface PrioritisedTask extends BaseTask {
|
|
+
|
|
+ /**
|
|
+ * Returns the current priority. Note that {@link PrioritisedExecutor.Priority#COMPLETING} will be returned
|
|
+ * if this task is completing or has completed.
|
|
+ */
|
|
+ public PrioritisedExecutor.Priority getPriority();
|
|
+
|
|
+ /**
|
|
+ * Attempts to set this task's priority level to the level specified.
|
|
+ *
|
|
+ * @param priority Specified priority level.
|
|
+ *
|
|
+ * @throws IllegalArgumentException If the priority is invalid
|
|
+ * @return {@code true} if successful, {@code false} if this task is completing or has completed or the queue
|
|
+ * this task was scheduled on was shutdown, or if the priority was already at the specified level.
|
|
+ */
|
|
+ public boolean setPriority(final PrioritisedExecutor.Priority priority);
|
|
+
|
|
+ /**
|
|
+ * Attempts to raise the priority to the priority level specified.
|
|
+ *
|
|
+ * @param priority Priority specified
|
|
+ *
|
|
+ * @throws IllegalArgumentException If the priority is invalid
|
|
+ * @return {@code false} if the current task is completing, {@code true} if the priority was raised to the specified level or was already at the specified level or higher.
|
|
+ */
|
|
+ public boolean raisePriority(final PrioritisedExecutor.Priority priority);
|
|
+
|
|
+ /**
|
|
+ * Attempts to lower the priority to the priority level specified.
|
|
+ *
|
|
+ * @param priority Priority specified
|
|
+ *
|
|
+ * @throws IllegalArgumentException If the priority is invalid
|
|
+ * @return {@code false} if the current task is completing, {@code true} if the priority was lowered to the specified level or was already at the specified level or lower.
|
|
+ */
|
|
+ public boolean lowerPriority(final PrioritisedExecutor.Priority priority);
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedQueueExecutorThread.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedQueueExecutorThread.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..91fe0f7049122f62f05ba09c24cba5d758340cff
|
|
--- /dev/null
|
|
+++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedQueueExecutorThread.java
|
|
@@ -0,0 +1,297 @@
|
|
+package ca.spottedleaf.concurrentutil.executor.standard;
|
|
+
|
|
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
|
|
+import com.mojang.logging.LogUtils;
|
|
+import org.slf4j.Logger;
|
|
+import java.lang.invoke.VarHandle;
|
|
+import java.util.concurrent.locks.LockSupport;
|
|
+
|
|
+/**
|
|
+ * Thread which will continuously drain from a specified queue.
|
|
+ * <p>
|
|
+ * Note: When using this thread, queue additions to the underlying {@link #queue} are not sufficient to get this thread
|
|
+ * to execute the task. The function {@link #notifyTasks()} must be used after scheduling a task. For expected behaviour
|
|
+ * of task scheduling (thread wakes up after tasks are scheduled), use the methods provided on {@link PrioritisedExecutor}
|
|
+ * methods.
|
|
+ * </p>
|
|
+ */
|
|
+public class PrioritisedQueueExecutorThread extends Thread implements PrioritisedExecutor {
|
|
+
|
|
+ private static final Logger LOGGER = LogUtils.getLogger();
|
|
+
|
|
+ protected final PrioritisedExecutor queue;
|
|
+
|
|
+ protected volatile boolean threadShutdown;
|
|
+
|
|
+ protected static final VarHandle THREAD_PARKED_HANDLE = ConcurrentUtil.getVarHandle(PrioritisedQueueExecutorThread.class, "threadParked", boolean.class);
|
|
+ protected volatile boolean threadParked;
|
|
+
|
|
+ protected volatile boolean halted;
|
|
+
|
|
+ protected final long spinWaitTime;
|
|
+
|
|
+ static final long DEFAULT_SPINWAIT_TIME = (long)(0.1e6);// 0.1ms
|
|
+
|
|
+ public PrioritisedQueueExecutorThread(final PrioritisedExecutor queue) {
|
|
+ this(queue, DEFAULT_SPINWAIT_TIME); // 0.1ms
|
|
+ }
|
|
+
|
|
+ public PrioritisedQueueExecutorThread(final PrioritisedExecutor queue, final long spinWaitTime) { // in ns
|
|
+ this.queue = queue;
|
|
+ this.spinWaitTime = spinWaitTime;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void run() {
|
|
+ final long spinWaitTime = this.spinWaitTime;
|
|
+
|
|
+ main_loop:
|
|
+ for (;;) {
|
|
+ this.pollTasks();
|
|
+
|
|
+ // spinwait
|
|
+
|
|
+ final long start = System.nanoTime();
|
|
+
|
|
+ for (;;) {
|
|
+ // If we are interrupted for any reason, park() will always return immediately. Clear so that we don't needlessly use cpu in such an event.
|
|
+ Thread.interrupted();
|
|
+ Thread.yield();
|
|
+ LockSupport.parkNanos("Spinwaiting on tasks", 10_000L); // 10us
|
|
+
|
|
+ if (this.pollTasks()) {
|
|
+ // restart loop, found tasks
|
|
+ continue main_loop;
|
|
+ }
|
|
+
|
|
+ if (this.handleClose()) {
|
|
+ return; // we're done
|
|
+ }
|
|
+
|
|
+ if ((System.nanoTime() - start) >= spinWaitTime) {
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (this.handleClose()) {
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ this.setThreadParkedVolatile(true);
|
|
+
|
|
+ // We need to parse here to avoid a race condition where a thread queues a task before we set parked to true
|
|
+ // (i.e it will not notify us)
|
|
+ if (this.pollTasks()) {
|
|
+ this.setThreadParkedVolatile(false);
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ if (this.handleClose()) {
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ // we don't need to check parked before sleeping, but we do need to check parked in a do-while loop
|
|
+ // LockSupport.park() can fail for any reason
|
|
+ while (this.getThreadParkedVolatile()) {
|
|
+ Thread.interrupted();
|
|
+ LockSupport.park("Waiting on tasks");
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ protected boolean pollTasks() {
|
|
+ boolean ret = false;
|
|
+
|
|
+ for (;;) {
|
|
+ if (this.halted) {
|
|
+ break;
|
|
+ }
|
|
+ try {
|
|
+ if (!this.queue.executeTask()) {
|
|
+ break;
|
|
+ }
|
|
+ ret = true;
|
|
+ } catch (final ThreadDeath death) {
|
|
+ throw death; // goodbye world...
|
|
+ } catch (final Throwable throwable) {
|
|
+ LOGGER.error("Exception thrown from prioritized runnable task in thread '" + this.getName() + "'", throwable);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ protected boolean handleClose() {
|
|
+ if (this.threadShutdown) {
|
|
+ this.pollTasks(); // this ensures we've emptied the queue
|
|
+ return true;
|
|
+ }
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Notify this thread that a task has been added to its queue
|
|
+ * @return {@code true} if this thread was waiting for tasks, {@code false} if it is executing tasks
|
|
+ */
|
|
+ public boolean notifyTasks() {
|
|
+ if (this.getThreadParkedVolatile() && this.exchangeThreadParkedVolatile(false)) {
|
|
+ LockSupport.unpark(this);
|
|
+ return true;
|
|
+ }
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public PrioritisedTask createTask(final Runnable task, final Priority priority) {
|
|
+ final PrioritisedExecutor.PrioritisedTask queueTask = this.queue.createTask(task, priority);
|
|
+
|
|
+ // need to override queue() to notify us of tasks
|
|
+ return new PrioritisedTask() {
|
|
+ @Override
|
|
+ public Priority getPriority() {
|
|
+ return queueTask.getPriority();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean setPriority(final Priority priority) {
|
|
+ return queueTask.setPriority(priority);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean raisePriority(final Priority priority) {
|
|
+ return queueTask.raisePriority(priority);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean lowerPriority(final Priority priority) {
|
|
+ return queueTask.lowerPriority(priority);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean queue() {
|
|
+ final boolean ret = queueTask.queue();
|
|
+ if (ret) {
|
|
+ PrioritisedQueueExecutorThread.this.notifyTasks();
|
|
+ }
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean cancel() {
|
|
+ return queueTask.cancel();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean execute() {
|
|
+ return queueTask.execute();
|
|
+ }
|
|
+ };
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public PrioritisedExecutor.PrioritisedTask queueRunnable(final Runnable task, final PrioritisedExecutor.Priority priority) {
|
|
+ final PrioritisedExecutor.PrioritisedTask ret = this.queue.queueRunnable(task, priority);
|
|
+
|
|
+ this.notifyTasks();
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean haveAllTasksExecuted() {
|
|
+ return this.queue.haveAllTasksExecuted();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public long getTotalTasksExecuted() {
|
|
+ return this.queue.getTotalTasksExecuted();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public long getTotalTasksScheduled() {
|
|
+ return this.queue.getTotalTasksScheduled();
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ * @throws IllegalStateException If the current thread is {@code this} thread, or the underlying queue throws this exception.
|
|
+ */
|
|
+ @Override
|
|
+ public void waitUntilAllExecuted() throws IllegalStateException {
|
|
+ if (Thread.currentThread() == this) {
|
|
+ throw new IllegalStateException("Cannot block on our own queue");
|
|
+ }
|
|
+ this.queue.waitUntilAllExecuted();
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ * @throws IllegalStateException Always
|
|
+ */
|
|
+ @Override
|
|
+ public boolean executeTask() throws IllegalStateException {
|
|
+ throw new IllegalStateException();
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Closes this queue executor's queue. Optionally waits for all tasks in queue to be executed if {@code wait} is true.
|
|
+ * <p>
|
|
+ * This function is MT-Safe.
|
|
+ * </p>
|
|
+ * @param wait If this call is to wait until the queue is empty and there are no tasks executing in the queue.
|
|
+ * @param killQueue Whether to shutdown this thread's queue
|
|
+ * @return whether this thread shut down the queue
|
|
+ * @see #halt(boolean)
|
|
+ */
|
|
+ public boolean close(final boolean wait, final boolean killQueue) {
|
|
+ final boolean ret = killQueue && this.queue.shutdown();
|
|
+ this.threadShutdown = true;
|
|
+
|
|
+ // force thread to respond to the shutdown
|
|
+ this.setThreadParkedVolatile(false);
|
|
+ LockSupport.unpark(this);
|
|
+
|
|
+ if (wait) {
|
|
+ this.waitUntilAllExecuted();
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+
|
|
+ /**
|
|
+ * Causes this thread to exit without draining the queue. To ensure tasks are completed, use {@link #close(boolean, boolean)}.
|
|
+ * <p>
|
|
+ * This is not safe to call with {@link #close(boolean, boolean)} if <code>wait = true</code>, in which case
|
|
+ * the waiting thread may block indefinitely.
|
|
+ * </p>
|
|
+ * <p>
|
|
+ * This function is MT-Safe.
|
|
+ * </p>
|
|
+ * @param killQueue Whether to shutdown this thread's queue
|
|
+ * @see #close(boolean, boolean)
|
|
+ */
|
|
+ public void halt(final boolean killQueue) {
|
|
+ if (killQueue) {
|
|
+ this.queue.shutdown();
|
|
+ }
|
|
+ this.threadShutdown = true;
|
|
+ this.halted = true;
|
|
+
|
|
+ // force thread to respond to the shutdown
|
|
+ this.setThreadParkedVolatile(false);
|
|
+ LockSupport.unpark(this);
|
|
+ }
|
|
+
|
|
+ protected final boolean getThreadParkedVolatile() {
|
|
+ return (boolean)THREAD_PARKED_HANDLE.getVolatile(this);
|
|
+ }
|
|
+
|
|
+ protected final boolean exchangeThreadParkedVolatile(final boolean value) {
|
|
+ return (boolean)THREAD_PARKED_HANDLE.getAndSet(this, value);
|
|
+ }
|
|
+
|
|
+ protected final void setThreadParkedVolatile(final boolean value) {
|
|
+ THREAD_PARKED_HANDLE.setVolatile(this, value);
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadPool.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadPool.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..26fa2caa18a9194e57574a4a7fa9f7a4265740e0
|
|
--- /dev/null
|
|
+++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadPool.java
|
|
@@ -0,0 +1,579 @@
|
|
+package ca.spottedleaf.concurrentutil.executor.standard;
|
|
+
|
|
+import com.mojang.logging.LogUtils;
|
|
+import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
|
|
+import org.slf4j.Logger;
|
|
+import java.util.ArrayList;
|
|
+import java.util.Arrays;
|
|
+import java.util.Comparator;
|
|
+import java.util.TreeSet;
|
|
+import java.util.concurrent.atomic.AtomicBoolean;
|
|
+import java.util.function.BiConsumer;
|
|
+
|
|
+public final class PrioritisedThreadPool {
|
|
+
|
|
+ private static final Logger LOGGER = LogUtils.getLogger();
|
|
+
|
|
+ protected final PrioritisedThread[] threads;
|
|
+ protected final TreeSet<PrioritisedPoolExecutorImpl> queues = new TreeSet<>(PrioritisedPoolExecutorImpl.comparator());
|
|
+ protected final String name;
|
|
+ protected final long queueMaxHoldTime;
|
|
+
|
|
+ protected final ReferenceOpenHashSet<PrioritisedPoolExecutorImpl> nonShutdownQueues = new ReferenceOpenHashSet<>();
|
|
+ protected final ReferenceOpenHashSet<PrioritisedPoolExecutorImpl> activeQueues = new ReferenceOpenHashSet<>();
|
|
+
|
|
+ protected boolean shutdown;
|
|
+
|
|
+ protected long schedulingIdGenerator;
|
|
+
|
|
+ protected static final long DEFAULT_QUEUE_HOLD_TIME = (long)(5.0e6);
|
|
+
|
|
+ public PrioritisedThreadPool(final String name, final int threads) {
|
|
+ this(name, threads, null);
|
|
+ }
|
|
+
|
|
+ public PrioritisedThreadPool(final String name, final int threads, final BiConsumer<Thread, Integer> threadModifier) {
|
|
+ this(name, threads, threadModifier, DEFAULT_QUEUE_HOLD_TIME); // 5ms
|
|
+ }
|
|
+
|
|
+ public PrioritisedThreadPool(final String name, final int threads, final BiConsumer<Thread, Integer> threadModifier,
|
|
+ final long queueHoldTime) { // in ns
|
|
+ if (threads <= 0) {
|
|
+ throw new IllegalArgumentException("Thread count must be > 0, not " + threads);
|
|
+ }
|
|
+ if (name == null) {
|
|
+ throw new IllegalArgumentException("Name cannot be null");
|
|
+ }
|
|
+ this.name = name;
|
|
+ this.queueMaxHoldTime = queueHoldTime;
|
|
+
|
|
+ this.threads = new PrioritisedThread[threads];
|
|
+ for (int i = 0; i < threads; ++i) {
|
|
+ this.threads[i] = new PrioritisedThread(this);
|
|
+
|
|
+ // set default attributes
|
|
+ this.threads[i].setName("Prioritised thread for pool '" + name + "' #" + i);
|
|
+ this.threads[i].setUncaughtExceptionHandler((final Thread thread, final Throwable throwable) -> {
|
|
+ LOGGER.error("Uncaught exception in thread " + thread.getName(), throwable);
|
|
+ });
|
|
+
|
|
+ // let thread modifier override defaults
|
|
+ if (threadModifier != null) {
|
|
+ threadModifier.accept(this.threads[i], Integer.valueOf(i));
|
|
+ }
|
|
+
|
|
+ // now the thread can start
|
|
+ this.threads[i].start();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public Thread[] getThreads() {
|
|
+ return Arrays.copyOf(this.threads, this.threads.length, Thread[].class);
|
|
+ }
|
|
+
|
|
+ public PrioritisedPoolExecutor createExecutor(final String name, final int parallelism) {
|
|
+ synchronized (this.nonShutdownQueues) {
|
|
+ if (this.shutdown) {
|
|
+ throw new IllegalStateException("Queue is shutdown: " + this.toString());
|
|
+ }
|
|
+ final PrioritisedPoolExecutorImpl ret = new PrioritisedPoolExecutorImpl(this, name, Math.min(Math.max(1, parallelism), this.threads.length));
|
|
+
|
|
+ this.nonShutdownQueues.add(ret);
|
|
+
|
|
+ synchronized (this.activeQueues) {
|
|
+ this.activeQueues.add(ret);
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Prevents creation of new queues, shutdowns all non-shutdown queues if specified
|
|
+ */
|
|
+ public void halt(final boolean shutdownQueues) {
|
|
+ synchronized (this.nonShutdownQueues) {
|
|
+ this.shutdown = true;
|
|
+ }
|
|
+ if (shutdownQueues) {
|
|
+ final ArrayList<PrioritisedPoolExecutorImpl> queuesToShutdown;
|
|
+ synchronized (this.nonShutdownQueues) {
|
|
+ this.shutdown = true;
|
|
+ queuesToShutdown = new ArrayList<>(this.nonShutdownQueues);
|
|
+ }
|
|
+
|
|
+ for (final PrioritisedPoolExecutorImpl queue : queuesToShutdown) {
|
|
+ queue.shutdown();
|
|
+ }
|
|
+ }
|
|
+
|
|
+
|
|
+ for (final PrioritisedThread thread : this.threads) {
|
|
+ // can't kill queue, queue is null
|
|
+ thread.halt(false);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Waits until all threads in this pool have shutdown, or until the specified time has passed.
|
|
+ * @param msToWait Maximum time to wait.
|
|
+ * @return {@code false} if the maximum time passed, {@code true} otherwise.
|
|
+ */
|
|
+ public boolean join(final long msToWait) {
|
|
+ try {
|
|
+ return this.join(msToWait, false);
|
|
+ } catch (final InterruptedException ex) {
|
|
+ throw new IllegalStateException(ex);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Waits until all threads in this pool have shutdown, or until the specified time has passed.
|
|
+ * @param msToWait Maximum time to wait.
|
|
+ * @return {@code false} if the maximum time passed, {@code true} otherwise.
|
|
+ * @throws InterruptedException If this thread is interrupted.
|
|
+ */
|
|
+ public boolean joinInterruptable(final long msToWait) throws InterruptedException {
|
|
+ return this.join(msToWait, true);
|
|
+ }
|
|
+
|
|
+ protected final boolean join(final long msToWait, final boolean interruptable) throws InterruptedException {
|
|
+ final long nsToWait = msToWait * (1000 * 1000);
|
|
+ final long start = System.nanoTime();
|
|
+ final long deadline = start + nsToWait;
|
|
+ boolean interrupted = false;
|
|
+ try {
|
|
+ for (final PrioritisedThread thread : this.threads) {
|
|
+ for (;;) {
|
|
+ if (!thread.isAlive()) {
|
|
+ break;
|
|
+ }
|
|
+ final long current = System.nanoTime();
|
|
+ if (current >= deadline) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ try {
|
|
+ thread.join(Math.max(1L, (deadline - current) / (1000 * 1000)));
|
|
+ } catch (final InterruptedException ex) {
|
|
+ if (interruptable) {
|
|
+ throw ex;
|
|
+ }
|
|
+ interrupted = true;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return true;
|
|
+ } finally {
|
|
+ if (interrupted) {
|
|
+ Thread.currentThread().interrupt();
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public void shutdown(final boolean wait) {
|
|
+ final ArrayList<PrioritisedPoolExecutorImpl> queuesToShutdown;
|
|
+ synchronized (this.nonShutdownQueues) {
|
|
+ this.shutdown = true;
|
|
+ queuesToShutdown = new ArrayList<>(this.nonShutdownQueues);
|
|
+ }
|
|
+
|
|
+ for (final PrioritisedPoolExecutorImpl queue : queuesToShutdown) {
|
|
+ queue.shutdown();
|
|
+ }
|
|
+
|
|
+ for (final PrioritisedThread thread : this.threads) {
|
|
+ // none of these can be true or else NPE
|
|
+ thread.close(false, false);
|
|
+ }
|
|
+
|
|
+ if (wait) {
|
|
+ final ArrayList<PrioritisedPoolExecutorImpl> queues;
|
|
+ synchronized (this.activeQueues) {
|
|
+ queues = new ArrayList<>(this.activeQueues);
|
|
+ }
|
|
+ for (final PrioritisedPoolExecutorImpl queue : queues) {
|
|
+ queue.waitUntilAllExecuted();
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ protected static final class PrioritisedThread extends PrioritisedQueueExecutorThread {
|
|
+
|
|
+ protected final PrioritisedThreadPool pool;
|
|
+ protected final AtomicBoolean alertedHighPriority = new AtomicBoolean();
|
|
+
|
|
+ public PrioritisedThread(final PrioritisedThreadPool pool) {
|
|
+ super(null);
|
|
+ this.pool = pool;
|
|
+ }
|
|
+
|
|
+ public boolean alertHighPriorityExecutor() {
|
|
+ if (!this.notifyTasks()) {
|
|
+ if (!this.alertedHighPriority.get()) {
|
|
+ this.alertedHighPriority.set(true);
|
|
+ }
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ private boolean isAlertedHighPriority() {
|
|
+ return this.alertedHighPriority.get() && this.alertedHighPriority.getAndSet(false);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ protected boolean pollTasks() {
|
|
+ final PrioritisedThreadPool pool = this.pool;
|
|
+ final TreeSet<PrioritisedPoolExecutorImpl> queues = this.pool.queues;
|
|
+
|
|
+ boolean ret = false;
|
|
+ for (;;) {
|
|
+ if (this.halted) {
|
|
+ break;
|
|
+ }
|
|
+ // try to find a queue
|
|
+ // note that if and ONLY IF the queues set is empty, this means there are no tasks for us to execute.
|
|
+ // so we can only break when it's empty
|
|
+ final PrioritisedPoolExecutorImpl queue;
|
|
+ // select queue
|
|
+ synchronized (queues) {
|
|
+ queue = queues.pollFirst();
|
|
+ if (queue == null) {
|
|
+ // no tasks to execute
|
|
+ break;
|
|
+ }
|
|
+
|
|
+ queue.schedulingId = ++pool.schedulingIdGenerator;
|
|
+ // we own this queue now, so increment the executor count
|
|
+ // do we also need to push this queue up for grabs for another executor?
|
|
+ if (++queue.concurrentExecutors < queue.maximumExecutors) {
|
|
+ // re-add to queues
|
|
+ // it's very important this is done in the same synchronised block for polling, as this prevents
|
|
+ // us from possibly later adding a queue that should not exist in the set
|
|
+ queues.add(queue);
|
|
+ queue.isQueued = true;
|
|
+ } else {
|
|
+ queue.isQueued = false;
|
|
+ }
|
|
+ // note: we cannot drain entries from the queue while holding this lock, as it will cause deadlock
|
|
+ // the queue addition holds the per-queue lock first then acquires the lock we have now, but if we
|
|
+ // try to poll now we don't hold the per queue lock but we do hold the global lock...
|
|
+ }
|
|
+
|
|
+ // parse tasks as long as we are allowed
|
|
+ final long start = System.nanoTime();
|
|
+ final long deadline = start + pool.queueMaxHoldTime;
|
|
+ do {
|
|
+ try {
|
|
+ if (this.halted) {
|
|
+ break;
|
|
+ }
|
|
+ if (!queue.executeTask()) {
|
|
+ // no more tasks, try next queue
|
|
+ break;
|
|
+ }
|
|
+ ret = true;
|
|
+ } catch (final ThreadDeath death) {
|
|
+ throw death; // goodbye world...
|
|
+ } catch (final Throwable throwable) {
|
|
+ LOGGER.error("Exception thrown from thread '" + this.getName() + "' in queue '" + queue.toString() + "'", throwable);
|
|
+ }
|
|
+ } while (!this.isAlertedHighPriority() && System.nanoTime() <= deadline);
|
|
+
|
|
+ synchronized (queues) {
|
|
+ // decrement executors, we are no longer executing
|
|
+ if (queue.isQueued) {
|
|
+ queues.remove(queue);
|
|
+ queue.isQueued = false;
|
|
+ }
|
|
+ if (--queue.concurrentExecutors == 0 && queue.scheduledPriority == null) {
|
|
+ // reset scheduling id once the queue is empty again
|
|
+ // this will ensure empty queues are not prioritised suddenly over active queues once tasks are
|
|
+ // queued
|
|
+ queue.schedulingId = 0L;
|
|
+ }
|
|
+
|
|
+ // ensure the executor is queued for execution again
|
|
+ if (!queue.isHalted && queue.scheduledPriority != null) { // make sure it actually has tasks
|
|
+ queues.add(queue);
|
|
+ queue.isQueued = true;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public interface PrioritisedPoolExecutor extends PrioritisedExecutor {
|
|
+
|
|
+ /**
|
|
+ * Removes this queue from the thread pool without shutting the queue down or waiting for queued tasks to be executed
|
|
+ */
|
|
+ public void halt();
|
|
+
|
|
+ /**
|
|
+ * Returns whether this executor is scheduled to run tasks or is running tasks, otherwise it returns whether
|
|
+ * this queue is not halted and not shutdown.
|
|
+ */
|
|
+ public boolean isActive();
|
|
+ }
|
|
+
|
|
+ protected static final class PrioritisedPoolExecutorImpl extends PrioritisedThreadedTaskQueue implements PrioritisedPoolExecutor {
|
|
+
|
|
+ protected final PrioritisedThreadPool pool;
|
|
+ protected final long[] priorityCounts = new long[Priority.TOTAL_SCHEDULABLE_PRIORITIES];
|
|
+ protected long schedulingId;
|
|
+ protected int concurrentExecutors;
|
|
+ protected Priority scheduledPriority;
|
|
+
|
|
+ protected final String name;
|
|
+ protected final int maximumExecutors;
|
|
+ protected boolean isQueued;
|
|
+
|
|
+ public PrioritisedPoolExecutorImpl(final PrioritisedThreadPool pool, final String name, final int maximumExecutors) {
|
|
+ this.pool = pool;
|
|
+ this.name = name;
|
|
+ this.maximumExecutors = maximumExecutors;
|
|
+ }
|
|
+
|
|
+ public static Comparator<PrioritisedPoolExecutorImpl> comparator() {
|
|
+ return (final PrioritisedPoolExecutorImpl p1, final PrioritisedPoolExecutorImpl p2) -> {
|
|
+ if (p1 == p2) {
|
|
+ return 0;
|
|
+ }
|
|
+
|
|
+ // prefer higher priority
|
|
+ final int priorityCompare = p1.scheduledPriority.ordinal() - p2.scheduledPriority.ordinal();
|
|
+ if (priorityCompare != 0) {
|
|
+ return priorityCompare;
|
|
+ }
|
|
+
|
|
+ // try to spread out the executors so that each can have threads executing
|
|
+ final int executorCompare = p1.concurrentExecutors - p2.concurrentExecutors;
|
|
+ if (executorCompare != 0) {
|
|
+ return executorCompare;
|
|
+ }
|
|
+
|
|
+ // if all else fails here we just choose whichever executor was queued first
|
|
+ return Long.compare(p1.schedulingId, p2.schedulingId);
|
|
+ };
|
|
+ }
|
|
+
|
|
+ private boolean isHalted;
|
|
+
|
|
+ @Override
|
|
+ public void halt() {
|
|
+ final PrioritisedThreadPool pool = this.pool;
|
|
+ final TreeSet<PrioritisedPoolExecutorImpl> queues = pool.queues;
|
|
+ synchronized (queues) {
|
|
+ if (this.isHalted) {
|
|
+ return;
|
|
+ }
|
|
+ this.isHalted = true;
|
|
+ if (this.isQueued) {
|
|
+ queues.remove(this);
|
|
+ this.isQueued = false;
|
|
+ }
|
|
+ }
|
|
+ synchronized (pool.nonShutdownQueues) {
|
|
+ pool.nonShutdownQueues.remove(this);
|
|
+ }
|
|
+ synchronized (pool.activeQueues) {
|
|
+ pool.activeQueues.remove(this);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean isActive() {
|
|
+ final PrioritisedThreadPool pool = this.pool;
|
|
+ final TreeSet<PrioritisedPoolExecutorImpl> queues = pool.queues;
|
|
+
|
|
+ synchronized (queues) {
|
|
+ if (this.concurrentExecutors != 0) {
|
|
+ return true;
|
|
+ }
|
|
+ synchronized (pool.activeQueues) {
|
|
+ if (pool.activeQueues.contains(this)) {
|
|
+ return true;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ private long totalQueuedTasks = 0L;
|
|
+
|
|
+ @Override
|
|
+ protected void priorityChange(final PrioritisedThreadedTaskQueue.PrioritisedTask task, final Priority from, final Priority to) {
|
|
+ // Note: The superclass' queue lock is ALWAYS held when inside this method. So we do NOT need to do any additional synchronisation
|
|
+ // for accessing this queue's state.
|
|
+ final long[] priorityCounts = this.priorityCounts;
|
|
+ final boolean shutdown = this.isShutdown();
|
|
+
|
|
+ if (from == null && to == Priority.COMPLETING) {
|
|
+ throw new IllegalStateException("Cannot complete task without queueing it first");
|
|
+ }
|
|
+
|
|
+ // we should only notify for queueing of tasks, not changing priorities
|
|
+ final boolean shouldNotifyTasks = from == null;
|
|
+
|
|
+ final Priority scheduledPriority = this.scheduledPriority;
|
|
+ if (from != null) {
|
|
+ --priorityCounts[from.priority];
|
|
+ }
|
|
+ if (to != Priority.COMPLETING) {
|
|
+ ++priorityCounts[to.priority];
|
|
+ }
|
|
+ final long totalQueuedTasks;
|
|
+ if (to == Priority.COMPLETING) {
|
|
+ totalQueuedTasks = --this.totalQueuedTasks;
|
|
+ } else if (from == null) {
|
|
+ totalQueuedTasks = ++this.totalQueuedTasks;
|
|
+ } else {
|
|
+ totalQueuedTasks = this.totalQueuedTasks;
|
|
+ }
|
|
+
|
|
+ // find new highest priority
|
|
+ int highest = Math.min(to == Priority.COMPLETING ? Priority.IDLE.priority : to.priority, scheduledPriority == null ? Priority.IDLE.priority : scheduledPriority.priority);
|
|
+ int lowestPriority = priorityCounts.length; // exclusive
|
|
+ for (;highest < lowestPriority; ++highest) {
|
|
+ final long count = priorityCounts[highest];
|
|
+ if (count < 0) {
|
|
+ throw new IllegalStateException("Priority " + highest + " has " + count + " scheduled tasks");
|
|
+ }
|
|
+
|
|
+ if (count != 0) {
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ final Priority newPriority;
|
|
+ if (highest == lowestPriority) {
|
|
+ // no tasks left
|
|
+ newPriority = null;
|
|
+ } else if (shutdown) {
|
|
+ // whichever is lower, the actual greatest priority or simply HIGHEST
|
|
+ // this is so shutdown automatically gets priority
|
|
+ newPriority = Priority.getPriority(Math.min(highest, Priority.HIGHEST.priority));
|
|
+ } else {
|
|
+ newPriority = Priority.getPriority(highest);
|
|
+ }
|
|
+
|
|
+ final int executorsWanted;
|
|
+ boolean shouldNotifyHighPriority = false;
|
|
+
|
|
+ final PrioritisedThreadPool pool = this.pool;
|
|
+ final TreeSet<PrioritisedPoolExecutorImpl> queues = pool.queues;
|
|
+
|
|
+ synchronized (queues) {
|
|
+ if (!this.isQueued) {
|
|
+ // see if we need to be queued
|
|
+ if (newPriority != null) {
|
|
+ if (this.schedulingId == 0L) {
|
|
+ this.schedulingId = ++pool.schedulingIdGenerator;
|
|
+ }
|
|
+ this.scheduledPriority = newPriority; // must be updated before queue add
|
|
+ if (!this.isHalted && this.concurrentExecutors < this.maximumExecutors) {
|
|
+ shouldNotifyHighPriority = newPriority.isHigherOrEqualPriority(Priority.HIGH);
|
|
+ queues.add(this);
|
|
+ this.isQueued = true;
|
|
+ }
|
|
+ } else {
|
|
+ // do not queue
|
|
+ this.scheduledPriority = null;
|
|
+ }
|
|
+ } else {
|
|
+ // see if we need to NOT be queued
|
|
+ if (newPriority == null) {
|
|
+ queues.remove(this);
|
|
+ this.scheduledPriority = null;
|
|
+ this.isQueued = false;
|
|
+ } else if (scheduledPriority != newPriority) {
|
|
+ // if our priority changed, we need to update it - which means removing and re-adding into the queue
|
|
+ queues.remove(this);
|
|
+ // only now can we update scheduledPriority, since we are no longer in queue
|
|
+ this.scheduledPriority = newPriority;
|
|
+ queues.add(this);
|
|
+ shouldNotifyHighPriority = (scheduledPriority == null || scheduledPriority.isLowerPriority(Priority.HIGH)) && newPriority.isHigherOrEqualPriority(Priority.HIGH);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (this.isQueued) {
|
|
+ executorsWanted = Math.min(this.maximumExecutors - this.concurrentExecutors, (int)totalQueuedTasks);
|
|
+ } else {
|
|
+ executorsWanted = 0;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (newPriority == null && shutdown) {
|
|
+ synchronized (pool.activeQueues) {
|
|
+ pool.activeQueues.remove(this);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // Wake up the number of executors we want
|
|
+ if (executorsWanted > 0 || (shouldNotifyTasks | shouldNotifyHighPriority)) {
|
|
+ int notified = 0;
|
|
+ for (final PrioritisedThread thread : pool.threads) {
|
|
+ if ((shouldNotifyHighPriority ? thread.alertHighPriorityExecutor() : thread.notifyTasks())
|
|
+ && (++notified >= executorsWanted)) {
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean shutdown() {
|
|
+ final boolean ret = super.shutdown();
|
|
+ if (!ret) {
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ final PrioritisedThreadPool pool = this.pool;
|
|
+
|
|
+ // remove from active queues
|
|
+ synchronized (pool.nonShutdownQueues) {
|
|
+ pool.nonShutdownQueues.remove(this);
|
|
+ }
|
|
+
|
|
+ final TreeSet<PrioritisedPoolExecutorImpl> queues = pool.queues;
|
|
+
|
|
+ // try and shift around our priority
|
|
+ synchronized (queues) {
|
|
+ if (this.scheduledPriority == null) {
|
|
+ // no tasks are queued, ensure we aren't in activeQueues
|
|
+ synchronized (pool.activeQueues) {
|
|
+ pool.activeQueues.remove(this);
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ // try to set scheduled priority to HIGHEST so it drains faster
|
|
+
|
|
+ if (this.scheduledPriority.isHigherOrEqualPriority(Priority.HIGHEST)) {
|
|
+ // already at target priority (highest or above)
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ // shift priority to HIGHEST
|
|
+
|
|
+ if (this.isQueued) {
|
|
+ queues.remove(this);
|
|
+ this.scheduledPriority = Priority.HIGHEST;
|
|
+ queues.add(this);
|
|
+ } else {
|
|
+ this.scheduledPriority = Priority.HIGHEST;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadedTaskQueue.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadedTaskQueue.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..b71404be2c82f7db35272b367af861e94d6c73d3
|
|
--- /dev/null
|
|
+++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadedTaskQueue.java
|
|
@@ -0,0 +1,378 @@
|
|
+package ca.spottedleaf.concurrentutil.executor.standard;
|
|
+
|
|
+import java.util.ArrayDeque;
|
|
+import java.util.concurrent.atomic.AtomicLong;
|
|
+
|
|
+public class PrioritisedThreadedTaskQueue implements PrioritisedExecutor {
|
|
+
|
|
+ protected final ArrayDeque<PrioritisedTask>[] queues = new ArrayDeque[Priority.TOTAL_SCHEDULABLE_PRIORITIES]; {
|
|
+ for (int i = 0; i < Priority.TOTAL_SCHEDULABLE_PRIORITIES; ++i) {
|
|
+ this.queues[i] = new ArrayDeque<>();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // Use AtomicLong to separate from the queue field, we don't want false sharing here.
|
|
+ protected final AtomicLong totalScheduledTasks = new AtomicLong();
|
|
+ protected final AtomicLong totalCompletedTasks = new AtomicLong();
|
|
+
|
|
+ // this is here to prevent failures to queue stalling flush() calls (as the schedule calls would increment totalScheduledTasks without this check)
|
|
+ protected volatile boolean hasShutdown;
|
|
+
|
|
+ protected long taskIdGenerator = 0;
|
|
+
|
|
+ @Override
|
|
+ public PrioritisedExecutor.PrioritisedTask queueRunnable(final Runnable task, final PrioritisedExecutor.Priority priority) throws IllegalStateException, IllegalArgumentException {
|
|
+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
|
|
+ throw new IllegalArgumentException("Priority " + priority + " is invalid");
|
|
+ }
|
|
+ if (task == null) {
|
|
+ throw new NullPointerException("Task cannot be null");
|
|
+ }
|
|
+
|
|
+ if (this.hasShutdown) {
|
|
+ // prevent us from stalling flush() calls by incrementing scheduled tasks when we really didn't schedule something
|
|
+ throw new IllegalStateException("Queue has shutdown");
|
|
+ }
|
|
+
|
|
+ final PrioritisedTask ret;
|
|
+
|
|
+ synchronized (this.queues) {
|
|
+ if (this.hasShutdown) {
|
|
+ throw new IllegalStateException("Queue has shutdown");
|
|
+ }
|
|
+ this.getAndAddTotalScheduledTasksVolatile(1L);
|
|
+
|
|
+ ret = new PrioritisedTask(this.taskIdGenerator++, task, priority, this);
|
|
+
|
|
+ this.queues[ret.priority.priority].add(ret);
|
|
+
|
|
+ // call priority change callback (note: only after we successfully queue!)
|
|
+ this.priorityChange(ret, null, priority);
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public PrioritisedExecutor.PrioritisedTask createTask(final Runnable task, final Priority priority) {
|
|
+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
|
|
+ throw new IllegalArgumentException("Priority " + priority + " is invalid");
|
|
+ }
|
|
+ if (task == null) {
|
|
+ throw new NullPointerException("Task cannot be null");
|
|
+ }
|
|
+
|
|
+ return new PrioritisedTask(task, priority, this);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public long getTotalTasksScheduled() {
|
|
+ return this.totalScheduledTasks.get();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public long getTotalTasksExecuted() {
|
|
+ return this.totalCompletedTasks.get();
|
|
+ }
|
|
+
|
|
+ // callback method for subclasses to override
|
|
+ // from is null when a task is immediately created
|
|
+ protected void priorityChange(final PrioritisedTask task, final Priority from, final Priority to) {}
|
|
+
|
|
+ /**
|
|
+ * Polls the highest priority task currently available. {@code null} if none. This will mark the
|
|
+ * returned task as completed.
|
|
+ */
|
|
+ protected PrioritisedTask poll() {
|
|
+ return this.poll(Priority.IDLE);
|
|
+ }
|
|
+
|
|
+ protected PrioritisedTask poll(final PrioritisedExecutor.Priority minPriority) {
|
|
+ final ArrayDeque<PrioritisedTask>[] queues = this.queues;
|
|
+ synchronized (queues) {
|
|
+ final int max = minPriority.priority;
|
|
+ for (int i = 0; i <= max; ++i) {
|
|
+ final ArrayDeque<PrioritisedTask> queue = queues[i];
|
|
+ PrioritisedTask task;
|
|
+ while ((task = queue.pollFirst()) != null) {
|
|
+ if (task.trySetCompleting(i)) {
|
|
+ return task;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Polls and executes the highest priority task currently available. Exceptions thrown during task execution will
|
|
+ * be rethrown.
|
|
+ * @return {@code true} if a task was executed, {@code false} otherwise.
|
|
+ */
|
|
+ @Override
|
|
+ public boolean executeTask() {
|
|
+ final PrioritisedTask task = this.poll();
|
|
+
|
|
+ if (task != null) {
|
|
+ task.executeInternal();
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean shutdown() {
|
|
+ synchronized (this.queues) {
|
|
+ if (this.hasShutdown) {
|
|
+ return false;
|
|
+ }
|
|
+ this.hasShutdown = true;
|
|
+ }
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean isShutdown() {
|
|
+ return this.hasShutdown;
|
|
+ }
|
|
+
|
|
+ /* totalScheduledTasks */
|
|
+
|
|
+ protected final long getTotalScheduledTasksVolatile() {
|
|
+ return this.totalScheduledTasks.get();
|
|
+ }
|
|
+
|
|
+ protected final long getAndAddTotalScheduledTasksVolatile(final long value) {
|
|
+ return this.totalScheduledTasks.getAndAdd(value);
|
|
+ }
|
|
+
|
|
+ /* totalCompletedTasks */
|
|
+
|
|
+ protected final long getTotalCompletedTasksVolatile() {
|
|
+ return this.totalCompletedTasks.get();
|
|
+ }
|
|
+
|
|
+ protected final long getAndAddTotalCompletedTasksVolatile(final long value) {
|
|
+ return this.totalCompletedTasks.getAndAdd(value);
|
|
+ }
|
|
+
|
|
+ protected static final class PrioritisedTask implements PrioritisedExecutor.PrioritisedTask {
|
|
+ protected final PrioritisedThreadedTaskQueue queue;
|
|
+ protected long id;
|
|
+ protected static final long NOT_SCHEDULED_ID = -1L;
|
|
+
|
|
+ protected Runnable runnable;
|
|
+ protected volatile PrioritisedExecutor.Priority priority;
|
|
+
|
|
+ protected PrioritisedTask(final long id, final Runnable runnable, final PrioritisedExecutor.Priority priority, final PrioritisedThreadedTaskQueue queue) {
|
|
+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
|
|
+ throw new IllegalArgumentException("Invalid priority " + priority);
|
|
+ }
|
|
+
|
|
+ this.priority = priority;
|
|
+ this.runnable = runnable;
|
|
+ this.queue = queue;
|
|
+ this.id = id;
|
|
+ }
|
|
+
|
|
+ protected PrioritisedTask(final Runnable runnable, final PrioritisedExecutor.Priority priority, final PrioritisedThreadedTaskQueue queue) {
|
|
+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
|
|
+ throw new IllegalArgumentException("Invalid priority " + priority);
|
|
+ }
|
|
+
|
|
+ this.priority = priority;
|
|
+ this.runnable = runnable;
|
|
+ this.queue = queue;
|
|
+ this.id = NOT_SCHEDULED_ID;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean queue() {
|
|
+ if (this.queue.hasShutdown) {
|
|
+ throw new IllegalStateException("Queue has shutdown");
|
|
+ }
|
|
+
|
|
+ synchronized (this.queue.queues) {
|
|
+ if (this.queue.hasShutdown) {
|
|
+ throw new IllegalStateException("Queue has shutdown");
|
|
+ }
|
|
+
|
|
+ final PrioritisedExecutor.Priority priority = this.priority;
|
|
+ if (priority == PrioritisedExecutor.Priority.COMPLETING) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ if (this.id != NOT_SCHEDULED_ID) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ this.queue.getAndAddTotalScheduledTasksVolatile(1L);
|
|
+ this.id = this.queue.taskIdGenerator++;
|
|
+ this.queue.queues[priority.priority].add(this);
|
|
+
|
|
+ this.queue.priorityChange(this, null, priority);
|
|
+
|
|
+ return true;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ protected boolean trySetCompleting(final int minPriority) {
|
|
+ final PrioritisedExecutor.Priority oldPriority = this.priority;
|
|
+ if (oldPriority != PrioritisedExecutor.Priority.COMPLETING && oldPriority.isHigherOrEqualPriority(minPriority)) {
|
|
+ this.priority = PrioritisedExecutor.Priority.COMPLETING;
|
|
+ if (this.id != NOT_SCHEDULED_ID) {
|
|
+ this.queue.priorityChange(this, oldPriority, PrioritisedExecutor.Priority.COMPLETING);
|
|
+ }
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public PrioritisedExecutor.Priority getPriority() {
|
|
+ return this.priority;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean setPriority(final PrioritisedExecutor.Priority priority) {
|
|
+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
|
|
+ throw new IllegalArgumentException("Invalid priority " + priority);
|
|
+ }
|
|
+ synchronized (this.queue.queues) {
|
|
+ final PrioritisedExecutor.Priority curr = this.priority;
|
|
+
|
|
+ if (curr == PrioritisedExecutor.Priority.COMPLETING) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ if (curr == priority) {
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ this.priority = priority;
|
|
+ if (this.id != NOT_SCHEDULED_ID) {
|
|
+ this.queue.queues[priority.priority].add(this);
|
|
+
|
|
+ // call priority change callback
|
|
+ this.queue.priorityChange(this, curr, priority);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean raisePriority(final PrioritisedExecutor.Priority priority) {
|
|
+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
|
|
+ throw new IllegalArgumentException("Invalid priority " + priority);
|
|
+ }
|
|
+
|
|
+ synchronized (this.queue.queues) {
|
|
+ final PrioritisedExecutor.Priority curr = this.priority;
|
|
+
|
|
+ if (curr == PrioritisedExecutor.Priority.COMPLETING) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ if (curr.isHigherOrEqualPriority(priority)) {
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ this.priority = priority;
|
|
+ if (this.id != NOT_SCHEDULED_ID) {
|
|
+ this.queue.queues[priority.priority].add(this);
|
|
+
|
|
+ // call priority change callback
|
|
+ this.queue.priorityChange(this, curr, priority);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean lowerPriority(final PrioritisedExecutor.Priority priority) {
|
|
+ if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
|
|
+ throw new IllegalArgumentException("Invalid priority " + priority);
|
|
+ }
|
|
+
|
|
+ synchronized (this.queue.queues) {
|
|
+ final PrioritisedExecutor.Priority curr = this.priority;
|
|
+
|
|
+ if (curr == PrioritisedExecutor.Priority.COMPLETING) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ if (curr.isLowerOrEqualPriority(priority)) {
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ this.priority = priority;
|
|
+ if (this.id != NOT_SCHEDULED_ID) {
|
|
+ this.queue.queues[priority.priority].add(this);
|
|
+
|
|
+ // call priority change callback
|
|
+ this.queue.priorityChange(this, curr, priority);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean cancel() {
|
|
+ final long id;
|
|
+ synchronized (this.queue.queues) {
|
|
+ final Priority oldPriority = this.priority;
|
|
+ if (oldPriority == PrioritisedExecutor.Priority.COMPLETING) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ this.priority = PrioritisedExecutor.Priority.COMPLETING;
|
|
+ // call priority change callback
|
|
+ if ((id = this.id) != NOT_SCHEDULED_ID) {
|
|
+ this.queue.priorityChange(this, oldPriority, PrioritisedExecutor.Priority.COMPLETING);
|
|
+ }
|
|
+ }
|
|
+ this.runnable = null;
|
|
+ if (id != NOT_SCHEDULED_ID) {
|
|
+ this.queue.getAndAddTotalCompletedTasksVolatile(1L);
|
|
+ }
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ protected void executeInternal() {
|
|
+ try {
|
|
+ final Runnable execute = this.runnable;
|
|
+ this.runnable = null;
|
|
+ execute.run();
|
|
+ } finally {
|
|
+ if (this.id != NOT_SCHEDULED_ID) {
|
|
+ this.queue.getAndAddTotalCompletedTasksVolatile(1L);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean execute() {
|
|
+ synchronized (this.queue.queues) {
|
|
+ final Priority oldPriority = this.priority;
|
|
+ if (oldPriority == PrioritisedExecutor.Priority.COMPLETING) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ this.priority = PrioritisedExecutor.Priority.COMPLETING;
|
|
+ // call priority change callback
|
|
+ if (this.id != NOT_SCHEDULED_ID) {
|
|
+ this.queue.priorityChange(this, oldPriority, PrioritisedExecutor.Priority.COMPLETING);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ this.executeInternal();
|
|
+ return true;
|
|
+ }
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRHashTable.java b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRHashTable.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..a037bb57bedc0cde6b979f5c1f9669678fa7bd16
|
|
--- /dev/null
|
|
+++ b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRHashTable.java
|
|
@@ -0,0 +1,1673 @@
|
|
+package ca.spottedleaf.concurrentutil.map;
|
|
+
|
|
+import ca.spottedleaf.concurrentutil.util.ArrayUtil;
|
|
+import ca.spottedleaf.concurrentutil.util.CollectionUtil;
|
|
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
|
|
+import ca.spottedleaf.concurrentutil.util.Validate;
|
|
+import io.papermc.paper.util.IntegerUtil;
|
|
+import java.lang.invoke.VarHandle;
|
|
+import java.util.ArrayList;
|
|
+import java.util.Arrays;
|
|
+import java.util.Collection;
|
|
+import java.util.Iterator;
|
|
+import java.util.List;
|
|
+import java.util.Map;
|
|
+import java.util.NoSuchElementException;
|
|
+import java.util.Set;
|
|
+import java.util.Spliterator;
|
|
+import java.util.Spliterators;
|
|
+import java.util.function.BiConsumer;
|
|
+import java.util.function.BiFunction;
|
|
+import java.util.function.BiPredicate;
|
|
+import java.util.function.Consumer;
|
|
+import java.util.function.Function;
|
|
+import java.util.function.IntFunction;
|
|
+import java.util.function.Predicate;
|
|
+
|
|
+/**
|
|
+ * <p>
|
|
+ * Note: Not really tested, use at your own risk.
|
|
+ * </p>
|
|
+ * This map is safe for reading from multiple threads, however it is only safe to write from a single thread.
|
|
+ * {@code null} keys or values are not permitted. Writes to values in this map are guaranteed to be ordered by release semantics,
|
|
+ * however immediate visibility to other threads is not guaranteed. However, writes are guaranteed to be made visible eventually.
|
|
+ * Reads are ordered by acquire semantics.
|
|
+ * <p>
|
|
+ * Iterators cannot be modified concurrently, and its backing map cannot be modified concurrently. There is no
|
|
+ * fast-fail attempt made by iterators, thus modifying the iterator's backing map while iterating will have undefined
|
|
+ * behaviour.
|
|
+ * </p>
|
|
+ * <p>
|
|
+ * Subclasses should override {@link #clone()} to return correct instances of this class.
|
|
+ * </p>
|
|
+ * @param <K> {@inheritDoc}
|
|
+ * @param <V> {@inheritDoc}
|
|
+ */
|
|
+public class SWMRHashTable<K, V> implements Map<K, V>, Iterable<Map.Entry<K, V>> {
|
|
+
|
|
+ protected int size;
|
|
+
|
|
+ protected TableEntry<K, V>[] table;
|
|
+
|
|
+ protected final float loadFactor;
|
|
+
|
|
+ protected static final VarHandle SIZE_HANDLE = ConcurrentUtil.getVarHandle(SWMRHashTable.class, "size", int.class);
|
|
+ protected static final VarHandle TABLE_HANDLE = ConcurrentUtil.getVarHandle(SWMRHashTable.class, "table", TableEntry[].class);
|
|
+
|
|
+ /* size */
|
|
+
|
|
+ protected final int getSizePlain() {
|
|
+ return (int)SIZE_HANDLE.get(this);
|
|
+ }
|
|
+
|
|
+ protected final int getSizeOpaque() {
|
|
+ return (int)SIZE_HANDLE.getOpaque(this);
|
|
+ }
|
|
+
|
|
+ protected final int getSizeAcquire() {
|
|
+ return (int)SIZE_HANDLE.getAcquire(this);
|
|
+ }
|
|
+
|
|
+ protected final void setSizePlain(final int value) {
|
|
+ SIZE_HANDLE.set(this, value);
|
|
+ }
|
|
+
|
|
+ protected final void setSizeOpaque(final int value) {
|
|
+ SIZE_HANDLE.setOpaque(this, value);
|
|
+ }
|
|
+
|
|
+ protected final void setSizeRelease(final int value) {
|
|
+ SIZE_HANDLE.setRelease(this, value);
|
|
+ }
|
|
+
|
|
+ /* table */
|
|
+
|
|
+ protected final TableEntry<K, V>[] getTablePlain() {
|
|
+ //noinspection unchecked
|
|
+ return (TableEntry<K, V>[])TABLE_HANDLE.get(this);
|
|
+ }
|
|
+
|
|
+ protected final TableEntry<K, V>[] getTableAcquire() {
|
|
+ //noinspection unchecked
|
|
+ return (TableEntry<K, V>[])TABLE_HANDLE.getAcquire(this);
|
|
+ }
|
|
+
|
|
+ protected final void setTablePlain(final TableEntry<K, V>[] table) {
|
|
+ TABLE_HANDLE.set(this, table);
|
|
+ }
|
|
+
|
|
+ protected final void setTableRelease(final TableEntry<K, V>[] table) {
|
|
+ TABLE_HANDLE.setRelease(this, table);
|
|
+ }
|
|
+
|
|
+ protected static final int DEFAULT_CAPACITY = 16;
|
|
+ protected static final float DEFAULT_LOAD_FACTOR = 0.75f;
|
|
+ protected static final int MAXIMUM_CAPACITY = Integer.MIN_VALUE >>> 1;
|
|
+
|
|
+ /**
|
|
+ * Constructs this map with a capacity of {@code 16} and load factor of {@code 0.75f}.
|
|
+ */
|
|
+ public SWMRHashTable() {
|
|
+ this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Constructs this map with the specified capacity and load factor of {@code 0.75f}.
|
|
+ * @param capacity specified initial capacity, > 0
|
|
+ */
|
|
+ public SWMRHashTable(final int capacity) {
|
|
+ this(capacity, DEFAULT_LOAD_FACTOR);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Constructs this map with the specified capacity and load factor.
|
|
+ * @param capacity specified capacity, > 0
|
|
+ * @param loadFactor specified load factor, > 0 && finite
|
|
+ */
|
|
+ public SWMRHashTable(final int capacity, final float loadFactor) {
|
|
+ final int tableSize = getCapacityFor(capacity);
|
|
+
|
|
+ if (loadFactor <= 0.0 || !Float.isFinite(loadFactor)) {
|
|
+ throw new IllegalArgumentException("Invalid load factor: " + loadFactor);
|
|
+ }
|
|
+
|
|
+ //noinspection unchecked
|
|
+ final TableEntry<K, V>[] table = new TableEntry[tableSize];
|
|
+ this.setTablePlain(table);
|
|
+
|
|
+ if (tableSize == MAXIMUM_CAPACITY) {
|
|
+ this.threshold = -1;
|
|
+ } else {
|
|
+ this.threshold = getTargetCapacity(tableSize, loadFactor);
|
|
+ }
|
|
+
|
|
+ this.loadFactor = loadFactor;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Constructs this map with a capacity of {@code 16} or the specified map's size, whichever is larger, and
|
|
+ * with a load factor of {@code 0.75f}.
|
|
+ * All of the specified map's entries are copied into this map.
|
|
+ * @param other The specified map.
|
|
+ */
|
|
+ public SWMRHashTable(final Map<K, V> other) {
|
|
+ this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR, other);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Constructs this map with a minimum capacity of the specified capacity or the specified map's size, whichever is larger, and
|
|
+ * with a load factor of {@code 0.75f}.
|
|
+ * All of the specified map's entries are copied into this map.
|
|
+ * @param capacity specified capacity, > 0
|
|
+ * @param other The specified map.
|
|
+ */
|
|
+ public SWMRHashTable(final int capacity, final Map<K, V> other) {
|
|
+ this(capacity, DEFAULT_LOAD_FACTOR, other);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Constructs this map with a min capacity of the specified capacity or the specified map's size, whichever is larger, and
|
|
+ * with the specified load factor.
|
|
+ * All of the specified map's entries are copied into this map.
|
|
+ * @param capacity specified capacity, > 0
|
|
+ * @param loadFactor specified load factor, > 0 && finite
|
|
+ * @param other The specified map.
|
|
+ */
|
|
+ public SWMRHashTable(final int capacity, final float loadFactor, final Map<K, V> other) {
|
|
+ this(Math.max(Validate.notNull(other, "Null map").size(), capacity), loadFactor);
|
|
+ this.putAll(other);
|
|
+ }
|
|
+
|
|
+ public final float getLoadFactor() {
|
|
+ return this.loadFactor;
|
|
+ }
|
|
+
|
|
+ protected static int getCapacityFor(final int capacity) {
|
|
+ if (capacity <= 0) {
|
|
+ throw new IllegalArgumentException("Invalid capacity: " + capacity);
|
|
+ }
|
|
+ if (capacity >= MAXIMUM_CAPACITY) {
|
|
+ return MAXIMUM_CAPACITY;
|
|
+ }
|
|
+ return IntegerUtil.roundCeilLog2(capacity);
|
|
+ }
|
|
+
|
|
+ /** Callers must still use acquire when reading the value of the entry. */
|
|
+ protected final TableEntry<K, V> getEntryForOpaque(final K key) {
|
|
+ final int hash = SWMRHashTable.getHash(key);
|
|
+ final TableEntry<K, V>[] table = this.getTableAcquire();
|
|
+
|
|
+ for (TableEntry<K, V> curr = ArrayUtil.getOpaque(table, hash & (table.length - 1)); curr != null; curr = curr.getNextOpaque()) {
|
|
+ if (hash == curr.hash && (key == curr.key || curr.key.equals(key))) {
|
|
+ return curr;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ protected final TableEntry<K, V> getEntryForPlain(final K key) {
|
|
+ final int hash = SWMRHashTable.getHash(key);
|
|
+ final TableEntry<K, V>[] table = this.getTablePlain();
|
|
+
|
|
+ for (TableEntry<K, V> curr = table[hash & (table.length - 1)]; curr != null; curr = curr.getNextPlain()) {
|
|
+ if (hash == curr.hash && (key == curr.key || curr.key.equals(key))) {
|
|
+ return curr;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ /* MT-Safe */
|
|
+
|
|
+ /** must be deterministic given a key */
|
|
+ private static int getHash(final Object key) {
|
|
+ int hash = key == null ? 0 : key.hashCode();
|
|
+ // inlined IntegerUtil#hash0
|
|
+ hash *= 0x36935555;
|
|
+ hash ^= hash >>> 16;
|
|
+ return hash;
|
|
+ }
|
|
+
|
|
+ static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
|
|
+ static final int spread(int h) {
|
|
+ return (h ^ (h >>> 16)) & HASH_BITS;
|
|
+ }
|
|
+
|
|
+ // rets -1 if capacity*loadFactor is too large
|
|
+ protected static int getTargetCapacity(final int capacity, final float loadFactor) {
|
|
+ final double ret = (double)capacity * (double)loadFactor;
|
|
+ if (Double.isInfinite(ret) || ret >= ((double)Integer.MAX_VALUE)) {
|
|
+ return -1;
|
|
+ }
|
|
+
|
|
+ return (int)ret;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public boolean equals(final Object obj) {
|
|
+ if (this == obj) {
|
|
+ return true;
|
|
+ }
|
|
+ /* Make no attempt to deal with concurrent modifications */
|
|
+ if (!(obj instanceof Map)) {
|
|
+ return false;
|
|
+ }
|
|
+ final Map<?, ?> other = (Map<?, ?>)obj;
|
|
+
|
|
+ if (this.size() != other.size()) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ final TableEntry<K, V>[] table = this.getTableAcquire();
|
|
+
|
|
+ for (int i = 0, len = table.length; i < len; ++i) {
|
|
+ for (TableEntry<K, V> curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
|
|
+ final V value = curr.getValueAcquire();
|
|
+
|
|
+ final Object otherValue = other.get(curr.key);
|
|
+ if (otherValue == null || (value != otherValue && value.equals(otherValue))) {
|
|
+ return false;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public int hashCode() {
|
|
+ /* Make no attempt to deal with concurrent modifications */
|
|
+ int hash = 0;
|
|
+ final TableEntry<K, V>[] table = this.getTableAcquire();
|
|
+
|
|
+ for (int i = 0, len = table.length; i < len; ++i) {
|
|
+ for (TableEntry<K, V> curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
|
|
+ hash += curr.hashCode();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return hash;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public String toString() {
|
|
+ final StringBuilder builder = new StringBuilder(64);
|
|
+ builder.append("SingleWriterMultiReaderHashMap:{");
|
|
+
|
|
+ this.forEach((final K key, final V value) -> {
|
|
+ builder.append("{key: \"").append(key).append("\", value: \"").append(value).append("\"}");
|
|
+ });
|
|
+
|
|
+ return builder.append('}').toString();
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public SWMRHashTable<K, V> clone() {
|
|
+ return new SWMRHashTable<>(this.getTableAcquire().length, this.loadFactor, this);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public Iterator<Entry<K, V>> iterator() {
|
|
+ return new EntryIterator<>(this.getTableAcquire(), this);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public void forEach(final Consumer<? super Entry<K, V>> action) {
|
|
+ Validate.notNull(action, "Null action");
|
|
+
|
|
+ final TableEntry<K, V>[] table = this.getTableAcquire();
|
|
+ for (int i = 0, len = table.length; i < len; ++i) {
|
|
+ for (TableEntry<K, V> curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
|
|
+ action.accept(curr);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public void forEach(final BiConsumer<? super K, ? super V> action) {
|
|
+ Validate.notNull(action, "Null action");
|
|
+
|
|
+ final TableEntry<K, V>[] table = this.getTableAcquire();
|
|
+ for (int i = 0, len = table.length; i < len; ++i) {
|
|
+ for (TableEntry<K, V> curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
|
|
+ final V value = curr.getValueAcquire();
|
|
+
|
|
+ action.accept(curr.key, value);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Provides the specified consumer with all keys contained within this map.
|
|
+ * @param action The specified consumer.
|
|
+ */
|
|
+ public void forEachKey(final Consumer<? super K> action) {
|
|
+ Validate.notNull(action, "Null action");
|
|
+
|
|
+ final TableEntry<K, V>[] table = this.getTableAcquire();
|
|
+ for (int i = 0, len = table.length; i < len; ++i) {
|
|
+ for (TableEntry<K, V> curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
|
|
+ action.accept(curr.key);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Provides the specified consumer with all values contained within this map. Equivalent to {@code map.values().forEach(Consumer)}.
|
|
+ * @param action The specified consumer.
|
|
+ */
|
|
+ public void forEachValue(final Consumer<? super V> action) {
|
|
+ Validate.notNull(action, "Null action");
|
|
+
|
|
+ final TableEntry<K, V>[] table = this.getTableAcquire();
|
|
+ for (int i = 0, len = table.length; i < len; ++i) {
|
|
+ for (TableEntry<K, V> curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
|
|
+ final V value = curr.getValueAcquire();
|
|
+
|
|
+ action.accept(value);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public V get(final Object key) {
|
|
+ Validate.notNull(key, "Null key");
|
|
+
|
|
+ //noinspection unchecked
|
|
+ final TableEntry<K, V> entry = this.getEntryForOpaque((K)key);
|
|
+ return entry == null ? null : entry.getValueAcquire();
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public boolean containsKey(final Object key) {
|
|
+ Validate.notNull(key, "Null key");
|
|
+
|
|
+ // note: we need to use getValueAcquire, so that the reads from this map are ordered by acquire semantics
|
|
+ return this.get(key) != null;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Returns {@code true} if this map contains an entry with the specified key and value at some point during this call.
|
|
+ * @param key The specified key.
|
|
+ * @param value The specified value.
|
|
+ * @return {@code true} if this map contains an entry with the specified key and value.
|
|
+ */
|
|
+ public boolean contains(final Object key, final Object value) {
|
|
+ Validate.notNull(key, "Null key");
|
|
+
|
|
+ //noinspection unchecked
|
|
+ final TableEntry<K, V> entry = this.getEntryForOpaque((K)key);
|
|
+
|
|
+ if (entry == null) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ final V entryVal = entry.getValueAcquire();
|
|
+ return entryVal == value || entryVal.equals(value);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public boolean containsValue(final Object value) {
|
|
+ Validate.notNull(value, "Null value");
|
|
+
|
|
+ final TableEntry<K, V>[] table = this.getTableAcquire();
|
|
+ for (int i = 0, len = table.length; i < len; ++i) {
|
|
+ for (TableEntry<K, V> curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
|
|
+ final V currVal = curr.getValueAcquire();
|
|
+ if (currVal == value || currVal.equals(value)) {
|
|
+ return true;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public V getOrDefault(final Object key, final V defaultValue) {
|
|
+ Validate.notNull(key, "Null key");
|
|
+
|
|
+ //noinspection unchecked
|
|
+ final TableEntry<K, V> entry = this.getEntryForOpaque((K)key);
|
|
+
|
|
+ return entry == null ? defaultValue : entry.getValueAcquire();
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public int size() {
|
|
+ return this.getSizeAcquire();
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public boolean isEmpty() {
|
|
+ return this.getSizeAcquire() == 0;
|
|
+ }
|
|
+
|
|
+ protected Set<K> keyset;
|
|
+ protected Collection<V> values;
|
|
+ protected Set<Map.Entry<K, V>> entrySet;
|
|
+
|
|
+ @Override
|
|
+ public Set<K> keySet() {
|
|
+ return this.keyset == null ? this.keyset = new KeySet<>(this) : this.keyset;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public Collection<V> values() {
|
|
+ return this.values == null ? this.values = new ValueCollection<>(this) : this.values;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public Set<Map.Entry<K, V>> entrySet() {
|
|
+ return this.entrySet == null ? this.entrySet = new EntrySet<>(this) : this.entrySet;
|
|
+ }
|
|
+
|
|
+ /* Non-MT-Safe */
|
|
+
|
|
+ protected int threshold;
|
|
+
|
|
+ protected final void checkResize(final int minCapacity) {
|
|
+ if (minCapacity <= this.threshold || this.threshold < 0) {
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ final TableEntry<K, V>[] table = this.getTablePlain();
|
|
+ int newCapacity = minCapacity >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY : IntegerUtil.roundCeilLog2(minCapacity);
|
|
+ if (newCapacity < 0) {
|
|
+ newCapacity = MAXIMUM_CAPACITY;
|
|
+ }
|
|
+ if (newCapacity <= table.length) {
|
|
+ if (newCapacity == MAXIMUM_CAPACITY) {
|
|
+ return;
|
|
+ }
|
|
+ newCapacity = table.length << 1;
|
|
+ }
|
|
+
|
|
+ //noinspection unchecked
|
|
+ final TableEntry<K, V>[] newTable = new TableEntry[newCapacity];
|
|
+ final int indexMask = newCapacity - 1;
|
|
+
|
|
+ for (int i = 0, len = table.length; i < len; ++i) {
|
|
+ for (TableEntry<K, V> entry = table[i]; entry != null; entry = entry.getNextPlain()) {
|
|
+ final int hash = entry.hash;
|
|
+ final int index = hash & indexMask;
|
|
+
|
|
+ /* we need to create a new entry since there could be reading threads */
|
|
+ final TableEntry<K, V> insert = new TableEntry<>(hash, entry.key, entry.getValuePlain());
|
|
+
|
|
+ final TableEntry<K, V> prev = newTable[index];
|
|
+
|
|
+ newTable[index] = insert;
|
|
+ insert.setNextPlain(prev);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (newCapacity == MAXIMUM_CAPACITY) {
|
|
+ this.threshold = -1; /* No more resizing */
|
|
+ } else {
|
|
+ this.threshold = getTargetCapacity(newCapacity, this.loadFactor);
|
|
+ }
|
|
+ this.setTableRelease(newTable); /* use release to publish entries in table */
|
|
+ }
|
|
+
|
|
+ protected final int addToSize(final int num) {
|
|
+ final int newSize = this.getSizePlain() + num;
|
|
+
|
|
+ this.setSizeOpaque(newSize);
|
|
+ this.checkResize(newSize);
|
|
+
|
|
+ return newSize;
|
|
+ }
|
|
+
|
|
+ protected final int removeFromSize(final int num) {
|
|
+ final int newSize = this.getSizePlain() - num;
|
|
+
|
|
+ this.setSizeOpaque(newSize);
|
|
+
|
|
+ return newSize;
|
|
+ }
|
|
+
|
|
+ /* Cannot be used to perform downsizing */
|
|
+ protected final int removeFromSizePlain(final int num) {
|
|
+ final int newSize = this.getSizePlain() - num;
|
|
+
|
|
+ this.setSizePlain(newSize);
|
|
+
|
|
+ return newSize;
|
|
+ }
|
|
+
|
|
+ protected final V put(final K key, final V value, final boolean onlyIfAbsent) {
|
|
+ final TableEntry<K, V>[] table = this.getTablePlain();
|
|
+ final int hash = SWMRHashTable.getHash(key);
|
|
+ final int index = hash & (table.length - 1);
|
|
+
|
|
+ final TableEntry<K, V> head = table[index];
|
|
+ if (head == null) {
|
|
+ final TableEntry<K, V> insert = new TableEntry<>(hash, key, value);
|
|
+ ArrayUtil.setRelease(table, index, insert);
|
|
+ this.addToSize(1);
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ for (TableEntry<K, V> curr = head;;) {
|
|
+ if (curr.hash == hash && (key == curr.key || curr.key.equals(key))) {
|
|
+ if (onlyIfAbsent) {
|
|
+ return curr.getValuePlain();
|
|
+ }
|
|
+
|
|
+ final V currVal = curr.getValuePlain();
|
|
+ curr.setValueRelease(value);
|
|
+ return currVal;
|
|
+ }
|
|
+
|
|
+ final TableEntry<K, V> next = curr.getNextPlain();
|
|
+ if (next != null) {
|
|
+ curr = next;
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ final TableEntry<K, V> insert = new TableEntry<>(hash, key, value);
|
|
+
|
|
+ curr.setNextRelease(insert);
|
|
+ this.addToSize(1);
|
|
+ return null;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Removes a key-value pair from this map if the specified predicate returns true. The specified predicate is
|
|
+ * tested with every entry in this map. Returns the number of key-value pairs removed.
|
|
+ * @param predicate The predicate to test key-value pairs against.
|
|
+ * @return The total number of key-value pairs removed from this map.
|
|
+ */
|
|
+ public int removeIf(final BiPredicate<K, V> predicate) {
|
|
+ Validate.notNull(predicate, "Null predicate");
|
|
+
|
|
+ int removed = 0;
|
|
+
|
|
+ final TableEntry<K, V>[] table = this.getTablePlain();
|
|
+
|
|
+ bin_iteration_loop:
|
|
+ for (int i = 0, len = table.length; i < len; ++i) {
|
|
+ TableEntry<K, V> curr = table[i];
|
|
+ if (curr == null) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ /* Handle bin nodes first */
|
|
+ while (predicate.test(curr.key, curr.getValuePlain())) {
|
|
+ ++removed;
|
|
+ this.removeFromSizePlain(1); /* required in case predicate throws an exception */
|
|
+
|
|
+ ArrayUtil.setRelease(table, i, curr = curr.getNextPlain());
|
|
+
|
|
+ if (curr == null) {
|
|
+ continue bin_iteration_loop;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ TableEntry<K, V> prev;
|
|
+
|
|
+ /* curr at this point is the bin node */
|
|
+
|
|
+ for (prev = curr, curr = curr.getNextPlain(); curr != null;) {
|
|
+ /* If we want to remove, then we should hold prev, as it will be a valid entry to link on */
|
|
+ if (predicate.test(curr.key, curr.getValuePlain())) {
|
|
+ ++removed;
|
|
+ this.removeFromSizePlain(1); /* required in case predicate throws an exception */
|
|
+
|
|
+ prev.setNextRelease(curr = curr.getNextPlain());
|
|
+ } else {
|
|
+ prev = curr;
|
|
+ curr = curr.getNextPlain();
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return removed;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Removes a key-value pair from this map if the specified predicate returns true. The specified predicate is
|
|
+ * tested with every entry in this map. Returns the number of key-value pairs removed.
|
|
+ * @param predicate The predicate to test key-value pairs against.
|
|
+ * @return The total number of key-value pairs removed from this map.
|
|
+ */
|
|
+ public int removeEntryIf(final Predicate<? super Entry<K, V>> predicate) {
|
|
+ Validate.notNull(predicate, "Null predicate");
|
|
+
|
|
+ int removed = 0;
|
|
+
|
|
+ final TableEntry<K, V>[] table = this.getTablePlain();
|
|
+
|
|
+ bin_iteration_loop:
|
|
+ for (int i = 0, len = table.length; i < len; ++i) {
|
|
+ TableEntry<K, V> curr = table[i];
|
|
+ if (curr == null) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ /* Handle bin nodes first */
|
|
+ while (predicate.test(curr)) {
|
|
+ ++removed;
|
|
+ this.removeFromSizePlain(1); /* required in case predicate throws an exception */
|
|
+
|
|
+ ArrayUtil.setRelease(table, i, curr = curr.getNextPlain());
|
|
+
|
|
+ if (curr == null) {
|
|
+ continue bin_iteration_loop;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ TableEntry<K, V> prev;
|
|
+
|
|
+ /* curr at this point is the bin node */
|
|
+
|
|
+ for (prev = curr, curr = curr.getNextPlain(); curr != null;) {
|
|
+ /* If we want to remove, then we should hold prev, as it will be a valid entry to link on */
|
|
+ if (predicate.test(curr)) {
|
|
+ ++removed;
|
|
+ this.removeFromSizePlain(1); /* required in case predicate throws an exception */
|
|
+
|
|
+ prev.setNextRelease(curr = curr.getNextPlain());
|
|
+ } else {
|
|
+ prev = curr;
|
|
+ curr = curr.getNextPlain();
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return removed;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public V put(final K key, final V value) {
|
|
+ Validate.notNull(key, "Null key");
|
|
+ Validate.notNull(value, "Null value");
|
|
+
|
|
+ return this.put(key, value, false);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public V putIfAbsent(final K key, final V value) {
|
|
+ Validate.notNull(key, "Null key");
|
|
+ Validate.notNull(value, "Null value");
|
|
+
|
|
+ return this.put(key, value, true);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public boolean remove(final Object key, final Object value) {
|
|
+ Validate.notNull(key, "Null key");
|
|
+ Validate.notNull(value, "Null value");
|
|
+
|
|
+ final TableEntry<K, V>[] table = this.getTablePlain();
|
|
+ final int hash = SWMRHashTable.getHash(key);
|
|
+ final int index = hash & (table.length - 1);
|
|
+
|
|
+ final TableEntry<K, V> head = table[index];
|
|
+ if (head == null) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ if (head.hash == hash && (head.key == key || head.key.equals(key))) {
|
|
+ final V currVal = head.getValuePlain();
|
|
+
|
|
+ if (currVal != value && !currVal.equals(value)) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ ArrayUtil.setRelease(table, index, head.getNextPlain());
|
|
+ this.removeFromSize(1);
|
|
+
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ for (TableEntry<K, V> curr = head.getNextPlain(), prev = head; curr != null; prev = curr, curr = curr.getNextPlain()) {
|
|
+ if (curr.hash == hash && (curr.key == key || curr.key.equals(key))) {
|
|
+ final V currVal = curr.getValuePlain();
|
|
+
|
|
+ if (currVal != value && !currVal.equals(value)) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ prev.setNextRelease(curr.getNextPlain());
|
|
+ this.removeFromSize(1);
|
|
+
|
|
+ return true;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ protected final V remove(final Object key, final int hash) {
|
|
+ final TableEntry<K, V>[] table = this.getTablePlain();
|
|
+ final int index = (table.length - 1) & hash;
|
|
+
|
|
+ final TableEntry<K, V> head = table[index];
|
|
+ if (head == null) {
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ if (hash == head.hash && (head.key == key || head.key.equals(key))) {
|
|
+ ArrayUtil.setRelease(table, index, head.getNextPlain());
|
|
+ this.removeFromSize(1);
|
|
+
|
|
+ return head.getValuePlain();
|
|
+ }
|
|
+
|
|
+ for (TableEntry<K, V> curr = head.getNextPlain(), prev = head; curr != null; prev = curr, curr = curr.getNextPlain()) {
|
|
+ if (curr.hash == hash && (key == curr.key || curr.key.equals(key))) {
|
|
+ prev.setNextRelease(curr.getNextPlain());
|
|
+ this.removeFromSize(1);
|
|
+
|
|
+ return curr.getValuePlain();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public V remove(final Object key) {
|
|
+ Validate.notNull(key, "Null key");
|
|
+
|
|
+ return this.remove(key, SWMRHashTable.getHash(key));
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public boolean replace(final K key, final V oldValue, final V newValue) {
|
|
+ Validate.notNull(key, "Null key");
|
|
+ Validate.notNull(oldValue, "Null oldValue");
|
|
+ Validate.notNull(newValue, "Null newValue");
|
|
+
|
|
+ final TableEntry<K, V> entry = this.getEntryForPlain(key);
|
|
+ if (entry == null) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ final V currValue = entry.getValuePlain();
|
|
+ if (currValue == oldValue || currValue.equals(oldValue)) {
|
|
+ entry.setValueRelease(newValue);
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public V replace(final K key, final V value) {
|
|
+ Validate.notNull(key, "Null key");
|
|
+ Validate.notNull(value, "Null value");
|
|
+
|
|
+ final TableEntry<K, V> entry = this.getEntryForPlain(key);
|
|
+ if (entry == null) {
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ final V prev = entry.getValuePlain();
|
|
+ entry.setValueRelease(value);
|
|
+ return prev;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public void replaceAll(final BiFunction<? super K, ? super V, ? extends V> function) {
|
|
+ Validate.notNull(function, "Null function");
|
|
+
|
|
+ final TableEntry<K, V>[] table = this.getTablePlain();
|
|
+ for (int i = 0, len = table.length; i < len; ++i) {
|
|
+ for (TableEntry<K, V> curr = table[i]; curr != null; curr = curr.getNextPlain()) {
|
|
+ final V value = curr.getValuePlain();
|
|
+
|
|
+ final V newValue = function.apply(curr.key, value);
|
|
+ if (newValue == null) {
|
|
+ throw new NullPointerException();
|
|
+ }
|
|
+
|
|
+ curr.setValueRelease(newValue);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public void putAll(final Map<? extends K, ? extends V> map) {
|
|
+ Validate.notNull(map, "Null map");
|
|
+
|
|
+ final int size = map.size();
|
|
+ this.checkResize(Math.max(this.getSizePlain() + size/2, size)); /* preemptively resize */
|
|
+ map.forEach(this::put);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ * <p>
|
|
+ * This call is non-atomic and the order that which entries are removed is undefined. The clear operation itself
|
|
+ * is release ordered, that is, after the clear operation is performed a release fence is performed.
|
|
+ * </p>
|
|
+ */
|
|
+ @Override
|
|
+ public void clear() {
|
|
+ Arrays.fill(this.getTablePlain(), null);
|
|
+ this.setSizeRelease(0);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public V compute(final K key, final BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
|
|
+ Validate.notNull(key, "Null key");
|
|
+ Validate.notNull(remappingFunction, "Null remappingFunction");
|
|
+
|
|
+ final int hash = SWMRHashTable.getHash(key);
|
|
+ final TableEntry<K, V>[] table = this.getTablePlain();
|
|
+ final int index = hash & (table.length - 1);
|
|
+
|
|
+ for (TableEntry<K, V> curr = table[index], prev = null;;prev = curr, curr = curr.getNextPlain()) {
|
|
+ if (curr == null) {
|
|
+ final V newVal = remappingFunction.apply(key ,null);
|
|
+
|
|
+ if (newVal == null) {
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ final TableEntry<K, V> insert = new TableEntry<>(hash, key, newVal);
|
|
+ if (prev == null) {
|
|
+ ArrayUtil.setRelease(table, index, insert);
|
|
+ } else {
|
|
+ prev.setNextRelease(insert);
|
|
+ }
|
|
+
|
|
+ this.addToSize(1);
|
|
+
|
|
+ return newVal;
|
|
+ }
|
|
+
|
|
+ if (curr.hash == hash && (curr.key == key || curr.key.equals(key))) {
|
|
+ final V newVal = remappingFunction.apply(key, curr.getValuePlain());
|
|
+
|
|
+ if (newVal != null) {
|
|
+ curr.setValueRelease(newVal);
|
|
+ return newVal;
|
|
+ }
|
|
+
|
|
+ if (prev == null) {
|
|
+ ArrayUtil.setRelease(table, index, curr.getNextPlain());
|
|
+ } else {
|
|
+ prev.setNextRelease(curr.getNextPlain());
|
|
+ }
|
|
+
|
|
+ this.removeFromSize(1);
|
|
+
|
|
+ return null;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public V computeIfPresent(final K key, final BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
|
|
+ Validate.notNull(key, "Null key");
|
|
+ Validate.notNull(remappingFunction, "Null remappingFunction");
|
|
+
|
|
+ final int hash = SWMRHashTable.getHash(key);
|
|
+ final TableEntry<K, V>[] table = this.getTablePlain();
|
|
+ final int index = hash & (table.length - 1);
|
|
+
|
|
+ for (TableEntry<K, V> curr = table[index], prev = null; curr != null; prev = curr, curr = curr.getNextPlain()) {
|
|
+ if (curr.hash != hash || (curr.key != key && !curr.key.equals(key))) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ final V newVal = remappingFunction.apply(key, curr.getValuePlain());
|
|
+ if (newVal != null) {
|
|
+ curr.setValueRelease(newVal);
|
|
+ return newVal;
|
|
+ }
|
|
+
|
|
+ if (prev == null) {
|
|
+ ArrayUtil.setRelease(table, index, curr.getNextPlain());
|
|
+ } else {
|
|
+ prev.setNextRelease(curr.getNextPlain());
|
|
+ }
|
|
+
|
|
+ this.removeFromSize(1);
|
|
+
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public V computeIfAbsent(final K key, final Function<? super K, ? extends V> mappingFunction) {
|
|
+ Validate.notNull(key, "Null key");
|
|
+ Validate.notNull(mappingFunction, "Null mappingFunction");
|
|
+
|
|
+ final int hash = SWMRHashTable.getHash(key);
|
|
+ final TableEntry<K, V>[] table = this.getTablePlain();
|
|
+ final int index = hash & (table.length - 1);
|
|
+
|
|
+ for (TableEntry<K, V> curr = table[index], prev = null;;prev = curr, curr = curr.getNextPlain()) {
|
|
+ if (curr != null) {
|
|
+ if (curr.hash == hash && (curr.key == key || curr.key.equals(key))) {
|
|
+ return curr.getValuePlain();
|
|
+ }
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ final V newVal = mappingFunction.apply(key);
|
|
+
|
|
+ if (newVal == null) {
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ final TableEntry<K, V> insert = new TableEntry<>(hash, key, newVal);
|
|
+ if (prev == null) {
|
|
+ ArrayUtil.setRelease(table, index, insert);
|
|
+ } else {
|
|
+ prev.setNextRelease(insert);
|
|
+ }
|
|
+
|
|
+ this.addToSize(1);
|
|
+
|
|
+ return newVal;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public V merge(final K key, final V value, final BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
|
|
+ Validate.notNull(key, "Null key");
|
|
+ Validate.notNull(value, "Null value");
|
|
+ Validate.notNull(remappingFunction, "Null remappingFunction");
|
|
+
|
|
+ final int hash = SWMRHashTable.getHash(key);
|
|
+ final TableEntry<K, V>[] table = this.getTablePlain();
|
|
+ final int index = hash & (table.length - 1);
|
|
+
|
|
+ for (TableEntry<K, V> curr = table[index], prev = null;;prev = curr, curr = curr.getNextPlain()) {
|
|
+ if (curr == null) {
|
|
+ final TableEntry<K, V> insert = new TableEntry<>(hash, key, value);
|
|
+ if (prev == null) {
|
|
+ ArrayUtil.setRelease(table, index, insert);
|
|
+ } else {
|
|
+ prev.setNextRelease(insert);
|
|
+ }
|
|
+
|
|
+ this.addToSize(1);
|
|
+
|
|
+ return value;
|
|
+ }
|
|
+
|
|
+ if (curr.hash == hash && (curr.key == key || curr.key.equals(key))) {
|
|
+ final V newVal = remappingFunction.apply(curr.getValuePlain(), value);
|
|
+
|
|
+ if (newVal != null) {
|
|
+ curr.setValueRelease(newVal);
|
|
+ return newVal;
|
|
+ }
|
|
+
|
|
+ if (prev == null) {
|
|
+ ArrayUtil.setRelease(table, index, curr.getNextPlain());
|
|
+ } else {
|
|
+ prev.setNextRelease(curr.getNextPlain());
|
|
+ }
|
|
+
|
|
+ this.removeFromSize(1);
|
|
+
|
|
+ return null;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ protected static final class TableEntry<K, V> implements Map.Entry<K, V> {
|
|
+
|
|
+ protected final int hash;
|
|
+ protected final K key;
|
|
+ protected V value;
|
|
+
|
|
+ protected TableEntry<K, V> next;
|
|
+
|
|
+ protected static final VarHandle VALUE_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "value", Object.class);
|
|
+ protected static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "next", TableEntry.class);
|
|
+
|
|
+ /* value */
|
|
+
|
|
+ protected final V getValuePlain() {
|
|
+ //noinspection unchecked
|
|
+ return (V)VALUE_HANDLE.get(this);
|
|
+ }
|
|
+
|
|
+ protected final V getValueAcquire() {
|
|
+ //noinspection unchecked
|
|
+ return (V)VALUE_HANDLE.getAcquire(this);
|
|
+ }
|
|
+
|
|
+ protected final void setValueRelease(final V to) {
|
|
+ VALUE_HANDLE.setRelease(this, to);
|
|
+ }
|
|
+
|
|
+ /* next */
|
|
+
|
|
+ protected final TableEntry<K, V> getNextPlain() {
|
|
+ //noinspection unchecked
|
|
+ return (TableEntry<K, V>)NEXT_HANDLE.get(this);
|
|
+ }
|
|
+
|
|
+ protected final TableEntry<K, V> getNextOpaque() {
|
|
+ //noinspection unchecked
|
|
+ return (TableEntry<K, V>)NEXT_HANDLE.getOpaque(this);
|
|
+ }
|
|
+
|
|
+ protected final void setNextPlain(final TableEntry<K, V> next) {
|
|
+ NEXT_HANDLE.set(this, next);
|
|
+ }
|
|
+
|
|
+ protected final void setNextRelease(final TableEntry<K, V> next) {
|
|
+ NEXT_HANDLE.setRelease(this, next);
|
|
+ }
|
|
+
|
|
+ protected TableEntry(final int hash, final K key, final V value) {
|
|
+ this.hash = hash;
|
|
+ this.key = key;
|
|
+ this.value = value;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public K getKey() {
|
|
+ return this.key;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public V getValue() {
|
|
+ return this.getValueAcquire();
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public V setValue(final V value) {
|
|
+ if (value == null) {
|
|
+ throw new NullPointerException();
|
|
+ }
|
|
+
|
|
+ final V curr = this.getValuePlain();
|
|
+
|
|
+ this.setValueRelease(value);
|
|
+ return curr;
|
|
+ }
|
|
+
|
|
+ protected static int hash(final Object key, final Object value) {
|
|
+ return key.hashCode() ^ (value == null ? 0 : value.hashCode());
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public int hashCode() {
|
|
+ return hash(this.key, this.getValueAcquire());
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public boolean equals(final Object obj) {
|
|
+ if (this == obj) {
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ if (!(obj instanceof Map.Entry)) {
|
|
+ return false;
|
|
+ }
|
|
+ final Map.Entry<?, ?> other = (Map.Entry<?, ?>)obj;
|
|
+ final Object otherKey = other.getKey();
|
|
+ final Object otherValue = other.getValue();
|
|
+
|
|
+ final K thisKey = this.getKey();
|
|
+ final V thisVal = this.getValueAcquire();
|
|
+ return (thisKey == otherKey || thisKey.equals(otherKey)) &&
|
|
+ (thisVal == otherValue || thisVal.equals(otherValue));
|
|
+ }
|
|
+ }
|
|
+
|
|
+
|
|
+ protected static abstract class TableEntryIterator<K, V, T> implements Iterator<T> {
|
|
+
|
|
+ protected final TableEntry<K, V>[] table;
|
|
+ protected final SWMRHashTable<K, V> map;
|
|
+
|
|
+ /* bin which our current element resides on */
|
|
+ protected int tableIndex;
|
|
+
|
|
+ protected TableEntry<K, V> currEntry; /* curr entry, null if no more to iterate or if curr was removed or if we've just init'd */
|
|
+ protected TableEntry<K, V> nextEntry; /* may not be on the same bin as currEntry */
|
|
+
|
|
+ protected TableEntryIterator(final TableEntry<K, V>[] table, final SWMRHashTable<K, V> map) {
|
|
+ this.table = table;
|
|
+ this.map = map;
|
|
+ int tableIndex = 0;
|
|
+ for (int len = table.length; tableIndex < len; ++tableIndex) {
|
|
+ final TableEntry<K, V> entry = ArrayUtil.getOpaque(table, tableIndex);
|
|
+ if (entry != null) {
|
|
+ this.nextEntry = entry;
|
|
+ this.tableIndex = tableIndex + 1;
|
|
+ return;
|
|
+ }
|
|
+ }
|
|
+ this.tableIndex = tableIndex;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean hasNext() {
|
|
+ return this.nextEntry != null;
|
|
+ }
|
|
+
|
|
+ protected final TableEntry<K, V> advanceEntry() {
|
|
+ final TableEntry<K, V>[] table = this.table;
|
|
+ final int tableLength = table.length;
|
|
+ int tableIndex = this.tableIndex;
|
|
+ final TableEntry<K, V> curr = this.nextEntry;
|
|
+ if (curr == null) {
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ this.currEntry = curr;
|
|
+
|
|
+ // set up nextEntry
|
|
+
|
|
+ // find next in chain
|
|
+ TableEntry<K, V> next = curr.getNextOpaque();
|
|
+
|
|
+ if (next != null) {
|
|
+ this.nextEntry = next;
|
|
+ return curr;
|
|
+ }
|
|
+
|
|
+ // nothing in chain, so find next available bin
|
|
+ for (;tableIndex < tableLength; ++tableIndex) {
|
|
+ next = ArrayUtil.getOpaque(table, tableIndex);
|
|
+ if (next != null) {
|
|
+ this.nextEntry = next;
|
|
+ this.tableIndex = tableIndex + 1;
|
|
+ return curr;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ this.nextEntry = null;
|
|
+ this.tableIndex = tableIndex;
|
|
+ return curr;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void remove() {
|
|
+ final TableEntry<K, V> curr = this.currEntry;
|
|
+ if (curr == null) {
|
|
+ throw new IllegalStateException();
|
|
+ }
|
|
+
|
|
+ this.map.remove(curr.key, curr.hash);
|
|
+
|
|
+ this.currEntry = null;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ protected static final class ValueIterator<K, V> extends TableEntryIterator<K, V, V> {
|
|
+
|
|
+ protected ValueIterator(final TableEntry<K, V>[] table, final SWMRHashTable<K, V> map) {
|
|
+ super(table, map);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public V next() {
|
|
+ final TableEntry<K, V> entry = this.advanceEntry();
|
|
+
|
|
+ if (entry == null) {
|
|
+ throw new NoSuchElementException();
|
|
+ }
|
|
+
|
|
+ return entry.getValueAcquire();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ protected static final class KeyIterator<K, V> extends TableEntryIterator<K, V, K> {
|
|
+
|
|
+ protected KeyIterator(final TableEntry<K, V>[] table, final SWMRHashTable<K, V> map) {
|
|
+ super(table, map);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public K next() {
|
|
+ final TableEntry<K, V> curr = this.advanceEntry();
|
|
+
|
|
+ if (curr == null) {
|
|
+ throw new NoSuchElementException();
|
|
+ }
|
|
+
|
|
+ return curr.key;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ protected static final class EntryIterator<K, V> extends TableEntryIterator<K, V, Map.Entry<K, V>> {
|
|
+
|
|
+ protected EntryIterator(final TableEntry<K, V>[] table, final SWMRHashTable<K, V> map) {
|
|
+ super(table, map);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public Map.Entry<K, V> next() {
|
|
+ final TableEntry<K, V> curr = this.advanceEntry();
|
|
+
|
|
+ if (curr == null) {
|
|
+ throw new NoSuchElementException();
|
|
+ }
|
|
+
|
|
+ return curr;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ protected static abstract class ViewCollection<K, V, T> implements Collection<T> {
|
|
+
|
|
+ protected final SWMRHashTable<K, V> map;
|
|
+
|
|
+ protected ViewCollection(final SWMRHashTable<K, V> map) {
|
|
+ this.map = map;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean add(final T element) {
|
|
+ throw new UnsupportedOperationException();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean addAll(final Collection<? extends T> collections) {
|
|
+ throw new UnsupportedOperationException();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean removeAll(final Collection<?> collection) {
|
|
+ Validate.notNull(collection, "Null collection");
|
|
+
|
|
+ boolean modified = false;
|
|
+ for (final Object element : collection) {
|
|
+ modified |= this.remove(element);
|
|
+ }
|
|
+ return modified;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public int size() {
|
|
+ return this.map.size();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean isEmpty() {
|
|
+ return this.size() == 0;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void clear() {
|
|
+ this.map.clear();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean containsAll(final Collection<?> collection) {
|
|
+ Validate.notNull(collection, "Null collection");
|
|
+
|
|
+ for (final Object element : collection) {
|
|
+ if (!this.contains(element)) {
|
|
+ return false;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public Object[] toArray() {
|
|
+ final List<T> list = new ArrayList<>(this.size());
|
|
+
|
|
+ this.forEach(list::add);
|
|
+
|
|
+ return list.toArray();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public <E> E[] toArray(final E[] array) {
|
|
+ final List<T> list = new ArrayList<>(this.size());
|
|
+
|
|
+ this.forEach(list::add);
|
|
+
|
|
+ return list.toArray(array);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public <E> E[] toArray(final IntFunction<E[]> generator) {
|
|
+ final List<T> list = new ArrayList<>(this.size());
|
|
+
|
|
+ this.forEach(list::add);
|
|
+
|
|
+ return list.toArray(generator);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public int hashCode() {
|
|
+ int hash = 0;
|
|
+ for (final T element : this) {
|
|
+ hash += element == null ? 0 : element.hashCode();
|
|
+ }
|
|
+ return hash;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public Spliterator<T> spliterator() { // TODO implement
|
|
+ return Spliterators.spliterator(this, Spliterator.NONNULL);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ protected static abstract class ViewSet<K, V, T> extends ViewCollection<K, V, T> implements Set<T> {
|
|
+
|
|
+ protected ViewSet(final SWMRHashTable<K, V> map) {
|
|
+ super(map);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean equals(final Object obj) {
|
|
+ if (this == obj) {
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ if (!(obj instanceof Set)) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ final Set<?> other = (Set<?>)obj;
|
|
+ if (other.size() != this.size()) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ return this.containsAll(other);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ protected static final class EntrySet<K, V> extends ViewSet<K, V, Map.Entry<K, V>> implements Set<Map.Entry<K, V>> {
|
|
+
|
|
+ protected EntrySet(final SWMRHashTable<K, V> map) {
|
|
+ super(map);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean remove(final Object object) {
|
|
+ if (!(object instanceof Map.Entry<?, ?>)) {
|
|
+ return false;
|
|
+ }
|
|
+ final Map.Entry<?, ?> entry = (Map.Entry<?, ?>)object;
|
|
+
|
|
+ final Object key;
|
|
+ final Object value;
|
|
+
|
|
+ try {
|
|
+ key = entry.getKey();
|
|
+ value = entry.getValue();
|
|
+ } catch (final IllegalStateException ex) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ return this.map.remove(key, value);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean removeIf(final Predicate<? super Map.Entry<K, V>> filter) {
|
|
+ Validate.notNull(filter, "Null filter");
|
|
+
|
|
+ return this.map.removeEntryIf(filter) != 0;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean retainAll(final Collection<?> collection) {
|
|
+ Validate.notNull(collection, "Null collection");
|
|
+
|
|
+ return this.map.removeEntryIf((final Map.Entry<K, V> entry) -> {
|
|
+ return !collection.contains(entry);
|
|
+ }) != 0;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public Iterator<Entry<K, V>> iterator() {
|
|
+ return new EntryIterator<>(this.map.getTableAcquire(), this.map);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void forEach(final Consumer<? super Entry<K, V>> action) {
|
|
+ this.map.forEach(action);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean contains(final Object object) {
|
|
+ if (!(object instanceof Map.Entry)) {
|
|
+ return false;
|
|
+ }
|
|
+ final Map.Entry<?, ?> entry = (Map.Entry<?, ?>)object;
|
|
+
|
|
+ final Object key;
|
|
+ final Object value;
|
|
+
|
|
+ try {
|
|
+ key = entry.getKey();
|
|
+ value = entry.getValue();
|
|
+ } catch (final IllegalStateException ex) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ return this.map.contains(key, value);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public String toString() {
|
|
+ return CollectionUtil.toString(this, "SWMRHashTableEntrySet");
|
|
+ }
|
|
+ }
|
|
+
|
|
+ protected static final class KeySet<K, V> extends ViewSet<K, V, K> {
|
|
+
|
|
+ protected KeySet(final SWMRHashTable<K, V> map) {
|
|
+ super(map);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public Iterator<K> iterator() {
|
|
+ return new KeyIterator<>(this.map.getTableAcquire(), this.map);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void forEach(final Consumer<? super K> action) {
|
|
+ Validate.notNull(action, "Null action");
|
|
+
|
|
+ this.map.forEachKey(action);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean contains(final Object key) {
|
|
+ Validate.notNull(key, "Null key");
|
|
+
|
|
+ return this.map.containsKey(key);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean remove(final Object key) {
|
|
+ Validate.notNull(key, "Null key");
|
|
+
|
|
+ return this.map.remove(key) != null;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean retainAll(final Collection<?> collection) {
|
|
+ Validate.notNull(collection, "Null collection");
|
|
+
|
|
+ return this.map.removeIf((final K key, final V value) -> {
|
|
+ return !collection.contains(key);
|
|
+ }) != 0;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean removeIf(final Predicate<? super K> filter) {
|
|
+ Validate.notNull(filter, "Null filter");
|
|
+
|
|
+ return this.map.removeIf((final K key, final V value) -> {
|
|
+ return filter.test(key);
|
|
+ }) != 0;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public String toString() {
|
|
+ return CollectionUtil.toString(this, "SWMRHashTableKeySet");
|
|
+ }
|
|
+ }
|
|
+
|
|
+ protected static final class ValueCollection<K, V> extends ViewSet<K, V, V> implements Collection<V> {
|
|
+
|
|
+ protected ValueCollection(final SWMRHashTable<K, V> map) {
|
|
+ super(map);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public Iterator<V> iterator() {
|
|
+ return new ValueIterator<>(this.map.getTableAcquire(), this.map);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void forEach(final Consumer<? super V> action) {
|
|
+ Validate.notNull(action, "Null action");
|
|
+
|
|
+ this.map.forEachValue(action);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean contains(final Object object) {
|
|
+ Validate.notNull(object, "Null object");
|
|
+
|
|
+ return this.map.containsValue(object);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean remove(final Object object) {
|
|
+ Validate.notNull(object, "Null object");
|
|
+
|
|
+ final Iterator<V> itr = this.iterator();
|
|
+ while (itr.hasNext()) {
|
|
+ final V val = itr.next();
|
|
+ if (val == object || val.equals(object)) {
|
|
+ itr.remove();
|
|
+ return true;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean removeIf(final Predicate<? super V> filter) {
|
|
+ Validate.notNull(filter, "Null filter");
|
|
+
|
|
+ return this.map.removeIf((final K key, final V value) -> {
|
|
+ return filter.test(value);
|
|
+ }) != 0;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean retainAll(final Collection<?> collection) {
|
|
+ Validate.notNull(collection, "Null collection");
|
|
+
|
|
+ return this.map.removeIf((final K key, final V value) -> {
|
|
+ return !collection.contains(value);
|
|
+ }) != 0;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public String toString() {
|
|
+ return CollectionUtil.toString(this, "SWMRHashTableValues");
|
|
+ }
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRLong2ObjectHashTable.java b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRLong2ObjectHashTable.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..1e98f778ffa0a7bb00ebccaaa8bde075183e41f0
|
|
--- /dev/null
|
|
+++ b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRLong2ObjectHashTable.java
|
|
@@ -0,0 +1,672 @@
|
|
+package ca.spottedleaf.concurrentutil.map;
|
|
+
|
|
+import ca.spottedleaf.concurrentutil.util.ArrayUtil;
|
|
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
|
|
+import ca.spottedleaf.concurrentutil.util.Validate;
|
|
+import io.papermc.paper.util.IntegerUtil;
|
|
+import java.lang.invoke.VarHandle;
|
|
+import java.util.Arrays;
|
|
+import java.util.function.Consumer;
|
|
+import java.util.function.LongConsumer;
|
|
+
|
|
+// trimmed down version of SWMRHashTable
|
|
+public class SWMRLong2ObjectHashTable<V> {
|
|
+
|
|
+ protected int size;
|
|
+
|
|
+ protected TableEntry<V>[] table;
|
|
+
|
|
+ protected final float loadFactor;
|
|
+
|
|
+ protected static final VarHandle SIZE_HANDLE = ConcurrentUtil.getVarHandle(SWMRLong2ObjectHashTable.class, "size", int.class);
|
|
+ protected static final VarHandle TABLE_HANDLE = ConcurrentUtil.getVarHandle(SWMRLong2ObjectHashTable.class, "table", TableEntry[].class);
|
|
+
|
|
+ /* size */
|
|
+
|
|
+ protected final int getSizePlain() {
|
|
+ return (int)SIZE_HANDLE.get(this);
|
|
+ }
|
|
+
|
|
+ protected final int getSizeOpaque() {
|
|
+ return (int)SIZE_HANDLE.getOpaque(this);
|
|
+ }
|
|
+
|
|
+ protected final int getSizeAcquire() {
|
|
+ return (int)SIZE_HANDLE.getAcquire(this);
|
|
+ }
|
|
+
|
|
+ protected final void setSizePlain(final int value) {
|
|
+ SIZE_HANDLE.set(this, value);
|
|
+ }
|
|
+
|
|
+ protected final void setSizeOpaque(final int value) {
|
|
+ SIZE_HANDLE.setOpaque(this, value);
|
|
+ }
|
|
+
|
|
+ protected final void setSizeRelease(final int value) {
|
|
+ SIZE_HANDLE.setRelease(this, value);
|
|
+ }
|
|
+
|
|
+ /* table */
|
|
+
|
|
+ protected final TableEntry<V>[] getTablePlain() {
|
|
+ //noinspection unchecked
|
|
+ return (TableEntry<V>[])TABLE_HANDLE.get(this);
|
|
+ }
|
|
+
|
|
+ protected final TableEntry<V>[] getTableAcquire() {
|
|
+ //noinspection unchecked
|
|
+ return (TableEntry<V>[])TABLE_HANDLE.getAcquire(this);
|
|
+ }
|
|
+
|
|
+ protected final void setTablePlain(final TableEntry<V>[] table) {
|
|
+ TABLE_HANDLE.set(this, table);
|
|
+ }
|
|
+
|
|
+ protected final void setTableRelease(final TableEntry<V>[] table) {
|
|
+ TABLE_HANDLE.setRelease(this, table);
|
|
+ }
|
|
+
|
|
+ protected static final int DEFAULT_CAPACITY = 16;
|
|
+ protected static final float DEFAULT_LOAD_FACTOR = 0.75f;
|
|
+ protected static final int MAXIMUM_CAPACITY = Integer.MIN_VALUE >>> 1;
|
|
+
|
|
+ /**
|
|
+ * Constructs this map with a capacity of {@code 16} and load factor of {@code 0.75f}.
|
|
+ */
|
|
+ public SWMRLong2ObjectHashTable() {
|
|
+ this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Constructs this map with the specified capacity and load factor of {@code 0.75f}.
|
|
+ * @param capacity specified initial capacity, > 0
|
|
+ */
|
|
+ public SWMRLong2ObjectHashTable(final int capacity) {
|
|
+ this(capacity, DEFAULT_LOAD_FACTOR);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Constructs this map with the specified capacity and load factor.
|
|
+ * @param capacity specified capacity, > 0
|
|
+ * @param loadFactor specified load factor, > 0 && finite
|
|
+ */
|
|
+ public SWMRLong2ObjectHashTable(final int capacity, final float loadFactor) {
|
|
+ final int tableSize = getCapacityFor(capacity);
|
|
+
|
|
+ if (loadFactor <= 0.0 || !Float.isFinite(loadFactor)) {
|
|
+ throw new IllegalArgumentException("Invalid load factor: " + loadFactor);
|
|
+ }
|
|
+
|
|
+ //noinspection unchecked
|
|
+ final TableEntry<V>[] table = new TableEntry[tableSize];
|
|
+ this.setTablePlain(table);
|
|
+
|
|
+ if (tableSize == MAXIMUM_CAPACITY) {
|
|
+ this.threshold = -1;
|
|
+ } else {
|
|
+ this.threshold = getTargetCapacity(tableSize, loadFactor);
|
|
+ }
|
|
+
|
|
+ this.loadFactor = loadFactor;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Constructs this map with a capacity of {@code 16} or the specified map's size, whichever is larger, and
|
|
+ * with a load factor of {@code 0.75f}.
|
|
+ * All of the specified map's entries are copied into this map.
|
|
+ * @param other The specified map.
|
|
+ */
|
|
+ public SWMRLong2ObjectHashTable(final SWMRLong2ObjectHashTable<V> other) {
|
|
+ this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR, other);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Constructs this map with a minimum capacity of the specified capacity or the specified map's size, whichever is larger, and
|
|
+ * with a load factor of {@code 0.75f}.
|
|
+ * All of the specified map's entries are copied into this map.
|
|
+ * @param capacity specified capacity, > 0
|
|
+ * @param other The specified map.
|
|
+ */
|
|
+ public SWMRLong2ObjectHashTable(final int capacity, final SWMRLong2ObjectHashTable<V> other) {
|
|
+ this(capacity, DEFAULT_LOAD_FACTOR, other);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Constructs this map with a min capacity of the specified capacity or the specified map's size, whichever is larger, and
|
|
+ * with the specified load factor.
|
|
+ * All of the specified map's entries are copied into this map.
|
|
+ * @param capacity specified capacity, > 0
|
|
+ * @param loadFactor specified load factor, > 0 && finite
|
|
+ * @param other The specified map.
|
|
+ */
|
|
+ public SWMRLong2ObjectHashTable(final int capacity, final float loadFactor, final SWMRLong2ObjectHashTable<V> other) {
|
|
+ this(Math.max(Validate.notNull(other, "Null map").size(), capacity), loadFactor);
|
|
+ this.putAll(other);
|
|
+ }
|
|
+
|
|
+ public final float getLoadFactor() {
|
|
+ return this.loadFactor;
|
|
+ }
|
|
+
|
|
+ protected static int getCapacityFor(final int capacity) {
|
|
+ if (capacity <= 0) {
|
|
+ throw new IllegalArgumentException("Invalid capacity: " + capacity);
|
|
+ }
|
|
+ if (capacity >= MAXIMUM_CAPACITY) {
|
|
+ return MAXIMUM_CAPACITY;
|
|
+ }
|
|
+ return IntegerUtil.roundCeilLog2(capacity);
|
|
+ }
|
|
+
|
|
+ /** Callers must still use acquire when reading the value of the entry. */
|
|
+ protected final TableEntry<V> getEntryForOpaque(final long key) {
|
|
+ final int hash = SWMRLong2ObjectHashTable.getHash(key);
|
|
+ final TableEntry<V>[] table = this.getTableAcquire();
|
|
+
|
|
+ for (TableEntry<V> curr = ArrayUtil.getOpaque(table, hash & (table.length - 1)); curr != null; curr = curr.getNextOpaque()) {
|
|
+ if (key == curr.key) {
|
|
+ return curr;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ protected final TableEntry<V> getEntryForPlain(final long key) {
|
|
+ final int hash = SWMRLong2ObjectHashTable.getHash(key);
|
|
+ final TableEntry<V>[] table = this.getTablePlain();
|
|
+
|
|
+ for (TableEntry<V> curr = table[hash & (table.length - 1)]; curr != null; curr = curr.getNextPlain()) {
|
|
+ if (key == curr.key) {
|
|
+ return curr;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ /* MT-Safe */
|
|
+
|
|
+ /** must be deterministic given a key */
|
|
+ protected static int getHash(final long key) {
|
|
+ return (int)it.unimi.dsi.fastutil.HashCommon.mix(key);
|
|
+ }
|
|
+
|
|
+ // rets -1 if capacity*loadFactor is too large
|
|
+ protected static int getTargetCapacity(final int capacity, final float loadFactor) {
|
|
+ final double ret = (double)capacity * (double)loadFactor;
|
|
+ if (Double.isInfinite(ret) || ret >= ((double)Integer.MAX_VALUE)) {
|
|
+ return -1;
|
|
+ }
|
|
+
|
|
+ return (int)ret;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public boolean equals(final Object obj) {
|
|
+ if (this == obj) {
|
|
+ return true;
|
|
+ }
|
|
+ /* Make no attempt to deal with concurrent modifications */
|
|
+ if (!(obj instanceof SWMRLong2ObjectHashTable)) {
|
|
+ return false;
|
|
+ }
|
|
+ final SWMRLong2ObjectHashTable<?> other = (SWMRLong2ObjectHashTable<?>)obj;
|
|
+
|
|
+ if (this.size() != other.size()) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ final TableEntry<V>[] table = this.getTableAcquire();
|
|
+
|
|
+ for (int i = 0, len = table.length; i < len; ++i) {
|
|
+ for (TableEntry<V> curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
|
|
+ final V value = curr.getValueAcquire();
|
|
+
|
|
+ final Object otherValue = other.get(curr.key);
|
|
+ if (otherValue == null || (value != otherValue && value.equals(otherValue))) {
|
|
+ return false;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public int hashCode() {
|
|
+ /* Make no attempt to deal with concurrent modifications */
|
|
+ int hash = 0;
|
|
+ final TableEntry<V>[] table = this.getTableAcquire();
|
|
+
|
|
+ for (int i = 0, len = table.length; i < len; ++i) {
|
|
+ for (TableEntry<V> curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
|
|
+ hash += curr.hashCode();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return hash;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public String toString() {
|
|
+ final StringBuilder builder = new StringBuilder(64);
|
|
+ builder.append("SingleWriterMultiReaderHashMap:{");
|
|
+
|
|
+ this.forEach((final long key, final V value) -> {
|
|
+ builder.append("{key: \"").append(key).append("\", value: \"").append(value).append("\"}");
|
|
+ });
|
|
+
|
|
+ return builder.append('}').toString();
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public SWMRLong2ObjectHashTable<V> clone() {
|
|
+ return new SWMRLong2ObjectHashTable<>(this.getTableAcquire().length, this.loadFactor, this);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ public void forEach(final Consumer<? super SWMRLong2ObjectHashTable.TableEntry<V>> action) {
|
|
+ Validate.notNull(action, "Null action");
|
|
+
|
|
+ final TableEntry<V>[] table = this.getTableAcquire();
|
|
+ for (int i = 0, len = table.length; i < len; ++i) {
|
|
+ for (TableEntry<V> curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
|
|
+ action.accept(curr);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ @FunctionalInterface
|
|
+ public static interface BiLongObjectConsumer<V> {
|
|
+ public void accept(final long key, final V value);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ public void forEach(final BiLongObjectConsumer<? super V> action) {
|
|
+ Validate.notNull(action, "Null action");
|
|
+
|
|
+ final TableEntry<V>[] table = this.getTableAcquire();
|
|
+ for (int i = 0, len = table.length; i < len; ++i) {
|
|
+ for (TableEntry<V> curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
|
|
+ final V value = curr.getValueAcquire();
|
|
+
|
|
+ action.accept(curr.key, value);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Provides the specified consumer with all keys contained within this map.
|
|
+ * @param action The specified consumer.
|
|
+ */
|
|
+ public void forEachKey(final LongConsumer action) {
|
|
+ Validate.notNull(action, "Null action");
|
|
+
|
|
+ final TableEntry<V>[] table = this.getTableAcquire();
|
|
+ for (int i = 0, len = table.length; i < len; ++i) {
|
|
+ for (TableEntry<V> curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
|
|
+ action.accept(curr.key);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Provides the specified consumer with all values contained within this map. Equivalent to {@code map.values().forEach(Consumer)}.
|
|
+ * @param action The specified consumer.
|
|
+ */
|
|
+ public void forEachValue(final Consumer<? super V> action) {
|
|
+ Validate.notNull(action, "Null action");
|
|
+
|
|
+ final TableEntry<V>[] table = this.getTableAcquire();
|
|
+ for (int i = 0, len = table.length; i < len; ++i) {
|
|
+ for (TableEntry<V> curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
|
|
+ final V value = curr.getValueAcquire();
|
|
+
|
|
+ action.accept(value);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ public V get(final long key) {
|
|
+ final TableEntry<V> entry = this.getEntryForOpaque(key);
|
|
+ return entry == null ? null : entry.getValueAcquire();
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ public boolean containsKey(final long key) {
|
|
+ // note: we need to use getValueAcquire, so that the reads from this map are ordered by acquire semantics
|
|
+ return this.get(key) != null;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ public V getOrDefault(final long key, final V defaultValue) {
|
|
+ final TableEntry<V> entry = this.getEntryForOpaque(key);
|
|
+
|
|
+ return entry == null ? defaultValue : entry.getValueAcquire();
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ public int size() {
|
|
+ return this.getSizeAcquire();
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ public boolean isEmpty() {
|
|
+ return this.getSizeAcquire() == 0;
|
|
+ }
|
|
+
|
|
+ /* Non-MT-Safe */
|
|
+
|
|
+ protected int threshold;
|
|
+
|
|
+ protected final void checkResize(final int minCapacity) {
|
|
+ if (minCapacity <= this.threshold || this.threshold < 0) {
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ final TableEntry<V>[] table = this.getTablePlain();
|
|
+ int newCapacity = minCapacity >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY : IntegerUtil.roundCeilLog2(minCapacity);
|
|
+ if (newCapacity < 0) {
|
|
+ newCapacity = MAXIMUM_CAPACITY;
|
|
+ }
|
|
+ if (newCapacity <= table.length) {
|
|
+ if (newCapacity == MAXIMUM_CAPACITY) {
|
|
+ return;
|
|
+ }
|
|
+ newCapacity = table.length << 1;
|
|
+ }
|
|
+
|
|
+ //noinspection unchecked
|
|
+ final TableEntry<V>[] newTable = new TableEntry[newCapacity];
|
|
+ final int indexMask = newCapacity - 1;
|
|
+
|
|
+ for (int i = 0, len = table.length; i < len; ++i) {
|
|
+ for (TableEntry<V> entry = table[i]; entry != null; entry = entry.getNextPlain()) {
|
|
+ final long key = entry.key;
|
|
+ final int hash = SWMRLong2ObjectHashTable.getHash(key);
|
|
+ final int index = hash & indexMask;
|
|
+
|
|
+ /* we need to create a new entry since there could be reading threads */
|
|
+ final TableEntry<V> insert = new TableEntry<>(key, entry.getValuePlain());
|
|
+
|
|
+ final TableEntry<V> prev = newTable[index];
|
|
+
|
|
+ newTable[index] = insert;
|
|
+ insert.setNextPlain(prev);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (newCapacity == MAXIMUM_CAPACITY) {
|
|
+ this.threshold = -1; /* No more resizing */
|
|
+ } else {
|
|
+ this.threshold = getTargetCapacity(newCapacity, this.loadFactor);
|
|
+ }
|
|
+ this.setTableRelease(newTable); /* use release to publish entries in table */
|
|
+ }
|
|
+
|
|
+ protected final int addToSize(final int num) {
|
|
+ final int newSize = this.getSizePlain() + num;
|
|
+
|
|
+ this.setSizeOpaque(newSize);
|
|
+ this.checkResize(newSize);
|
|
+
|
|
+ return newSize;
|
|
+ }
|
|
+
|
|
+ protected final int removeFromSize(final int num) {
|
|
+ final int newSize = this.getSizePlain() - num;
|
|
+
|
|
+ this.setSizeOpaque(newSize);
|
|
+
|
|
+ return newSize;
|
|
+ }
|
|
+
|
|
+ protected final V put(final long key, final V value, final boolean onlyIfAbsent) {
|
|
+ final TableEntry<V>[] table = this.getTablePlain();
|
|
+ final int hash = SWMRLong2ObjectHashTable.getHash(key);
|
|
+ final int index = hash & (table.length - 1);
|
|
+
|
|
+ final TableEntry<V> head = table[index];
|
|
+ if (head == null) {
|
|
+ final TableEntry<V> insert = new TableEntry<>(key, value);
|
|
+ ArrayUtil.setRelease(table, index, insert);
|
|
+ this.addToSize(1);
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ for (TableEntry<V> curr = head;;) {
|
|
+ if (key == curr.key) {
|
|
+ if (onlyIfAbsent) {
|
|
+ return curr.getValuePlain();
|
|
+ }
|
|
+
|
|
+ final V currVal = curr.getValuePlain();
|
|
+ curr.setValueRelease(value);
|
|
+ return currVal;
|
|
+ }
|
|
+
|
|
+ final TableEntry<V> next = curr.getNextPlain();
|
|
+ if (next != null) {
|
|
+ curr = next;
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ final TableEntry<V> insert = new TableEntry<>(key, value);
|
|
+
|
|
+ curr.setNextRelease(insert);
|
|
+ this.addToSize(1);
|
|
+ return null;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ public V put(final long key, final V value) {
|
|
+ Validate.notNull(value, "Null value");
|
|
+
|
|
+ return this.put(key, value, false);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ public V putIfAbsent(final long key, final V value) {
|
|
+ Validate.notNull(value, "Null value");
|
|
+
|
|
+ return this.put(key, value, true);
|
|
+ }
|
|
+
|
|
+ protected final V remove(final long key, final int hash) {
|
|
+ final TableEntry<V>[] table = this.getTablePlain();
|
|
+ final int index = (table.length - 1) & hash;
|
|
+
|
|
+ final TableEntry<V> head = table[index];
|
|
+ if (head == null) {
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ if (head.key == key) {
|
|
+ ArrayUtil.setRelease(table, index, head.getNextPlain());
|
|
+ this.removeFromSize(1);
|
|
+
|
|
+ return head.getValuePlain();
|
|
+ }
|
|
+
|
|
+ for (TableEntry<V> curr = head.getNextPlain(), prev = head; curr != null; prev = curr, curr = curr.getNextPlain()) {
|
|
+ if (key == curr.key) {
|
|
+ prev.setNextRelease(curr.getNextPlain());
|
|
+ this.removeFromSize(1);
|
|
+
|
|
+ return curr.getValuePlain();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ public V remove(final long key) {
|
|
+ return this.remove(key, SWMRLong2ObjectHashTable.getHash(key));
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ public void putAll(final SWMRLong2ObjectHashTable<? extends V> map) {
|
|
+ Validate.notNull(map, "Null map");
|
|
+
|
|
+ final int size = map.size();
|
|
+ this.checkResize(Math.max(this.getSizePlain() + size/2, size)); /* preemptively resize */
|
|
+ map.forEach(this::put);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ * <p>
|
|
+ * This call is non-atomic and the order that which entries are removed is undefined. The clear operation itself
|
|
+ * is release ordered, that is, after the clear operation is performed a release fence is performed.
|
|
+ * </p>
|
|
+ */
|
|
+ public void clear() {
|
|
+ Arrays.fill(this.getTablePlain(), null);
|
|
+ this.setSizeRelease(0);
|
|
+ }
|
|
+
|
|
+ public static final class TableEntry<V> {
|
|
+
|
|
+ protected final long key;
|
|
+ protected V value;
|
|
+
|
|
+ protected TableEntry<V> next;
|
|
+
|
|
+ protected static final VarHandle VALUE_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "value", Object.class);
|
|
+ protected static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "next", TableEntry.class);
|
|
+
|
|
+ /* value */
|
|
+
|
|
+ protected final V getValuePlain() {
|
|
+ //noinspection unchecked
|
|
+ return (V)VALUE_HANDLE.get(this);
|
|
+ }
|
|
+
|
|
+ protected final V getValueAcquire() {
|
|
+ //noinspection unchecked
|
|
+ return (V)VALUE_HANDLE.getAcquire(this);
|
|
+ }
|
|
+
|
|
+ protected final void setValueRelease(final V to) {
|
|
+ VALUE_HANDLE.setRelease(this, to);
|
|
+ }
|
|
+
|
|
+ /* next */
|
|
+
|
|
+ protected final TableEntry<V> getNextPlain() {
|
|
+ //noinspection unchecked
|
|
+ return (TableEntry<V>)NEXT_HANDLE.get(this);
|
|
+ }
|
|
+
|
|
+ protected final TableEntry<V> getNextOpaque() {
|
|
+ //noinspection unchecked
|
|
+ return (TableEntry<V>)NEXT_HANDLE.getOpaque(this);
|
|
+ }
|
|
+
|
|
+ protected final void setNextPlain(final TableEntry<V> next) {
|
|
+ NEXT_HANDLE.set(this, next);
|
|
+ }
|
|
+
|
|
+ protected final void setNextRelease(final TableEntry<V> next) {
|
|
+ NEXT_HANDLE.setRelease(this, next);
|
|
+ }
|
|
+
|
|
+ protected TableEntry(final long key, final V value) {
|
|
+ this.key = key;
|
|
+ this.value = value;
|
|
+ }
|
|
+
|
|
+ public long getKey() {
|
|
+ return this.key;
|
|
+ }
|
|
+
|
|
+ public V getValue() {
|
|
+ return this.getValueAcquire();
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ public V setValue(final V value) {
|
|
+ if (value == null) {
|
|
+ throw new NullPointerException();
|
|
+ }
|
|
+
|
|
+ final V curr = this.getValuePlain();
|
|
+
|
|
+ this.setValueRelease(value);
|
|
+ return curr;
|
|
+ }
|
|
+
|
|
+ protected static int hash(final long key, final Object value) {
|
|
+ return SWMRLong2ObjectHashTable.getHash(key) ^ (value == null ? 0 : value.hashCode());
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public int hashCode() {
|
|
+ return hash(this.key, this.getValueAcquire());
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public boolean equals(final Object obj) {
|
|
+ if (this == obj) {
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ if (!(obj instanceof TableEntry<?>)) {
|
|
+ return false;
|
|
+ }
|
|
+ final TableEntry<?> other = (TableEntry<?>)obj;
|
|
+ final long otherKey = other.getKey();
|
|
+ final long thisKey = this.getKey();
|
|
+ final Object otherValue = other.getValueAcquire();
|
|
+ final V thisVal = this.getValueAcquire();
|
|
+ return (thisKey == otherKey) && (thisVal == otherValue || thisVal.equals(otherValue));
|
|
+ }
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/ArrayUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/ArrayUtil.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..ebb1ab06165addb173fea4d295001fe37f4e79d3
|
|
--- /dev/null
|
|
+++ b/src/main/java/ca/spottedleaf/concurrentutil/util/ArrayUtil.java
|
|
@@ -0,0 +1,816 @@
|
|
+package ca.spottedleaf.concurrentutil.util;
|
|
+
|
|
+import java.lang.invoke.VarHandle;
|
|
+
|
|
+public final class ArrayUtil {
|
|
+
|
|
+ public static final VarHandle BOOLEAN_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(boolean[].class);
|
|
+
|
|
+ public static final VarHandle BYTE_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(byte[].class);
|
|
+
|
|
+ public static final VarHandle SHORT_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(short[].class);
|
|
+
|
|
+ public static final VarHandle INT_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(int[].class);
|
|
+
|
|
+ public static final VarHandle LONG_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(long[].class);
|
|
+
|
|
+ public static final VarHandle OBJECT_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(Object[].class);
|
|
+
|
|
+ private ArrayUtil() {
|
|
+ throw new RuntimeException();
|
|
+ }
|
|
+
|
|
+ /* byte array */
|
|
+
|
|
+ public static byte getPlain(final byte[] array, final int index) {
|
|
+ return (byte)BYTE_ARRAY_HANDLE.get(array, index);
|
|
+ }
|
|
+
|
|
+ public static byte getOpaque(final byte[] array, final int index) {
|
|
+ return (byte)BYTE_ARRAY_HANDLE.getOpaque(array, index);
|
|
+ }
|
|
+
|
|
+ public static byte getAcquire(final byte[] array, final int index) {
|
|
+ return (byte)BYTE_ARRAY_HANDLE.getAcquire(array, index);
|
|
+ }
|
|
+
|
|
+ public static byte getVolatile(final byte[] array, final int index) {
|
|
+ return (byte)BYTE_ARRAY_HANDLE.getVolatile(array, index);
|
|
+ }
|
|
+
|
|
+ public static void setPlain(final byte[] array, final int index, final byte value) {
|
|
+ BYTE_ARRAY_HANDLE.set(array, index, value);
|
|
+ }
|
|
+
|
|
+ public static void setOpaque(final byte[] array, final int index, final byte value) {
|
|
+ BYTE_ARRAY_HANDLE.setOpaque(array, index, value);
|
|
+ }
|
|
+
|
|
+ public static void setRelease(final byte[] array, final int index, final byte value) {
|
|
+ BYTE_ARRAY_HANDLE.setRelease(array, index, value);
|
|
+ }
|
|
+
|
|
+ public static void setVolatile(final byte[] array, final int index, final byte value) {
|
|
+ BYTE_ARRAY_HANDLE.setVolatile(array, index, value);
|
|
+ }
|
|
+
|
|
+ public static void setVolatileContended(final byte[] array, final int index, final byte param) {
|
|
+ int failures = 0;
|
|
+
|
|
+ for (byte curr = getVolatile(array, index);;++failures) {
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+
|
|
+ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
|
|
+ return;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public static byte compareAndExchangeVolatile(final byte[] array, final int index, final byte expect, final byte update) {
|
|
+ return (byte)BYTE_ARRAY_HANDLE.compareAndExchange(array, index, expect, update);
|
|
+ }
|
|
+
|
|
+ public static byte getAndAddVolatile(final byte[] array, final int index, final byte param) {
|
|
+ return (byte)BYTE_ARRAY_HANDLE.getAndAdd(array, index, param);
|
|
+ }
|
|
+
|
|
+ public static byte getAndAndVolatile(final byte[] array, final int index, final byte param) {
|
|
+ return (byte)BYTE_ARRAY_HANDLE.getAndBitwiseAnd(array, index, param);
|
|
+ }
|
|
+
|
|
+ public static byte getAndOrVolatile(final byte[] array, final int index, final byte param) {
|
|
+ return (byte)BYTE_ARRAY_HANDLE.getAndBitwiseOr(array, index, param);
|
|
+ }
|
|
+
|
|
+ public static byte getAndXorVolatile(final byte[] array, final int index, final byte param) {
|
|
+ return (byte)BYTE_ARRAY_HANDLE.getAndBitwiseXor(array, index, param);
|
|
+ }
|
|
+
|
|
+ public static byte getAndSetVolatile(final byte[] array, final int index, final byte param) {
|
|
+ return (byte)BYTE_ARRAY_HANDLE.getAndSet(array, index, param);
|
|
+ }
|
|
+
|
|
+ public static byte compareAndExchangeVolatileContended(final byte[] array, final int index, final byte expect, final byte update) {
|
|
+ return (byte)BYTE_ARRAY_HANDLE.compareAndExchange(array, index, expect, update);
|
|
+ }
|
|
+
|
|
+ public static byte getAndAddVolatileContended(final byte[] array, final int index, final byte param) {
|
|
+ int failures = 0;
|
|
+
|
|
+ for (byte curr = getVolatile(array, index);;++failures) {
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+
|
|
+ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (byte) (curr + param)))) {
|
|
+ return curr;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public static byte getAndAndVolatileContended(final byte[] array, final int index, final byte param) {
|
|
+ int failures = 0;
|
|
+
|
|
+ for (byte curr = getVolatile(array, index);;++failures) {
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+
|
|
+ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (byte) (curr & param)))) {
|
|
+ return curr;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public static byte getAndOrVolatileContended(final byte[] array, final int index, final byte param) {
|
|
+ int failures = 0;
|
|
+
|
|
+ for (byte curr = getVolatile(array, index);;++failures) {
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+
|
|
+ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (byte) (curr | param)))) {
|
|
+ return curr;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public static byte getAndXorVolatileContended(final byte[] array, final int index, final byte param) {
|
|
+ int failures = 0;
|
|
+
|
|
+ for (byte curr = getVolatile(array, index);;++failures) {
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+
|
|
+ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (byte) (curr ^ param)))) {
|
|
+ return curr;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public static byte getAndSetVolatileContended(final byte[] array, final int index, final byte param) {
|
|
+ int failures = 0;
|
|
+
|
|
+ for (byte curr = getVolatile(array, index);;++failures) {
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+
|
|
+ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
|
|
+ return curr;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /* short array */
|
|
+
|
|
+ public static short getPlain(final short[] array, final int index) {
|
|
+ return (short)SHORT_ARRAY_HANDLE.get(array, index);
|
|
+ }
|
|
+
|
|
+ public static short getOpaque(final short[] array, final int index) {
|
|
+ return (short)SHORT_ARRAY_HANDLE.getOpaque(array, index);
|
|
+ }
|
|
+
|
|
+ public static short getAcquire(final short[] array, final int index) {
|
|
+ return (short)SHORT_ARRAY_HANDLE.getAcquire(array, index);
|
|
+ }
|
|
+
|
|
+ public static short getVolatile(final short[] array, final int index) {
|
|
+ return (short)SHORT_ARRAY_HANDLE.getVolatile(array, index);
|
|
+ }
|
|
+
|
|
+ public static void setPlain(final short[] array, final int index, final short value) {
|
|
+ SHORT_ARRAY_HANDLE.set(array, index, value);
|
|
+ }
|
|
+
|
|
+ public static void setOpaque(final short[] array, final int index, final short value) {
|
|
+ SHORT_ARRAY_HANDLE.setOpaque(array, index, value);
|
|
+ }
|
|
+
|
|
+ public static void setRelease(final short[] array, final int index, final short value) {
|
|
+ SHORT_ARRAY_HANDLE.setRelease(array, index, value);
|
|
+ }
|
|
+
|
|
+ public static void setVolatile(final short[] array, final int index, final short value) {
|
|
+ SHORT_ARRAY_HANDLE.setVolatile(array, index, value);
|
|
+ }
|
|
+
|
|
+ public static void setVolatileContended(final short[] array, final int index, final short param) {
|
|
+ int failures = 0;
|
|
+
|
|
+ for (short curr = getVolatile(array, index);;++failures) {
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+
|
|
+ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
|
|
+ return;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public static short compareAndExchangeVolatile(final short[] array, final int index, final short expect, final short update) {
|
|
+ return (short)SHORT_ARRAY_HANDLE.compareAndExchange(array, index, expect, update);
|
|
+ }
|
|
+
|
|
+ public static short getAndAddVolatile(final short[] array, final int index, final short param) {
|
|
+ return (short)SHORT_ARRAY_HANDLE.getAndAdd(array, index, param);
|
|
+ }
|
|
+
|
|
+ public static short getAndAndVolatile(final short[] array, final int index, final short param) {
|
|
+ return (short)SHORT_ARRAY_HANDLE.getAndBitwiseAnd(array, index, param);
|
|
+ }
|
|
+
|
|
+ public static short getAndOrVolatile(final short[] array, final int index, final short param) {
|
|
+ return (short)SHORT_ARRAY_HANDLE.getAndBitwiseOr(array, index, param);
|
|
+ }
|
|
+
|
|
+ public static short getAndXorVolatile(final short[] array, final int index, final short param) {
|
|
+ return (short)SHORT_ARRAY_HANDLE.getAndBitwiseXor(array, index, param);
|
|
+ }
|
|
+
|
|
+ public static short getAndSetVolatile(final short[] array, final int index, final short param) {
|
|
+ return (short)SHORT_ARRAY_HANDLE.getAndSet(array, index, param);
|
|
+ }
|
|
+
|
|
+ public static short compareAndExchangeVolatileContended(final short[] array, final int index, final short expect, final short update) {
|
|
+ return (short)SHORT_ARRAY_HANDLE.compareAndExchange(array, index, expect, update);
|
|
+ }
|
|
+
|
|
+ public static short getAndAddVolatileContended(final short[] array, final int index, final short param) {
|
|
+ int failures = 0;
|
|
+
|
|
+ for (short curr = getVolatile(array, index);;++failures) {
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+
|
|
+ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (short) (curr + param)))) {
|
|
+ return curr;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public static short getAndAndVolatileContended(final short[] array, final int index, final short param) {
|
|
+ int failures = 0;
|
|
+
|
|
+ for (short curr = getVolatile(array, index);;++failures) {
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+
|
|
+ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (short) (curr & param)))) {
|
|
+ return curr;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public static short getAndOrVolatileContended(final short[] array, final int index, final short param) {
|
|
+ int failures = 0;
|
|
+
|
|
+ for (short curr = getVolatile(array, index);;++failures) {
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+
|
|
+ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (short) (curr | param)))) {
|
|
+ return curr;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public static short getAndXorVolatileContended(final short[] array, final int index, final short param) {
|
|
+ int failures = 0;
|
|
+
|
|
+ for (short curr = getVolatile(array, index);;++failures) {
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+
|
|
+ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (short) (curr ^ param)))) {
|
|
+ return curr;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public static short getAndSetVolatileContended(final short[] array, final int index, final short param) {
|
|
+ int failures = 0;
|
|
+
|
|
+ for (short curr = getVolatile(array, index);;++failures) {
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+
|
|
+ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
|
|
+ return curr;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /* int array */
|
|
+
|
|
+ public static int getPlain(final int[] array, final int index) {
|
|
+ return (int)INT_ARRAY_HANDLE.get(array, index);
|
|
+ }
|
|
+
|
|
+ public static int getOpaque(final int[] array, final int index) {
|
|
+ return (int)INT_ARRAY_HANDLE.getOpaque(array, index);
|
|
+ }
|
|
+
|
|
+ public static int getAcquire(final int[] array, final int index) {
|
|
+ return (int)INT_ARRAY_HANDLE.getAcquire(array, index);
|
|
+ }
|
|
+
|
|
+ public static int getVolatile(final int[] array, final int index) {
|
|
+ return (int)INT_ARRAY_HANDLE.getVolatile(array, index);
|
|
+ }
|
|
+
|
|
+ public static void setPlain(final int[] array, final int index, final int value) {
|
|
+ INT_ARRAY_HANDLE.set(array, index, value);
|
|
+ }
|
|
+
|
|
+ public static void setOpaque(final int[] array, final int index, final int value) {
|
|
+ INT_ARRAY_HANDLE.setOpaque(array, index, value);
|
|
+ }
|
|
+
|
|
+ public static void setRelease(final int[] array, final int index, final int value) {
|
|
+ INT_ARRAY_HANDLE.setRelease(array, index, value);
|
|
+ }
|
|
+
|
|
+ public static void setVolatile(final int[] array, final int index, final int value) {
|
|
+ INT_ARRAY_HANDLE.setVolatile(array, index, value);
|
|
+ }
|
|
+
|
|
+ public static void setVolatileContended(final int[] array, final int index, final int param) {
|
|
+ int failures = 0;
|
|
+
|
|
+ for (int curr = getVolatile(array, index);;++failures) {
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+
|
|
+ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
|
|
+ return;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public static int compareAndExchangeVolatile(final int[] array, final int index, final int expect, final int update) {
|
|
+ return (int)INT_ARRAY_HANDLE.compareAndExchange(array, index, expect, update);
|
|
+ }
|
|
+
|
|
+ public static int getAndAddVolatile(final int[] array, final int index, final int param) {
|
|
+ return (int)INT_ARRAY_HANDLE.getAndAdd(array, index, param);
|
|
+ }
|
|
+
|
|
+ public static int getAndAndVolatile(final int[] array, final int index, final int param) {
|
|
+ return (int)INT_ARRAY_HANDLE.getAndBitwiseAnd(array, index, param);
|
|
+ }
|
|
+
|
|
+ public static int getAndOrVolatile(final int[] array, final int index, final int param) {
|
|
+ return (int)INT_ARRAY_HANDLE.getAndBitwiseOr(array, index, param);
|
|
+ }
|
|
+
|
|
+ public static int getAndXorVolatile(final int[] array, final int index, final int param) {
|
|
+ return (int)INT_ARRAY_HANDLE.getAndBitwiseXor(array, index, param);
|
|
+ }
|
|
+
|
|
+ public static int getAndSetVolatile(final int[] array, final int index, final int param) {
|
|
+ return (int)INT_ARRAY_HANDLE.getAndSet(array, index, param);
|
|
+ }
|
|
+
|
|
+ public static int compareAndExchangeVolatileContended(final int[] array, final int index, final int expect, final int update) {
|
|
+ return (int)INT_ARRAY_HANDLE.compareAndExchange(array, index, expect, update);
|
|
+ }
|
|
+
|
|
+ public static int getAndAddVolatileContended(final int[] array, final int index, final int param) {
|
|
+ int failures = 0;
|
|
+
|
|
+ for (int curr = getVolatile(array, index);;++failures) {
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+
|
|
+ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (int) (curr + param)))) {
|
|
+ return curr;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public static int getAndAndVolatileContended(final int[] array, final int index, final int param) {
|
|
+ int failures = 0;
|
|
+
|
|
+ for (int curr = getVolatile(array, index);;++failures) {
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+
|
|
+ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (int) (curr & param)))) {
|
|
+ return curr;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public static int getAndOrVolatileContended(final int[] array, final int index, final int param) {
|
|
+ int failures = 0;
|
|
+
|
|
+ for (int curr = getVolatile(array, index);;++failures) {
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+
|
|
+ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (int) (curr | param)))) {
|
|
+ return curr;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public static int getAndXorVolatileContended(final int[] array, final int index, final int param) {
|
|
+ int failures = 0;
|
|
+
|
|
+ for (int curr = getVolatile(array, index);;++failures) {
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+
|
|
+ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (int) (curr ^ param)))) {
|
|
+ return curr;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public static int getAndSetVolatileContended(final int[] array, final int index, final int param) {
|
|
+ int failures = 0;
|
|
+
|
|
+ for (int curr = getVolatile(array, index);;++failures) {
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+
|
|
+ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
|
|
+ return curr;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /* long array */
|
|
+
|
|
+ public static long getPlain(final long[] array, final int index) {
|
|
+ return (long)LONG_ARRAY_HANDLE.get(array, index);
|
|
+ }
|
|
+
|
|
+ public static long getOpaque(final long[] array, final int index) {
|
|
+ return (long)LONG_ARRAY_HANDLE.getOpaque(array, index);
|
|
+ }
|
|
+
|
|
+ public static long getAcquire(final long[] array, final int index) {
|
|
+ return (long)LONG_ARRAY_HANDLE.getAcquire(array, index);
|
|
+ }
|
|
+
|
|
+ public static long getVolatile(final long[] array, final int index) {
|
|
+ return (long)LONG_ARRAY_HANDLE.getVolatile(array, index);
|
|
+ }
|
|
+
|
|
+ public static void setPlain(final long[] array, final int index, final long value) {
|
|
+ LONG_ARRAY_HANDLE.set(array, index, value);
|
|
+ }
|
|
+
|
|
+ public static void setOpaque(final long[] array, final int index, final long value) {
|
|
+ LONG_ARRAY_HANDLE.setOpaque(array, index, value);
|
|
+ }
|
|
+
|
|
+ public static void setRelease(final long[] array, final int index, final long value) {
|
|
+ LONG_ARRAY_HANDLE.setRelease(array, index, value);
|
|
+ }
|
|
+
|
|
+ public static void setVolatile(final long[] array, final int index, final long value) {
|
|
+ LONG_ARRAY_HANDLE.setVolatile(array, index, value);
|
|
+ }
|
|
+
|
|
+ public static void setVolatileContended(final long[] array, final int index, final long param) {
|
|
+ int failures = 0;
|
|
+
|
|
+ for (long curr = getVolatile(array, index);;++failures) {
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+
|
|
+ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
|
|
+ return;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public static long compareAndExchangeVolatile(final long[] array, final int index, final long expect, final long update) {
|
|
+ return (long)LONG_ARRAY_HANDLE.compareAndExchange(array, index, expect, update);
|
|
+ }
|
|
+
|
|
+ public static long getAndAddVolatile(final long[] array, final int index, final long param) {
|
|
+ return (long)LONG_ARRAY_HANDLE.getAndAdd(array, index, param);
|
|
+ }
|
|
+
|
|
+ public static long getAndAndVolatile(final long[] array, final int index, final long param) {
|
|
+ return (long)LONG_ARRAY_HANDLE.getAndBitwiseAnd(array, index, param);
|
|
+ }
|
|
+
|
|
+ public static long getAndOrVolatile(final long[] array, final int index, final long param) {
|
|
+ return (long)LONG_ARRAY_HANDLE.getAndBitwiseOr(array, index, param);
|
|
+ }
|
|
+
|
|
+ public static long getAndXorVolatile(final long[] array, final int index, final long param) {
|
|
+ return (long)LONG_ARRAY_HANDLE.getAndBitwiseXor(array, index, param);
|
|
+ }
|
|
+
|
|
+ public static long getAndSetVolatile(final long[] array, final int index, final long param) {
|
|
+ return (long)LONG_ARRAY_HANDLE.getAndSet(array, index, param);
|
|
+ }
|
|
+
|
|
+ public static long compareAndExchangeVolatileContended(final long[] array, final int index, final long expect, final long update) {
|
|
+ return (long)LONG_ARRAY_HANDLE.compareAndExchange(array, index, expect, update);
|
|
+ }
|
|
+
|
|
+ public static long getAndAddVolatileContended(final long[] array, final int index, final long param) {
|
|
+ int failures = 0;
|
|
+
|
|
+ for (long curr = getVolatile(array, index);;++failures) {
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+
|
|
+ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (long) (curr + param)))) {
|
|
+ return curr;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public static long getAndAndVolatileContended(final long[] array, final int index, final long param) {
|
|
+ int failures = 0;
|
|
+
|
|
+ for (long curr = getVolatile(array, index);;++failures) {
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+
|
|
+ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (long) (curr & param)))) {
|
|
+ return curr;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public static long getAndOrVolatileContended(final long[] array, final int index, final long param) {
|
|
+ int failures = 0;
|
|
+
|
|
+ for (long curr = getVolatile(array, index);;++failures) {
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+
|
|
+ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (long) (curr | param)))) {
|
|
+ return curr;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public static long getAndXorVolatileContended(final long[] array, final int index, final long param) {
|
|
+ int failures = 0;
|
|
+
|
|
+ for (long curr = getVolatile(array, index);;++failures) {
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+
|
|
+ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (long) (curr ^ param)))) {
|
|
+ return curr;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public static long getAndSetVolatileContended(final long[] array, final int index, final long param) {
|
|
+ int failures = 0;
|
|
+
|
|
+ for (long curr = getVolatile(array, index);;++failures) {
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+
|
|
+ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
|
|
+ return curr;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /* boolean array */
|
|
+
|
|
+ public static boolean getPlain(final boolean[] array, final int index) {
|
|
+ return (boolean)BOOLEAN_ARRAY_HANDLE.get(array, index);
|
|
+ }
|
|
+
|
|
+ public static boolean getOpaque(final boolean[] array, final int index) {
|
|
+ return (boolean)BOOLEAN_ARRAY_HANDLE.getOpaque(array, index);
|
|
+ }
|
|
+
|
|
+ public static boolean getAcquire(final boolean[] array, final int index) {
|
|
+ return (boolean)BOOLEAN_ARRAY_HANDLE.getAcquire(array, index);
|
|
+ }
|
|
+
|
|
+ public static boolean getVolatile(final boolean[] array, final int index) {
|
|
+ return (boolean)BOOLEAN_ARRAY_HANDLE.getVolatile(array, index);
|
|
+ }
|
|
+
|
|
+ public static void setPlain(final boolean[] array, final int index, final boolean value) {
|
|
+ BOOLEAN_ARRAY_HANDLE.set(array, index, value);
|
|
+ }
|
|
+
|
|
+ public static void setOpaque(final boolean[] array, final int index, final boolean value) {
|
|
+ BOOLEAN_ARRAY_HANDLE.setOpaque(array, index, value);
|
|
+ }
|
|
+
|
|
+ public static void setRelease(final boolean[] array, final int index, final boolean value) {
|
|
+ BOOLEAN_ARRAY_HANDLE.setRelease(array, index, value);
|
|
+ }
|
|
+
|
|
+ public static void setVolatile(final boolean[] array, final int index, final boolean value) {
|
|
+ BOOLEAN_ARRAY_HANDLE.setVolatile(array, index, value);
|
|
+ }
|
|
+
|
|
+ public static void setVolatileContended(final boolean[] array, final int index, final boolean param) {
|
|
+ int failures = 0;
|
|
+
|
|
+ for (boolean curr = getVolatile(array, index);;++failures) {
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+
|
|
+ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
|
|
+ return;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public static boolean compareAndExchangeVolatile(final boolean[] array, final int index, final boolean expect, final boolean update) {
|
|
+ return (boolean)BOOLEAN_ARRAY_HANDLE.compareAndExchange(array, index, expect, update);
|
|
+ }
|
|
+
|
|
+ public static boolean getAndOrVolatile(final boolean[] array, final int index, final boolean param) {
|
|
+ return (boolean)BOOLEAN_ARRAY_HANDLE.getAndBitwiseOr(array, index, param);
|
|
+ }
|
|
+
|
|
+ public static boolean getAndXorVolatile(final boolean[] array, final int index, final boolean param) {
|
|
+ return (boolean)BOOLEAN_ARRAY_HANDLE.getAndBitwiseXor(array, index, param);
|
|
+ }
|
|
+
|
|
+ public static boolean getAndSetVolatile(final boolean[] array, final int index, final boolean param) {
|
|
+ return (boolean)BOOLEAN_ARRAY_HANDLE.getAndSet(array, index, param);
|
|
+ }
|
|
+
|
|
+ public static boolean compareAndExchangeVolatileContended(final boolean[] array, final int index, final boolean expect, final boolean update) {
|
|
+ return (boolean)BOOLEAN_ARRAY_HANDLE.compareAndExchange(array, index, expect, update);
|
|
+ }
|
|
+
|
|
+ public static boolean getAndAndVolatileContended(final boolean[] array, final int index, final boolean param) {
|
|
+ int failures = 0;
|
|
+
|
|
+ for (boolean curr = getVolatile(array, index);;++failures) {
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+
|
|
+ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (boolean) (curr & param)))) {
|
|
+ return curr;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public static boolean getAndOrVolatileContended(final boolean[] array, final int index, final boolean param) {
|
|
+ int failures = 0;
|
|
+
|
|
+ for (boolean curr = getVolatile(array, index);;++failures) {
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+
|
|
+ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (boolean) (curr | param)))) {
|
|
+ return curr;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public static boolean getAndXorVolatileContended(final boolean[] array, final int index, final boolean param) {
|
|
+ int failures = 0;
|
|
+
|
|
+ for (boolean curr = getVolatile(array, index);;++failures) {
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+
|
|
+ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (boolean) (curr ^ param)))) {
|
|
+ return curr;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public static boolean getAndSetVolatileContended(final boolean[] array, final int index, final boolean param) {
|
|
+ int failures = 0;
|
|
+
|
|
+ for (boolean curr = getVolatile(array, index);;++failures) {
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+
|
|
+ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
|
|
+ return curr;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ @SuppressWarnings("unchecked")
|
|
+ public static <T> T getPlain(final T[] array, final int index) {
|
|
+ final Object ret = OBJECT_ARRAY_HANDLE.get((Object[])array, index);
|
|
+ return (T)ret;
|
|
+ }
|
|
+
|
|
+ @SuppressWarnings("unchecked")
|
|
+ public static <T> T getOpaque(final T[] array, final int index) {
|
|
+ final Object ret = OBJECT_ARRAY_HANDLE.getOpaque((Object[])array, index);
|
|
+ return (T)ret;
|
|
+ }
|
|
+
|
|
+ @SuppressWarnings("unchecked")
|
|
+ public static <T> T getAcquire(final T[] array, final int index) {
|
|
+ final Object ret = OBJECT_ARRAY_HANDLE.getAcquire((Object[])array, index);
|
|
+ return (T)ret;
|
|
+ }
|
|
+
|
|
+ @SuppressWarnings("unchecked")
|
|
+ public static <T> T getVolatile(final T[] array, final int index) {
|
|
+ final Object ret = OBJECT_ARRAY_HANDLE.getVolatile((Object[])array, index);
|
|
+ return (T)ret;
|
|
+ }
|
|
+
|
|
+ public static <T> void setPlain(final T[] array, final int index, final T value) {
|
|
+ OBJECT_ARRAY_HANDLE.set((Object[])array, index, (Object)value);
|
|
+ }
|
|
+
|
|
+ public static <T> void setOpaque(final T[] array, final int index, final T value) {
|
|
+ OBJECT_ARRAY_HANDLE.setOpaque((Object[])array, index, (Object)value);
|
|
+ }
|
|
+
|
|
+ public static <T> void setRelease(final T[] array, final int index, final T value) {
|
|
+ OBJECT_ARRAY_HANDLE.setRelease((Object[])array, index, (Object)value);
|
|
+ }
|
|
+
|
|
+ public static <T> void setVolatile(final T[] array, final int index, final T value) {
|
|
+ OBJECT_ARRAY_HANDLE.setVolatile((Object[])array, index, (Object)value);
|
|
+ }
|
|
+
|
|
+ public static <T> void setVolatileContended(final T[] array, final int index, final T param) {
|
|
+ int failures = 0;
|
|
+
|
|
+ for (T curr = getVolatile(array, index);;++failures) {
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+
|
|
+ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
|
|
+ return;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ @SuppressWarnings("unchecked")
|
|
+ public static <T> T compareAndExchangeVolatile(final T[] array, final int index, final T expect, final T update) {
|
|
+ final Object ret = OBJECT_ARRAY_HANDLE.compareAndExchange((Object[])array, index, (Object)expect, (Object)update);
|
|
+ return (T)ret;
|
|
+ }
|
|
+
|
|
+ @SuppressWarnings("unchecked")
|
|
+ public static <T> T getAndSetVolatile(final T[] array, final int index, final T param) {
|
|
+ final Object ret = BYTE_ARRAY_HANDLE.getAndSet((Object[])array, index, (Object)param);
|
|
+ return (T)ret;
|
|
+ }
|
|
+
|
|
+ @SuppressWarnings("unchecked")
|
|
+ public static <T> T compareAndExchangeVolatileContended(final T[] array, final int index, final T expect, final T update) {
|
|
+ final Object ret = OBJECT_ARRAY_HANDLE.compareAndExchange((Object[])array, index, (Object)expect, (Object)update);
|
|
+ return (T)ret;
|
|
+ }
|
|
+
|
|
+ public static <T> T getAndSetVolatileContended(final T[] array, final int index, final T param) {
|
|
+ int failures = 0;
|
|
+
|
|
+ for (T curr = getVolatile(array, index);;++failures) {
|
|
+ for (int i = 0; i < failures; ++i) {
|
|
+ ConcurrentUtil.backoff();
|
|
+ }
|
|
+
|
|
+ if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
|
|
+ return curr;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/CollectionUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/CollectionUtil.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..9420b9822de99d3a31224642452835b0c986f7b4
|
|
--- /dev/null
|
|
+++ b/src/main/java/ca/spottedleaf/concurrentutil/util/CollectionUtil.java
|
|
@@ -0,0 +1,31 @@
|
|
+package ca.spottedleaf.concurrentutil.util;
|
|
+
|
|
+import java.util.Collection;
|
|
+
|
|
+public final class CollectionUtil {
|
|
+
|
|
+ public static String toString(final Collection<?> collection, final String name) {
|
|
+ return CollectionUtil.toString(collection, name, new StringBuilder(name.length() + 128)).toString();
|
|
+ }
|
|
+
|
|
+ public static StringBuilder toString(final Collection<?> collection, final String name, final StringBuilder builder) {
|
|
+ builder.append(name).append("{elements={");
|
|
+
|
|
+ boolean first = true;
|
|
+
|
|
+ for (final Object element : collection) {
|
|
+ if (!first) {
|
|
+ builder.append(", ");
|
|
+ }
|
|
+ first = false;
|
|
+
|
|
+ builder.append('"').append(element).append('"');
|
|
+ }
|
|
+
|
|
+ return builder.append("}}");
|
|
+ }
|
|
+
|
|
+ private CollectionUtil() {
|
|
+ throw new RuntimeException();
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/ConcurrentUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/ConcurrentUtil.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..23ae82e55696a7e2ff0e0f9609c0df6a48bb8d1d
|
|
--- /dev/null
|
|
+++ b/src/main/java/ca/spottedleaf/concurrentutil/util/ConcurrentUtil.java
|
|
@@ -0,0 +1,166 @@
|
|
+package ca.spottedleaf.concurrentutil.util;
|
|
+
|
|
+import java.lang.invoke.MethodHandles;
|
|
+import java.lang.invoke.VarHandle;
|
|
+import java.util.concurrent.locks.LockSupport;
|
|
+
|
|
+public final class ConcurrentUtil {
|
|
+
|
|
+ public static String genericToString(final Object object) {
|
|
+ return object == null ? "null" : object.getClass().getName() + ":" + object.hashCode() + ":" + object.toString();
|
|
+ }
|
|
+
|
|
+ public static void rethrow(Throwable exception) {
|
|
+ rethrow0(exception);
|
|
+ }
|
|
+
|
|
+ private static <T extends Throwable> void rethrow0(Throwable thr) throws T {
|
|
+ throw (T)thr;
|
|
+ }
|
|
+
|
|
+ public static VarHandle getVarHandle(final Class<?> lookIn, final String fieldName, final Class<?> fieldType) {
|
|
+ try {
|
|
+ return MethodHandles.privateLookupIn(lookIn, MethodHandles.lookup()).findVarHandle(lookIn, fieldName, fieldType);
|
|
+ } catch (final Exception ex) {
|
|
+ throw new RuntimeException(ex); // unreachable
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public static VarHandle getStaticVarHandle(final Class<?> lookIn, final String fieldName, final Class<?> fieldType) {
|
|
+ try {
|
|
+ return MethodHandles.privateLookupIn(lookIn, MethodHandles.lookup()).findStaticVarHandle(lookIn, fieldName, fieldType);
|
|
+ } catch (final Exception ex) {
|
|
+ throw new RuntimeException(ex); // unreachable
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Non-exponential backoff algorithm to use in lightly contended areas.
|
|
+ * @see ConcurrentUtil#exponentiallyBackoffSimple(long)
|
|
+ * @see ConcurrentUtil#exponentiallyBackoffComplex(long)
|
|
+ */
|
|
+ public static void backoff() {
|
|
+ Thread.onSpinWait();
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Backoff algorithm to use for a short held lock (i.e compareAndExchange operation). Generally this should not be
|
|
+ * used when a thread can block another thread. Instead, use {@link ConcurrentUtil#exponentiallyBackoffComplex(long)}.
|
|
+ * @param counter The current counter.
|
|
+ * @return The counter plus 1.
|
|
+ * @see ConcurrentUtil#backoff()
|
|
+ * @see ConcurrentUtil#exponentiallyBackoffComplex(long)
|
|
+ */
|
|
+ public static long exponentiallyBackoffSimple(final long counter) {
|
|
+ for (long i = 0; i < counter; ++i) {
|
|
+ backoff();
|
|
+ }
|
|
+ return counter + 1L;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Backoff algorithm to use for a lock that can block other threads (i.e if another thread contending with this thread
|
|
+ * can be thrown off the scheduler). This lock should not be used for simple locks such as compareAndExchange.
|
|
+ * @param counter The current counter.
|
|
+ * @return The next (if any) step in the backoff logic.
|
|
+ * @see ConcurrentUtil#backoff()
|
|
+ * @see ConcurrentUtil#exponentiallyBackoffSimple(long)
|
|
+ */
|
|
+ public static long exponentiallyBackoffComplex(final long counter) {
|
|
+ // TODO experimentally determine counters
|
|
+ if (counter < 100L) {
|
|
+ return exponentiallyBackoffSimple(counter);
|
|
+ }
|
|
+ if (counter < 1_200L) {
|
|
+ Thread.yield();
|
|
+ LockSupport.parkNanos(1_000L);
|
|
+ return counter + 1L;
|
|
+ }
|
|
+ // scale 0.1ms (100us) per failure
|
|
+ Thread.yield();
|
|
+ LockSupport.parkNanos(100_000L * counter);
|
|
+ return counter + 1;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Simple exponential backoff that will linearly increase the time per failure, according to the scale.
|
|
+ * @param counter The current failure counter.
|
|
+ * @param scale Time per failure, in ns.
|
|
+ * @param max The maximum time to wait for, in ns.
|
|
+ * @return The next counter.
|
|
+ */
|
|
+ public static long linearLongBackoff(long counter, final long scale, long max) {
|
|
+ counter = Math.min(Long.MAX_VALUE, counter + 1); // prevent overflow
|
|
+ max = Math.max(0, max);
|
|
+
|
|
+ if (scale <= 0L) {
|
|
+ return counter;
|
|
+ }
|
|
+
|
|
+ long time = scale * counter;
|
|
+
|
|
+ if (time > max || time / scale != counter) {
|
|
+ time = max;
|
|
+ }
|
|
+
|
|
+ boolean interrupted = Thread.interrupted();
|
|
+ if (time > 1_000_000L) { // 1ms
|
|
+ Thread.yield();
|
|
+ }
|
|
+ LockSupport.parkNanos(time);
|
|
+ if (interrupted) {
|
|
+ Thread.currentThread().interrupt();
|
|
+ }
|
|
+ return counter;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Simple exponential backoff that will linearly increase the time per failure, according to the scale.
|
|
+ * @param counter The current failure counter.
|
|
+ * @param scale Time per failure, in ns.
|
|
+ * @param max The maximum time to wait for, in ns.
|
|
+ * @param deadline The deadline in ns. Deadline time source: {@link System#nanoTime()}.
|
|
+ * @return The next counter.
|
|
+ */
|
|
+ public static long linearLongBackoffDeadline(long counter, final long scale, long max, long deadline) {
|
|
+ counter = Math.min(Long.MAX_VALUE, counter + 1); // prevent overflow
|
|
+ max = Math.max(0, max);
|
|
+
|
|
+ if (scale <= 0L) {
|
|
+ return counter;
|
|
+ }
|
|
+
|
|
+ long time = scale * counter;
|
|
+
|
|
+ // check overflow
|
|
+ if (time / scale != counter) {
|
|
+ // overflew
|
|
+ --counter;
|
|
+ time = max;
|
|
+ } else if (time > max) {
|
|
+ time = max;
|
|
+ }
|
|
+
|
|
+ final long currTime = System.nanoTime();
|
|
+ final long diff = deadline - currTime;
|
|
+ if (diff <= 0) {
|
|
+ return counter;
|
|
+ }
|
|
+ if (diff <= 1_500_000L) { // 1.5ms
|
|
+ time = 100_000L; // 100us
|
|
+ } else if (time > 1_000_000L) { // 1ms
|
|
+ Thread.yield();
|
|
+ }
|
|
+
|
|
+ boolean interrupted = Thread.interrupted();
|
|
+ LockSupport.parkNanos(time);
|
|
+ if (interrupted) {
|
|
+ Thread.currentThread().interrupt();
|
|
+ }
|
|
+ return counter;
|
|
+ }
|
|
+
|
|
+ public static VarHandle getArrayHandle(final Class<?> type) {
|
|
+ return MethodHandles.arrayElementVarHandle(type);
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/Validate.java b/src/main/java/ca/spottedleaf/concurrentutil/util/Validate.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..382177d0d162fa3139c9078a873ce2504a2b17b2
|
|
--- /dev/null
|
|
+++ b/src/main/java/ca/spottedleaf/concurrentutil/util/Validate.java
|
|
@@ -0,0 +1,28 @@
|
|
+package ca.spottedleaf.concurrentutil.util;
|
|
+
|
|
+public final class Validate {
|
|
+
|
|
+ public static <T> T notNull(final T obj) {
|
|
+ if (obj == null) {
|
|
+ throw new NullPointerException();
|
|
+ }
|
|
+ return obj;
|
|
+ }
|
|
+
|
|
+ public static <T> T notNull(final T obj, final String msgIfNull) {
|
|
+ if (obj == null) {
|
|
+ throw new NullPointerException(msgIfNull);
|
|
+ }
|
|
+ return obj;
|
|
+ }
|
|
+
|
|
+ public static void arrayBounds(final int off, final int len, final int arrayLength, final String msgPrefix) {
|
|
+ if (off < 0 || len < 0 || (arrayLength - off) < len) {
|
|
+ throw new ArrayIndexOutOfBoundsException(msgPrefix + ": off: " + off + ", len: " + len + ", array length: " + arrayLength);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private Validate() {
|
|
+ throw new RuntimeException();
|
|
+ }
|
|
+}
|