afe633df08
* convert API tests to mockito * convert server tests to mockito * add co-author
7019 Zeilen
241 KiB
Diff
7019 Zeilen
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();
|
|
+ }
|
|
+}
|