mirror of
https://github.com/PaperMC/Paper.git
synced 2024-12-27 11:27:35 +01:00
df3b6544f7
The function assumed that the current resize chain pointed to the previous table, when in fact it pointed to the current table. The function is supposed to restore the resize chain to the previous table, previous increment, and previous index + new increment.
10486 lines
369 KiB
Diff
10486 lines
369 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..f84a622dc29750139ac280f480b7cd132b036287
|
|
--- /dev/null
|
|
+++ b/src/main/java/ca/spottedleaf/concurrentutil/collection/MultiThreadedQueue.java
|
|
@@ -0,0 +1,1421 @@
|
|
+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;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Returns whether this queue is currently add-blocked. That is, whether {@link #add(Object)} and friends will return {@code false}.
|
|
+ */
|
|
+ public boolean isAddBlocked() {
|
|
+ for (LinkedNode<E> tail = this.getTailOpaque();;) {
|
|
+ LinkedNode<E> next = tail.getNextVolatile();
|
|
+ if (next == null) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ if (next == tail) {
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ tail = next;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * 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..094eff418b4e3bffce020d650931b4d9e58fa9ed
|
|
--- /dev/null
|
|
+++ b/src/main/java/ca/spottedleaf/concurrentutil/collection/SRSWLinkedQueue.java
|
|
@@ -0,0 +1,149 @@
|
|
+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..46d1bd01542ebeeffc0006a5c585a50dbbbff907
|
|
--- /dev/null
|
|
+++ b/src/main/java/ca/spottedleaf/concurrentutil/completable/Completable.java
|
|
@@ -0,0 +1,112 @@
|
|
+package ca.spottedleaf.concurrentutil.completable;
|
|
+
|
|
+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
|
|
+import ca.spottedleaf.concurrentutil.executor.Cancellable;
|
|
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
|
|
+import org.slf4j.Logger;
|
|
+import org.slf4j.LoggerFactory;
|
|
+import java.util.function.BiConsumer;
|
|
+
|
|
+public final class Completable<T> {
|
|
+
|
|
+ private static final Logger LOGGER = LoggerFactory.getLogger(Completable.class);
|
|
+
|
|
+ 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;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Adds a waiter that should only be completed asynchronously by the complete() calls. If complete()
|
|
+ * has already been called, returns {@code null} and does not invoke the specified consumer.
|
|
+ * @param consumer Consumer to be executed on completion
|
|
+ * @throws NullPointerException If consumer is null
|
|
+ * @return A cancellable which will control the execution of the specified consumer
|
|
+ */
|
|
+ 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);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Adds a waiter that will be completed asynchronously by the complete() calls. If complete()
|
|
+ * has already been called, then invokes the consumer synchronously with the completed result.
|
|
+ * @param consumer Consumer to be executed on completion
|
|
+ * @throws NullPointerException If consumer is null
|
|
+ * @return A cancellable which will control the execution of the specified consumer
|
|
+ */
|
|
+ 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..18d646676fd022afd64afaac30ec1bd283a73b0e
|
|
--- /dev/null
|
|
+++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/BaseExecutor.java
|
|
@@ -0,0 +1,208 @@
|
|
+package ca.spottedleaf.concurrentutil.executor;
|
|
+
|
|
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
|
|
+import java.util.function.BooleanSupplier;
|
|
+
|
|
+/**
|
|
+ * Base implementation for an abstract queue of tasks which are executed either synchronously or asynchronously.
|
|
+ *
|
|
+ * <p>
|
|
+ * The implementation supports tracking task executions using {@link #getTotalTasksScheduled()} and
|
|
+ * {@link #getTotalTasksExecuted()}, and optionally shutting down the executor using {@link #shutdown()}
|
|
+ * </p>
|
|
+ *
|
|
+ * <p>
|
|
+ * The base implementation does not provide a method to queue a task for execution, rather that is specified in
|
|
+ * the specific implementation. However, it is required that a specific implementation provides a method to
|
|
+ * <i>queue</i> a task or <i>create</i> a task. A <i>queued</i> task is one which will eventually be executed,
|
|
+ * and a <i>created</i> task must be queued to execute via {@link BaseTask#queue()} or be executed manually via
|
|
+ * {@link BaseTask#execute()}. This choice of delaying the queueing of a task may be useful to provide a task handle
|
|
+ * which may be cancelled or adjusted before the actual real task logic is ready to be executed.
|
|
+ * </p>
|
|
+ */
|
|
+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 call.
|
|
+ * </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.
|
|
+ *
|
|
+ * @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 >= 0}.
|
|
+ */
|
|
+ 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 < 0L) && !condition.getAsBoolean() && (System.nanoTime() - deadline < 0L)) {
|
|
+ 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 >= 0}.
|
|
+ */
|
|
+ public default void executeUntil(final long deadline) {
|
|
+ long failures = 0;
|
|
+ while (System.nanoTime() - deadline < 0L) {
|
|
+ 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
|
|
+ * @see #isShutdown()
|
|
+ */
|
|
+ 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 the tasks scheduled have been executed.
|
|
+ * @return Returns whether this queue has shut down.
|
|
+ * @see #waitUntilAllExecuted()
|
|
+ */
|
|
+ public default boolean isShutdown() {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Task object returned for any {@link BaseExecutor} scheduled task.
|
|
+ * @see BaseExecutor
|
|
+ */
|
|
+ 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..91beb6f23f257cf265fe3150f760892e605f217a
|
|
--- /dev/null
|
|
+++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedExecutor.java
|
|
@@ -0,0 +1,276 @@
|
|
+package ca.spottedleaf.concurrentutil.executor.standard;
|
|
+
|
|
+import ca.spottedleaf.concurrentutil.executor.BaseExecutor;
|
|
+
|
|
+/**
|
|
+ * Implementation of {@link BaseExecutor} which schedules tasks to be executed by a given priority.
|
|
+ * @see 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 Priority max(final Priority p1, final Priority p2) {
|
|
+ return p1.isHigherOrEqualPriority(p2) ? p1 : p2;
|
|
+ }
|
|
+
|
|
+ // returns the lower priroity of the two
|
|
+ public static 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 Priority[] PRIORITIES = 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 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;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * 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
|
|
+ */
|
|
+ @Override
|
|
+ public boolean executeTask() throws IllegalStateException;
|
|
+
|
|
+ /**
|
|
+ * 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, 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 Priority priority);
|
|
+
|
|
+ /**
|
|
+ * Creates, but does not execute or queue the task. The task must later be queued via {@link 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 PrioritisedTask createTask(final Runnable task) {
|
|
+ return this.createTask(task, Priority.NORMAL);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Creates, but does not execute or queue the task. The task must later be queued via {@link 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 PrioritisedTask createTask(final Runnable task, final Priority priority);
|
|
+
|
|
+ /**
|
|
+ * Extension of {@link ca.spottedleaf.concurrentutil.executor.BaseExecutor.BaseTask} which adds functions
|
|
+ * to retrieve and modify the task's associated priority.
|
|
+ *
|
|
+ * @see ca.spottedleaf.concurrentutil.executor.BaseExecutor.BaseTask
|
|
+ */
|
|
+ public static interface PrioritisedTask extends BaseTask {
|
|
+
|
|
+ /**
|
|
+ * Returns the current priority. Note that {@link Priority#COMPLETING} will be returned
|
|
+ * if this task is completing or has completed.
|
|
+ */
|
|
+ public 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 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 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 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..d1683ba6350e530373944f98192c0f2baf241e70
|
|
--- /dev/null
|
|
+++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedQueueExecutorThread.java
|
|
@@ -0,0 +1,301 @@
|
|
+package ca.spottedleaf.concurrentutil.executor.standard;
|
|
+
|
|
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
|
|
+import org.slf4j.Logger;
|
|
+import org.slf4j.LoggerFactory;
|
|
+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 = LoggerFactory.getLogger(PrioritisedQueueExecutorThread.class);
|
|
+
|
|
+ protected final PrioritisedExecutor queue;
|
|
+
|
|
+ protected volatile boolean threadShutdown;
|
|
+
|
|
+ protected volatile boolean threadParked;
|
|
+ protected static final VarHandle THREAD_PARKED_HANDLE = ConcurrentUtil.getVarHandle(PrioritisedQueueExecutorThread.class, "threadParked", boolean.class);
|
|
+
|
|
+ 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");
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Attempts to poll as many tasks as possible, returning when finished.
|
|
+ * @return Whether any tasks were executed.
|
|
+ */
|
|
+ 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 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 PrioritisedTask queueRunnable(final Runnable task, final Priority priority) {
|
|
+ final 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..2ba36e29d0d8693f2f5e6c6d195ca27f2a5099aa
|
|
--- /dev/null
|
|
+++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadPool.java
|
|
@@ -0,0 +1,632 @@
|
|
+package ca.spottedleaf.concurrentutil.executor.standard;
|
|
+
|
|
+import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
|
|
+import org.slf4j.Logger;
|
|
+import org.slf4j.LoggerFactory;
|
|
+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 = LoggerFactory.getLogger(PrioritisedThreadPool.class);
|
|
+
|
|
+ private final PrioritisedThread[] threads;
|
|
+ private final TreeSet<PrioritisedPoolExecutorImpl> queues = new TreeSet<>(PrioritisedPoolExecutorImpl.comparator());
|
|
+ private final String name;
|
|
+ private final long queueMaxHoldTime;
|
|
+
|
|
+ private final ReferenceOpenHashSet<PrioritisedPoolExecutorImpl> nonShutdownQueues = new ReferenceOpenHashSet<>();
|
|
+ private final ReferenceOpenHashSet<PrioritisedPoolExecutorImpl> activeQueues = new ReferenceOpenHashSet<>();
|
|
+
|
|
+ private boolean shutdown;
|
|
+
|
|
+ private long schedulingIdGenerator;
|
|
+
|
|
+ private static final long DEFAULT_QUEUE_HOLD_TIME = (long)(5.0e6);
|
|
+
|
|
+ /**
|
|
+ * @param name Specified debug name of this thread pool
|
|
+ * @param threads The number of threads to use
|
|
+ */
|
|
+ public PrioritisedThreadPool(final String name, final int threads) {
|
|
+ this(name, threads, null);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * @param name Specified debug name of this thread pool
|
|
+ * @param threads The number of threads to use
|
|
+ * @param threadModifier Invoked for each created thread with its incremental id before starting them
|
|
+ */
|
|
+ public PrioritisedThreadPool(final String name, final int threads, final BiConsumer<Thread, Integer> threadModifier) {
|
|
+ this(name, threads, threadModifier, DEFAULT_QUEUE_HOLD_TIME); // 5ms
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * @param name Specified debug name of this thread pool
|
|
+ * @param threads The number of threads to use
|
|
+ * @param threadModifier Invoked for each created thread with its incremental id before starting them
|
|
+ * @param queueHoldTime The maximum amount of time to spend executing tasks in a specific queue before attempting
|
|
+ * to switch to another queue, per thread
|
|
+ */
|
|
+ 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();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Returns an array representing the threads backing this thread pool.
|
|
+ */
|
|
+ public Thread[] getThreads() {
|
|
+ return Arrays.copyOf(this.threads, this.threads.length, Thread[].class);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Creates and returns a {@link PrioritisedPoolExecutor} to schedule tasks onto. The returned executor will execute
|
|
+ * tasks on this thread pool only.
|
|
+ * @param name The debug name of the executor.
|
|
+ * @param minParallelism The minimum number of threads to be executing tasks from the returned executor
|
|
+ * before threads may be allocated to other queues in this thread pool.
|
|
+ * @param parallelism The maximum number of threads which may be executing tasks from the returned executor.
|
|
+ * @throws IllegalStateException If this thread pool is shut down
|
|
+ */
|
|
+ public PrioritisedPoolExecutor createExecutor(final String name, final int minParallelism, 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),
|
|
+ Math.min(Math.max(0, minParallelism), 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();
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Shuts down this thread pool, optionally waiting for all tasks to be executed.
|
|
+ * This function will invoke {@link PrioritisedPoolExecutor#shutdown()} on all created executors on this
|
|
+ * thread pool.
|
|
+ * @param wait Whether to wait for tasks to be executed
|
|
+ */
|
|
+ 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 final int minimumExecutors;
|
|
+ protected boolean isQueued;
|
|
+
|
|
+ public PrioritisedPoolExecutorImpl(final PrioritisedThreadPool pool, final String name, final int maximumExecutors, final int minimumExecutors) {
|
|
+ this.pool = pool;
|
|
+ this.name = name;
|
|
+ this.maximumExecutors = maximumExecutors;
|
|
+ this.minimumExecutors = minimumExecutors;
|
|
+ }
|
|
+
|
|
+ public static Comparator<PrioritisedPoolExecutorImpl> comparator() {
|
|
+ return (final PrioritisedPoolExecutorImpl p1, final PrioritisedPoolExecutorImpl p2) -> {
|
|
+ if (p1 == p2) {
|
|
+ return 0;
|
|
+ }
|
|
+
|
|
+ final int belowMin1 = p1.minimumExecutors - p1.concurrentExecutors;
|
|
+ final int belowMin2 = p2.minimumExecutors - p2.concurrentExecutors;
|
|
+
|
|
+ // test minimum executors
|
|
+ if (belowMin1 > 0 || belowMin2 > 0) {
|
|
+ // want the largest belowMin to be first
|
|
+ final int minCompare = Integer.compare(belowMin2, belowMin1);
|
|
+
|
|
+ if (minCompare != 0) {
|
|
+ return minCompare;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // 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..3e8401b1b1f833c4f01bc87059a2f48d761d989f
|
|
--- /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 Priority priority) throws IllegalStateException, IllegalArgumentException {
|
|
+ if (!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 (!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 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 Priority priority;
|
|
+
|
|
+ protected PrioritisedTask(final long id, final Runnable runnable, final Priority priority, final PrioritisedThreadedTaskQueue queue) {
|
|
+ if (!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 Priority priority, final PrioritisedThreadedTaskQueue queue) {
|
|
+ if (!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 Priority priority = this.priority;
|
|
+ if (priority == 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 Priority oldPriority = this.priority;
|
|
+ if (oldPriority != Priority.COMPLETING && oldPriority.isHigherOrEqualPriority(minPriority)) {
|
|
+ this.priority = Priority.COMPLETING;
|
|
+ if (this.id != NOT_SCHEDULED_ID) {
|
|
+ this.queue.priorityChange(this, oldPriority, Priority.COMPLETING);
|
|
+ }
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public Priority getPriority() {
|
|
+ return this.priority;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean setPriority(final Priority priority) {
|
|
+ if (!Priority.isValidPriority(priority)) {
|
|
+ throw new IllegalArgumentException("Invalid priority " + priority);
|
|
+ }
|
|
+ synchronized (this.queue.queues) {
|
|
+ final Priority curr = this.priority;
|
|
+
|
|
+ if (curr == 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 Priority priority) {
|
|
+ if (!Priority.isValidPriority(priority)) {
|
|
+ throw new IllegalArgumentException("Invalid priority " + priority);
|
|
+ }
|
|
+
|
|
+ synchronized (this.queue.queues) {
|
|
+ final Priority curr = this.priority;
|
|
+
|
|
+ if (curr == 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 Priority priority) {
|
|
+ if (!Priority.isValidPriority(priority)) {
|
|
+ throw new IllegalArgumentException("Invalid priority " + priority);
|
|
+ }
|
|
+
|
|
+ synchronized (this.queue.queues) {
|
|
+ final Priority curr = this.priority;
|
|
+
|
|
+ if (curr == 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 == Priority.COMPLETING) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ this.priority = Priority.COMPLETING;
|
|
+ // call priority change callback
|
|
+ if ((id = this.id) != NOT_SCHEDULED_ID) {
|
|
+ this.queue.priorityChange(this, oldPriority, 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 == Priority.COMPLETING) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ this.priority = Priority.COMPLETING;
|
|
+ // call priority change callback
|
|
+ if (this.id != NOT_SCHEDULED_ID) {
|
|
+ this.queue.priorityChange(this, oldPriority, Priority.COMPLETING);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ this.executeInternal();
|
|
+ return true;
|
|
+ }
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/function/BiLong1Function.java b/src/main/java/ca/spottedleaf/concurrentutil/function/BiLong1Function.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..94bfd7c56ffcea7d6491e94a7804bc3bd60fe9c3
|
|
--- /dev/null
|
|
+++ b/src/main/java/ca/spottedleaf/concurrentutil/function/BiLong1Function.java
|
|
@@ -0,0 +1,8 @@
|
|
+package ca.spottedleaf.concurrentutil.function;
|
|
+
|
|
+@FunctionalInterface
|
|
+public interface BiLong1Function<T, R> {
|
|
+
|
|
+ public R apply(final long t1, final T t2);
|
|
+
|
|
+}
|
|
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/function/BiLongObjectConsumer.java b/src/main/java/ca/spottedleaf/concurrentutil/function/BiLongObjectConsumer.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..8e7eef07960a18d0593688eba55adfa1c85efadf
|
|
--- /dev/null
|
|
+++ b/src/main/java/ca/spottedleaf/concurrentutil/function/BiLongObjectConsumer.java
|
|
@@ -0,0 +1,8 @@
|
|
+package ca.spottedleaf.concurrentutil.function;
|
|
+
|
|
+@FunctionalInterface
|
|
+public interface BiLongObjectConsumer<V> {
|
|
+
|
|
+ public void accept(final long key, final V value);
|
|
+
|
|
+}
|
|
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/lock/ReentrantAreaLock.java b/src/main/java/ca/spottedleaf/concurrentutil/lock/ReentrantAreaLock.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..7ffe4379b06c03c56abbcbdee3bb720894a10702
|
|
--- /dev/null
|
|
+++ b/src/main/java/ca/spottedleaf/concurrentutil/lock/ReentrantAreaLock.java
|
|
@@ -0,0 +1,350 @@
|
|
+package ca.spottedleaf.concurrentutil.lock;
|
|
+
|
|
+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
|
|
+import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable;
|
|
+import ca.spottedleaf.concurrentutil.util.IntPairUtil;
|
|
+import java.util.Objects;
|
|
+import java.util.concurrent.locks.LockSupport;
|
|
+
|
|
+public final class ReentrantAreaLock {
|
|
+
|
|
+ public final int coordinateShift;
|
|
+
|
|
+ // aggressive load factor to reduce contention
|
|
+ private final ConcurrentLong2ReferenceChainedHashTable<Node> nodes = ConcurrentLong2ReferenceChainedHashTable.createWithCapacity(128, 0.2f);
|
|
+
|
|
+ public ReentrantAreaLock(final int coordinateShift) {
|
|
+ this.coordinateShift = coordinateShift;
|
|
+ }
|
|
+
|
|
+ public boolean isHeldByCurrentThread(final int x, final int z) {
|
|
+ final Thread currThread = Thread.currentThread();
|
|
+ final int shift = this.coordinateShift;
|
|
+ final int sectionX = x >> shift;
|
|
+ final int sectionZ = z >> shift;
|
|
+
|
|
+ final long coordinate = IntPairUtil.key(sectionX, sectionZ);
|
|
+ final Node node = this.nodes.get(coordinate);
|
|
+
|
|
+ return node != null && node.thread == currThread;
|
|
+ }
|
|
+
|
|
+ public boolean isHeldByCurrentThread(final int centerX, final int centerZ, final int radius) {
|
|
+ return this.isHeldByCurrentThread(centerX - radius, centerZ - radius, centerX + radius, centerZ + radius);
|
|
+ }
|
|
+
|
|
+ public boolean isHeldByCurrentThread(final int fromX, final int fromZ, final int toX, final int toZ) {
|
|
+ if (fromX > toX || fromZ > toZ) {
|
|
+ throw new IllegalArgumentException();
|
|
+ }
|
|
+
|
|
+ final Thread currThread = Thread.currentThread();
|
|
+ final int shift = this.coordinateShift;
|
|
+ final int fromSectionX = fromX >> shift;
|
|
+ final int fromSectionZ = fromZ >> shift;
|
|
+ final int toSectionX = toX >> shift;
|
|
+ final int toSectionZ = toZ >> shift;
|
|
+
|
|
+ for (int currZ = fromSectionZ; currZ <= toSectionZ; ++currZ) {
|
|
+ for (int currX = fromSectionX; currX <= toSectionX; ++currX) {
|
|
+ final long coordinate = IntPairUtil.key(currX, currZ);
|
|
+
|
|
+ final Node node = this.nodes.get(coordinate);
|
|
+
|
|
+ if (node == null || node.thread != currThread) {
|
|
+ return false;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ public Node tryLock(final int x, final int z) {
|
|
+ return this.tryLock(x, z, x, z);
|
|
+ }
|
|
+
|
|
+ public Node tryLock(final int centerX, final int centerZ, final int radius) {
|
|
+ return this.tryLock(centerX - radius, centerZ - radius, centerX + radius, centerZ + radius);
|
|
+ }
|
|
+
|
|
+ public Node tryLock(final int fromX, final int fromZ, final int toX, final int toZ) {
|
|
+ if (fromX > toX || fromZ > toZ) {
|
|
+ throw new IllegalArgumentException();
|
|
+ }
|
|
+
|
|
+ final Thread currThread = Thread.currentThread();
|
|
+ final int shift = this.coordinateShift;
|
|
+ final int fromSectionX = fromX >> shift;
|
|
+ final int fromSectionZ = fromZ >> shift;
|
|
+ final int toSectionX = toX >> shift;
|
|
+ final int toSectionZ = toZ >> shift;
|
|
+
|
|
+ final long[] areaAffected = new long[(toSectionX - fromSectionX + 1) * (toSectionZ - fromSectionZ + 1)];
|
|
+ int areaAffectedLen = 0;
|
|
+
|
|
+ final Node ret = new Node(this, areaAffected, currThread);
|
|
+
|
|
+ boolean failed = false;
|
|
+
|
|
+ // try to fast acquire area
|
|
+ for (int currZ = fromSectionZ; currZ <= toSectionZ; ++currZ) {
|
|
+ for (int currX = fromSectionX; currX <= toSectionX; ++currX) {
|
|
+ final long coordinate = IntPairUtil.key(currX, currZ);
|
|
+
|
|
+ final Node prev = this.nodes.putIfAbsent(coordinate, ret);
|
|
+
|
|
+ if (prev == null) {
|
|
+ areaAffected[areaAffectedLen++] = coordinate;
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ if (prev.thread != currThread) {
|
|
+ failed = true;
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (!failed) {
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ // failed, undo logic
|
|
+ if (areaAffectedLen != 0) {
|
|
+ for (int i = 0; i < areaAffectedLen; ++i) {
|
|
+ final long key = areaAffected[i];
|
|
+
|
|
+ if (this.nodes.remove(key) != ret) {
|
|
+ throw new IllegalStateException();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ areaAffectedLen = 0;
|
|
+
|
|
+ // since we inserted, we need to drain waiters
|
|
+ Thread unpark;
|
|
+ while ((unpark = ret.pollOrBlockAdds()) != null) {
|
|
+ LockSupport.unpark(unpark);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ public Node lock(final int x, final int z) {
|
|
+ final Thread currThread = Thread.currentThread();
|
|
+ final int shift = this.coordinateShift;
|
|
+ final int sectionX = x >> shift;
|
|
+ final int sectionZ = z >> shift;
|
|
+
|
|
+ final long coordinate = IntPairUtil.key(sectionX, sectionZ);
|
|
+ final long[] areaAffected = new long[1];
|
|
+ areaAffected[0] = coordinate;
|
|
+
|
|
+ final Node ret = new Node(this, areaAffected, currThread);
|
|
+
|
|
+ for (long failures = 0L;;) {
|
|
+ final Node park;
|
|
+
|
|
+ // try to fast acquire area
|
|
+ {
|
|
+ final Node prev = this.nodes.putIfAbsent(coordinate, ret);
|
|
+
|
|
+ if (prev == null) {
|
|
+ ret.areaAffectedLen = 1;
|
|
+ return ret;
|
|
+ } else if (prev.thread != currThread) {
|
|
+ park = prev;
|
|
+ } else {
|
|
+ // only one node we would want to acquire, and it's owned by this thread already
|
|
+ // areaAffectedLen = 0 already
|
|
+ return ret;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ ++failures;
|
|
+
|
|
+ if (failures > 128L && park.add(currThread)) {
|
|
+ LockSupport.park();
|
|
+ } else {
|
|
+ // high contention, spin wait
|
|
+ if (failures < 128L) {
|
|
+ for (long i = 0; i < failures; ++i) {
|
|
+ Thread.onSpinWait();
|
|
+ }
|
|
+ failures = failures << 1;
|
|
+ } else if (failures < 1_200L) {
|
|
+ LockSupport.parkNanos(1_000L);
|
|
+ failures = failures + 1L;
|
|
+ } else { // scale 0.1ms (100us) per failure
|
|
+ Thread.yield();
|
|
+ LockSupport.parkNanos(100_000L * failures);
|
|
+ failures = failures + 1L;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public Node lock(final int centerX, final int centerZ, final int radius) {
|
|
+ return this.lock(centerX - radius, centerZ - radius, centerX + radius, centerZ + radius);
|
|
+ }
|
|
+
|
|
+ public Node lock(final int fromX, final int fromZ, final int toX, final int toZ) {
|
|
+ if (fromX > toX || fromZ > toZ) {
|
|
+ throw new IllegalArgumentException();
|
|
+ }
|
|
+
|
|
+ final Thread currThread = Thread.currentThread();
|
|
+ final int shift = this.coordinateShift;
|
|
+ final int fromSectionX = fromX >> shift;
|
|
+ final int fromSectionZ = fromZ >> shift;
|
|
+ final int toSectionX = toX >> shift;
|
|
+ final int toSectionZ = toZ >> shift;
|
|
+
|
|
+ if (((fromSectionX ^ toSectionX) | (fromSectionZ ^ toSectionZ)) == 0) {
|
|
+ return this.lock(fromX, fromZ);
|
|
+ }
|
|
+
|
|
+ final long[] areaAffected = new long[(toSectionX - fromSectionX + 1) * (toSectionZ - fromSectionZ + 1)];
|
|
+ int areaAffectedLen = 0;
|
|
+
|
|
+ final Node ret = new Node(this, areaAffected, currThread);
|
|
+
|
|
+ for (long failures = 0L;;) {
|
|
+ Node park = null;
|
|
+ boolean addedToArea = false;
|
|
+ boolean alreadyOwned = false;
|
|
+ boolean allOwned = true;
|
|
+
|
|
+ // try to fast acquire area
|
|
+ for (int currZ = fromSectionZ; currZ <= toSectionZ; ++currZ) {
|
|
+ for (int currX = fromSectionX; currX <= toSectionX; ++currX) {
|
|
+ final long coordinate = IntPairUtil.key(currX, currZ);
|
|
+
|
|
+ final Node prev = this.nodes.putIfAbsent(coordinate, ret);
|
|
+
|
|
+ if (prev == null) {
|
|
+ addedToArea = true;
|
|
+ allOwned = false;
|
|
+ areaAffected[areaAffectedLen++] = coordinate;
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ if (prev.thread != currThread) {
|
|
+ park = prev;
|
|
+ alreadyOwned = true;
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // check for failure
|
|
+ if ((park != null && addedToArea) || (park == null && alreadyOwned && !allOwned)) {
|
|
+ // failure to acquire: added and we need to block, or improper lock usage
|
|
+ for (int i = 0; i < areaAffectedLen; ++i) {
|
|
+ final long key = areaAffected[i];
|
|
+
|
|
+ if (this.nodes.remove(key) != ret) {
|
|
+ throw new IllegalStateException();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ areaAffectedLen = 0;
|
|
+
|
|
+ // since we inserted, we need to drain waiters
|
|
+ Thread unpark;
|
|
+ while ((unpark = ret.pollOrBlockAdds()) != null) {
|
|
+ LockSupport.unpark(unpark);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (park == null) {
|
|
+ if (alreadyOwned && !allOwned) {
|
|
+ throw new IllegalStateException("Improper lock usage: Should never acquire intersecting areas");
|
|
+ }
|
|
+ ret.areaAffectedLen = areaAffectedLen;
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ // failed
|
|
+
|
|
+ ++failures;
|
|
+
|
|
+ if (failures > 128L && park.add(currThread)) {
|
|
+ LockSupport.park(park);
|
|
+ } else {
|
|
+ // high contention, spin wait
|
|
+ if (failures < 128L) {
|
|
+ for (long i = 0; i < failures; ++i) {
|
|
+ Thread.onSpinWait();
|
|
+ }
|
|
+ failures = failures << 1;
|
|
+ } else if (failures < 1_200L) {
|
|
+ LockSupport.parkNanos(1_000L);
|
|
+ failures = failures + 1L;
|
|
+ } else { // scale 0.1ms (100us) per failure
|
|
+ Thread.yield();
|
|
+ LockSupport.parkNanos(100_000L * failures);
|
|
+ failures = failures + 1L;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (addedToArea) {
|
|
+ // try again, so we need to allow adds so that other threads can properly block on us
|
|
+ ret.allowAdds();
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public void unlock(final Node node) {
|
|
+ if (node.lock != this) {
|
|
+ throw new IllegalStateException("Unlock target lock mismatch");
|
|
+ }
|
|
+
|
|
+ final long[] areaAffected = node.areaAffected;
|
|
+ final int areaAffectedLen = node.areaAffectedLen;
|
|
+
|
|
+ if (areaAffectedLen == 0) {
|
|
+ // here we are not in the node map, and so do not need to remove from the node map or unblock any waiters
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ Objects.checkFromToIndex(0, areaAffectedLen, areaAffected.length);
|
|
+
|
|
+ // remove from node map; allowing other threads to lock
|
|
+ for (int i = 0; i < areaAffectedLen; ++i) {
|
|
+ final long coordinate = areaAffected[i];
|
|
+ if (this.nodes.remove(coordinate, node) != node) {
|
|
+ throw new IllegalStateException();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ Thread unpark;
|
|
+ while ((unpark = node.pollOrBlockAdds()) != null) {
|
|
+ LockSupport.unpark(unpark);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public static final class Node extends MultiThreadedQueue<Thread> {
|
|
+
|
|
+ private final ReentrantAreaLock lock;
|
|
+ private final long[] areaAffected;
|
|
+ private int areaAffectedLen;
|
|
+ private final Thread thread;
|
|
+
|
|
+ private Node(final ReentrantAreaLock lock, final long[] areaAffected, final Thread thread) {
|
|
+ this.lock = lock;
|
|
+ this.areaAffected = areaAffected;
|
|
+ this.thread = thread;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public String toString() {
|
|
+ return "Node{" +
|
|
+ "areaAffected=" + IntPairUtil.toString(this.areaAffected, 0, this.areaAffectedLen) +
|
|
+ ", thread=" + this.thread +
|
|
+ '}';
|
|
+ }
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/map/ConcurrentLong2ReferenceChainedHashTable.java b/src/main/java/ca/spottedleaf/concurrentutil/map/ConcurrentLong2ReferenceChainedHashTable.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..d701998b376579ec652fb94823befa3cc0bc4090
|
|
--- /dev/null
|
|
+++ b/src/main/java/ca/spottedleaf/concurrentutil/map/ConcurrentLong2ReferenceChainedHashTable.java
|
|
@@ -0,0 +1,1684 @@
|
|
+package ca.spottedleaf.concurrentutil.map;
|
|
+
|
|
+import ca.spottedleaf.concurrentutil.function.BiLong1Function;
|
|
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
|
|
+import ca.spottedleaf.concurrentutil.util.HashUtil;
|
|
+import ca.spottedleaf.concurrentutil.util.IntegerUtil;
|
|
+import ca.spottedleaf.concurrentutil.util.ThrowUtil;
|
|
+import ca.spottedleaf.concurrentutil.util.Validate;
|
|
+import java.lang.invoke.VarHandle;
|
|
+import java.util.Arrays;
|
|
+import java.util.Iterator;
|
|
+import java.util.NoSuchElementException;
|
|
+import java.util.PrimitiveIterator;
|
|
+import java.util.concurrent.atomic.LongAdder;
|
|
+import java.util.function.BiFunction;
|
|
+import java.util.function.Consumer;
|
|
+import java.util.function.Function;
|
|
+import java.util.function.LongConsumer;
|
|
+import java.util.function.LongFunction;
|
|
+import java.util.function.Predicate;
|
|
+
|
|
+/**
|
|
+ * Concurrent hashtable implementation supporting mapping arbitrary {@code long} values onto non-null {@code Object}
|
|
+ * values with support for multiple writer and multiple reader threads.
|
|
+ *
|
|
+ * <p><h3>Happens-before relationship</h3></p>
|
|
+ * <p>
|
|
+ * As with {@link java.util.concurrent.ConcurrentMap}, there is a happens-before relationship between actions in one thread
|
|
+ * prior to writing to the map and access to the results of those actions in another thread.
|
|
+ * </p>
|
|
+ *
|
|
+ * <p><h3>Atomicity of functional methods</h3></p>
|
|
+ * <p>
|
|
+ * Functional methods are functions declared in this class which possibly perform a write (remove, replace, or modify)
|
|
+ * to an entry in this map as a result of invoking a function on an input parameter. For example, {@link #compute(long, BiLong1Function)},
|
|
+ * {@link #merge(long, Object, BiFunction)} and {@link #removeIf(long, Predicate)} are examples of functional methods.
|
|
+ * Functional methods will be performed atomically, that is, the input parameter is guaranteed to only be invoked at most
|
|
+ * once per function call. The consequence of this behavior however is that a critical lock for a bin entry is held, which
|
|
+ * means that if the input parameter invocation makes additional calls to write into this hash table that the result
|
|
+ * is undefined and deadlock-prone.
|
|
+ * </p>
|
|
+ *
|
|
+ * @param <V>
|
|
+ * @see java.util.concurrent.ConcurrentMap
|
|
+ */
|
|
+public class ConcurrentLong2ReferenceChainedHashTable<V> {
|
|
+
|
|
+ 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;
|
|
+
|
|
+ protected final LongAdder size = new LongAdder();
|
|
+ protected final float loadFactor;
|
|
+
|
|
+ protected volatile TableEntry<V>[] table;
|
|
+
|
|
+ protected static final int THRESHOLD_NO_RESIZE = -1;
|
|
+ protected static final int THRESHOLD_RESIZING = -2;
|
|
+ protected volatile int threshold;
|
|
+ protected static final VarHandle THRESHOLD_HANDLE = ConcurrentUtil.getVarHandle(ConcurrentLong2ReferenceChainedHashTable.class, "threshold", int.class);
|
|
+
|
|
+ protected final int getThresholdAcquire() {
|
|
+ return (int)THRESHOLD_HANDLE.getAcquire(this);
|
|
+ }
|
|
+
|
|
+ protected final int getThresholdVolatile() {
|
|
+ return (int)THRESHOLD_HANDLE.getVolatile(this);
|
|
+ }
|
|
+
|
|
+ protected final void setThresholdPlain(final int threshold) {
|
|
+ THRESHOLD_HANDLE.set(this, threshold);
|
|
+ }
|
|
+
|
|
+ protected final void setThresholdRelease(final int threshold) {
|
|
+ THRESHOLD_HANDLE.setRelease(this, threshold);
|
|
+ }
|
|
+
|
|
+ protected final void setThresholdVolatile(final int threshold) {
|
|
+ THRESHOLD_HANDLE.setVolatile(this, threshold);
|
|
+ }
|
|
+
|
|
+ protected final int compareExchangeThresholdVolatile(final int expect, final int update) {
|
|
+ return (int)THRESHOLD_HANDLE.compareAndExchange(this, expect, update);
|
|
+ }
|
|
+
|
|
+ public ConcurrentLong2ReferenceChainedHashTable() {
|
|
+ this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR);
|
|
+ }
|
|
+
|
|
+ protected static int getTargetThreshold(final int capacity, final float loadFactor) {
|
|
+ final double ret = (double)capacity * (double)loadFactor;
|
|
+ if (Double.isInfinite(ret) || ret >= ((double)Integer.MAX_VALUE)) {
|
|
+ return THRESHOLD_NO_RESIZE;
|
|
+ }
|
|
+
|
|
+ return (int)Math.ceil(ret);
|
|
+ }
|
|
+
|
|
+ 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);
|
|
+ }
|
|
+
|
|
+ protected ConcurrentLong2ReferenceChainedHashTable(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);
|
|
+ }
|
|
+
|
|
+ if (tableSize == MAXIMUM_CAPACITY) {
|
|
+ this.setThresholdPlain(THRESHOLD_NO_RESIZE);
|
|
+ } else {
|
|
+ this.setThresholdPlain(getTargetThreshold(tableSize, loadFactor));
|
|
+ }
|
|
+
|
|
+ this.loadFactor = loadFactor;
|
|
+ // noinspection unchecked
|
|
+ this.table = (TableEntry<V>[])new TableEntry[tableSize];
|
|
+ }
|
|
+
|
|
+ public static <V> ConcurrentLong2ReferenceChainedHashTable<V> createWithCapacity(final int capacity) {
|
|
+ return createWithCapacity(capacity, DEFAULT_LOAD_FACTOR);
|
|
+ }
|
|
+
|
|
+ public static <V> ConcurrentLong2ReferenceChainedHashTable<V> createWithCapacity(final int capacity, final float loadFactor) {
|
|
+ return new ConcurrentLong2ReferenceChainedHashTable<>(capacity, loadFactor);
|
|
+ }
|
|
+
|
|
+ public static <V> ConcurrentLong2ReferenceChainedHashTable<V> createWithExpected(final int expected) {
|
|
+ return createWithExpected(expected, DEFAULT_LOAD_FACTOR);
|
|
+ }
|
|
+
|
|
+ public static <V> ConcurrentLong2ReferenceChainedHashTable<V> createWithExpected(final int expected, final float loadFactor) {
|
|
+ final int capacity = (int)Math.ceil((double)expected / (double)loadFactor);
|
|
+
|
|
+ return createWithCapacity(capacity, loadFactor);
|
|
+ }
|
|
+
|
|
+ /** must be deterministic given a key */
|
|
+ protected static int getHash(final long key) {
|
|
+ return (int)HashUtil.mix(key);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Returns the load factor associated with this map.
|
|
+ */
|
|
+ public final float getLoadFactor() {
|
|
+ return this.loadFactor;
|
|
+ }
|
|
+
|
|
+ protected static <V> TableEntry<V> getAtIndexVolatile(final TableEntry<V>[] table, final int index) {
|
|
+ //noinspection unchecked
|
|
+ return (TableEntry<V>)TableEntry.TABLE_ENTRY_ARRAY_HANDLE.getVolatile(table, index);
|
|
+ }
|
|
+
|
|
+ protected static <V> void setAtIndexRelease(final TableEntry<V>[] table, final int index, final TableEntry<V> value) {
|
|
+ TableEntry.TABLE_ENTRY_ARRAY_HANDLE.setRelease(table, index, value);
|
|
+ }
|
|
+
|
|
+ protected static <V> void setAtIndexVolatile(final TableEntry<V>[] table, final int index, final TableEntry<V> value) {
|
|
+ TableEntry.TABLE_ENTRY_ARRAY_HANDLE.setVolatile(table, index, value);
|
|
+ }
|
|
+
|
|
+ protected static <V> TableEntry<V> compareAndExchangeAtIndexVolatile(final TableEntry<V>[] table, final int index,
|
|
+ final TableEntry<V> expect, final TableEntry<V> update) {
|
|
+ //noinspection unchecked
|
|
+ return (TableEntry<V>)TableEntry.TABLE_ENTRY_ARRAY_HANDLE.compareAndExchange(table, index, expect, update);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Returns the possible node associated with the key, or {@code null} if there is no such node. The node
|
|
+ * returned may have a {@code null} {@link TableEntry#value}, in which case the node is a placeholder for
|
|
+ * a compute/computeIfAbsent call. The placeholder node should not be considered mapped in order to preserve
|
|
+ * happens-before relationships between writes and reads in the map.
|
|
+ */
|
|
+ protected final TableEntry<V> getNode(final long key) {
|
|
+ final int hash = getHash(key);
|
|
+
|
|
+ TableEntry<V>[] table = this.table;
|
|
+ for (;;) {
|
|
+ TableEntry<V> node = getAtIndexVolatile(table, hash & (table.length - 1));
|
|
+
|
|
+ if (node == null) {
|
|
+ // node == null
|
|
+ return node;
|
|
+ }
|
|
+
|
|
+ if (node.resize) {
|
|
+ table = (TableEntry<V>[])node.getValuePlain();
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ for (; node != null; node = node.getNextVolatile()) {
|
|
+ if (node.key == key) {
|
|
+ return node;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // node == null
|
|
+ return node;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Returns the currently mapped value associated with the specified key, or {@code null} if there is none.
|
|
+ *
|
|
+ * @param key Specified key
|
|
+ */
|
|
+ public V get(final long key) {
|
|
+ final TableEntry<V> node = this.getNode(key);
|
|
+ return node == null ? null : node.getValueVolatile();
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Returns the currently mapped value associated with the specified key, or the specified default value if there is none.
|
|
+ *
|
|
+ * @param key Specified key
|
|
+ * @param defaultValue Specified default value
|
|
+ */
|
|
+ public V getOrDefault(final long key, final V defaultValue) {
|
|
+ final TableEntry<V> node = this.getNode(key);
|
|
+ if (node == null) {
|
|
+ return defaultValue;
|
|
+ }
|
|
+
|
|
+ final V ret = node.getValueVolatile();
|
|
+ if (ret == null) {
|
|
+ // ret == null for nodes pre-allocated to compute() and friends
|
|
+ return defaultValue;
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Returns whether the specified key is mapped to some value.
|
|
+ * @param key Specified key
|
|
+ */
|
|
+ public boolean containsKey(final long key) {
|
|
+ // cannot use getNode, as the node may be a placeholder for compute()
|
|
+ return this.get(key) != null;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Returns whether the specified value has a key mapped to it.
|
|
+ * @param value Specified value
|
|
+ * @throws NullPointerException If value is null
|
|
+ */
|
|
+ public boolean containsValue(final V value) {
|
|
+ Validate.notNull(value, "Value cannot be null");
|
|
+
|
|
+ final NodeIterator<V> iterator = new NodeIterator<>(this.table);
|
|
+
|
|
+ TableEntry<V> node;
|
|
+ while ((node = iterator.findNext()) != null) {
|
|
+ // need to use acquire here to ensure the happens-before relationship
|
|
+ if (node.getValueAcquire() == value) {
|
|
+ return true;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Returns the number of mappings in this map.
|
|
+ */
|
|
+ public int size() {
|
|
+ final long ret = this.size.sum();
|
|
+
|
|
+ if (ret <= 0L) {
|
|
+ return 0;
|
|
+ }
|
|
+ if (ret >= (long)Integer.MAX_VALUE) {
|
|
+ return Integer.MAX_VALUE;
|
|
+ }
|
|
+
|
|
+ return (int)ret;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Returns whether this map has no mappings.
|
|
+ */
|
|
+ public boolean isEmpty() {
|
|
+ return this.size.sum() <= 0L;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Adds count to size and checks threshold for resizing
|
|
+ */
|
|
+ protected final void addSize(final long count) {
|
|
+ this.size.add(count);
|
|
+
|
|
+ final int threshold = this.getThresholdAcquire();
|
|
+
|
|
+ if (threshold < 0L) {
|
|
+ // resizing or no resizing allowed, in either cases we do not need to do anything
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ final long sum = this.size.sum();
|
|
+
|
|
+ if (sum < (long)threshold) {
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ if (threshold != this.compareExchangeThresholdVolatile(threshold, THRESHOLD_RESIZING)) {
|
|
+ // some other thread resized
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ // create new table
|
|
+ this.resize(sum);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Resizes table, only invoke for the thread which has successfully updated threshold to {@link #THRESHOLD_RESIZING}
|
|
+ * @param sum Estimate of current mapping count, must be >= old threshold
|
|
+ */
|
|
+ private void resize(final long sum) {
|
|
+ int capacity;
|
|
+
|
|
+ // add 1.0, as sum may equal threshold (in which case, sum / loadFactor = current capacity)
|
|
+ // adding 1.0 should at least raise the size by a factor of two due to usage of roundCeilLog2
|
|
+ final double targetD = ((double)sum / (double)this.loadFactor) + 1.0;
|
|
+ if (targetD >= (double)MAXIMUM_CAPACITY) {
|
|
+ capacity = MAXIMUM_CAPACITY;
|
|
+ } else {
|
|
+ capacity = (int)Math.ceil(targetD);
|
|
+ capacity = IntegerUtil.roundCeilLog2(capacity);
|
|
+ if (capacity > MAXIMUM_CAPACITY) {
|
|
+ capacity = MAXIMUM_CAPACITY;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // create new table data
|
|
+
|
|
+ final TableEntry<V>[] newTable = new TableEntry[capacity];
|
|
+ // noinspection unchecked
|
|
+ final TableEntry<V> resizeNode = new TableEntry<>(0L, (V)newTable, true);
|
|
+
|
|
+ // transfer nodes from old table
|
|
+
|
|
+ // does not need to be volatile read, just plain
|
|
+ final TableEntry<V>[] oldTable = this.table;
|
|
+
|
|
+ // when resizing, the old entries at bin i (where i = hash % oldTable.length) are assigned to
|
|
+ // bin k in the new table (where k = hash % newTable.length)
|
|
+ // since both table lengths are powers of two (specifically, newTable is a multiple of oldTable),
|
|
+ // the possible number of locations in the new table to assign any given i is newTable.length/oldTable.length
|
|
+
|
|
+ // we can build the new linked nodes for the new table by using a work array sized to newTable.length/oldTable.length
|
|
+ // which holds the _last_ entry in the chain per bin
|
|
+
|
|
+ final int capOldShift = IntegerUtil.floorLog2(oldTable.length);
|
|
+ final int capDiffShift = IntegerUtil.floorLog2(capacity) - capOldShift;
|
|
+
|
|
+ if (capDiffShift == 0) {
|
|
+ throw new IllegalStateException("Resizing to same size");
|
|
+ }
|
|
+
|
|
+ final TableEntry<V>[] work = new TableEntry[1 << capDiffShift]; // typically, capDiffShift = 1
|
|
+
|
|
+ for (int i = 0, len = oldTable.length; i < len; ++i) {
|
|
+ TableEntry<V> binNode = getAtIndexVolatile(oldTable, i);
|
|
+
|
|
+ for (;;) {
|
|
+ if (binNode == null) {
|
|
+ // just need to replace the bin node, do not need to move anything
|
|
+ if (null == (binNode = compareAndExchangeAtIndexVolatile(oldTable, i, null, resizeNode))) {
|
|
+ break;
|
|
+ } // else: binNode != null, fall through
|
|
+ }
|
|
+
|
|
+ // need write lock to block other writers
|
|
+ synchronized (binNode) {
|
|
+ if (binNode != (binNode = getAtIndexVolatile(oldTable, i))) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ // an important detail of resizing is that we do not need to be concerned with synchronisation on
|
|
+ // writes to the new table, as no access to any nodes on bin i on oldTable will occur until a thread
|
|
+ // sees the resizeNode
|
|
+ // specifically, as long as the resizeNode is release written there are no cases where another thread
|
|
+ // will see our writes to the new table
|
|
+
|
|
+ TableEntry<V> next = binNode.getNextPlain();
|
|
+
|
|
+ if (next == null) {
|
|
+ // simple case: do not use work array
|
|
+
|
|
+ // do not need to create new node, readers only need to see the state of the map at the
|
|
+ // beginning of a call, so any additions onto _next_ don't really matter
|
|
+ // additionally, the old node is replaced so that writers automatically forward to the new table,
|
|
+ // which resolves any issues
|
|
+ newTable[getHash(binNode.key) & (capacity - 1)] = binNode;
|
|
+ } else {
|
|
+ // reset for next usage
|
|
+ Arrays.fill(work, null);
|
|
+
|
|
+ for (TableEntry<V> curr = binNode; curr != null; curr = curr.getNextPlain()) {
|
|
+ final int newTableIdx = getHash(curr.key) & (capacity - 1);
|
|
+ final int workIdx = newTableIdx >>> capOldShift;
|
|
+
|
|
+ final TableEntry<V> replace = new TableEntry<>(curr.key, curr.getValuePlain());
|
|
+
|
|
+ final TableEntry<V> workNode = work[workIdx];
|
|
+ work[workIdx] = replace;
|
|
+
|
|
+ if (workNode == null) {
|
|
+ newTable[newTableIdx] = replace;
|
|
+ continue;
|
|
+ } else {
|
|
+ workNode.setNextPlain(replace);
|
|
+ continue;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ setAtIndexRelease(oldTable, i, resizeNode);
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // calculate new threshold
|
|
+ final int newThreshold;
|
|
+ if (capacity == MAXIMUM_CAPACITY) {
|
|
+ newThreshold = THRESHOLD_NO_RESIZE;
|
|
+ } else {
|
|
+ newThreshold = getTargetThreshold(capacity, loadFactor);
|
|
+ }
|
|
+
|
|
+ this.table = newTable;
|
|
+ // finish resize operation by releasing hold on threshold
|
|
+ this.setThresholdVolatile(newThreshold);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Subtracts count from size
|
|
+ */
|
|
+ protected final void subSize(final long count) {
|
|
+ this.size.add(-count);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Atomically updates the value associated with {@code key} to {@code value}, or inserts a new mapping with {@code key}
|
|
+ * mapped to {@code value}.
|
|
+ * @param key Specified key
|
|
+ * @param value Specified value
|
|
+ * @throws NullPointerException If value is null
|
|
+ * @return Old value previously associated with key, or {@code null} if none.
|
|
+ */
|
|
+ public V put(final long key, final V value) {
|
|
+ Validate.notNull(value, "Value may not be null");
|
|
+
|
|
+ final int hash = getHash(key);
|
|
+
|
|
+ TableEntry<V>[] table = this.table;
|
|
+ table_loop:
|
|
+ for (;;) {
|
|
+ final int index = hash & (table.length - 1);
|
|
+
|
|
+ TableEntry<V> node = getAtIndexVolatile(table, index);
|
|
+ node_loop:
|
|
+ for (;;) {
|
|
+ if (node == null) {
|
|
+ if (null == (node = compareAndExchangeAtIndexVolatile(table, index, null, new TableEntry<>(key, value)))) {
|
|
+ // successfully inserted
|
|
+ this.addSize(1L);
|
|
+ return null;
|
|
+ } // else: node != null, fall through
|
|
+ }
|
|
+
|
|
+ if (node.resize) {
|
|
+ table = (TableEntry<V>[])node.getValuePlain();
|
|
+ continue table_loop;
|
|
+ }
|
|
+
|
|
+ synchronized (node) {
|
|
+ if (node != (node = getAtIndexVolatile(table, index))) {
|
|
+ continue node_loop;
|
|
+ }
|
|
+ // plain reads are fine during synchronised access, as we are the only writer
|
|
+ TableEntry<V> prev = null;
|
|
+ for (; node != null; prev = node, node = node.getNextPlain()) {
|
|
+ if (node.key == key) {
|
|
+ final V ret = node.getValuePlain();
|
|
+ node.setValueVolatile(value);
|
|
+ return ret;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // volatile ordering ensured by addSize(), but we need release here
|
|
+ // to ensure proper ordering with reads and other writes
|
|
+ prev.setNextRelease(new TableEntry<>(key, value));
|
|
+ }
|
|
+
|
|
+ this.addSize(1L);
|
|
+ return null;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Atomically inserts a new mapping with {@code key} mapped to {@code value} if and only if {@code key} is not
|
|
+ * currently mapped to some value.
|
|
+ * @param key Specified key
|
|
+ * @param value Specified value
|
|
+ * @throws NullPointerException If value is null
|
|
+ * @return Value currently associated with key, or {@code null} if none and {@code value} was associated.
|
|
+ */
|
|
+ public V putIfAbsent(final long key, final V value) {
|
|
+ Validate.notNull(value, "Value may not be null");
|
|
+
|
|
+ final int hash = getHash(key);
|
|
+
|
|
+ TableEntry<V>[] table = this.table;
|
|
+ table_loop:
|
|
+ for (;;) {
|
|
+ final int index = hash & (table.length - 1);
|
|
+
|
|
+ TableEntry<V> node = getAtIndexVolatile(table, index);
|
|
+ node_loop:
|
|
+ for (;;) {
|
|
+ if (node == null) {
|
|
+ if (null == (node = compareAndExchangeAtIndexVolatile(table, index, null, new TableEntry<>(key, value)))) {
|
|
+ // successfully inserted
|
|
+ this.addSize(1L);
|
|
+ return null;
|
|
+ } // else: node != null, fall through
|
|
+ }
|
|
+
|
|
+ if (node.resize) {
|
|
+ table = (TableEntry<V>[])node.getValuePlain();
|
|
+ continue table_loop;
|
|
+ }
|
|
+
|
|
+ // optimise ifAbsent calls: check if first node is key before attempting lock acquire
|
|
+ if (node.key == key) {
|
|
+ final V ret = node.getValueVolatile();
|
|
+ if (ret != null) {
|
|
+ return ret;
|
|
+ } // else: fall back to lock to read the node
|
|
+ }
|
|
+
|
|
+ synchronized (node) {
|
|
+ if (node != (node = getAtIndexVolatile(table, index))) {
|
|
+ continue node_loop;
|
|
+ }
|
|
+ // plain reads are fine during synchronised access, as we are the only writer
|
|
+ TableEntry<V> prev = null;
|
|
+ for (; node != null; prev = node, node = node.getNextPlain()) {
|
|
+ if (node.key == key) {
|
|
+ return node.getValuePlain();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // volatile ordering ensured by addSize(), but we need release here
|
|
+ // to ensure proper ordering with reads and other writes
|
|
+ prev.setNextRelease(new TableEntry<>(key, value));
|
|
+ }
|
|
+
|
|
+ this.addSize(1L);
|
|
+ return null;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Atomically updates the value associated with {@code key} to {@code value}, or does nothing if {@code key} is not
|
|
+ * associated with a value.
|
|
+ * @param key Specified key
|
|
+ * @param value Specified value
|
|
+ * @throws NullPointerException If value is null
|
|
+ * @return Old value previously associated with key, or {@code null} if none.
|
|
+ */
|
|
+ public V replace(final long key, final V value) {
|
|
+ Validate.notNull(value, "Value may not be null");
|
|
+
|
|
+ final int hash = getHash(key);
|
|
+
|
|
+ TableEntry<V>[] table = this.table;
|
|
+ table_loop:
|
|
+ for (;;) {
|
|
+ final int index = hash & (table.length - 1);
|
|
+
|
|
+ TableEntry<V> node = getAtIndexVolatile(table, index);
|
|
+ node_loop:
|
|
+ for (;;) {
|
|
+ if (node == null) {
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ if (node.resize) {
|
|
+ table = (TableEntry<V>[])node.getValuePlain();
|
|
+ continue table_loop;
|
|
+ }
|
|
+
|
|
+ synchronized (node) {
|
|
+ if (node != (node = getAtIndexVolatile(table, index))) {
|
|
+ continue node_loop;
|
|
+ }
|
|
+
|
|
+ // plain reads are fine during synchronised access, as we are the only writer
|
|
+ for (; node != null; node = node.getNextPlain()) {
|
|
+ if (node.key == key) {
|
|
+ final V ret = node.getValuePlain();
|
|
+ node.setValueVolatile(value);
|
|
+ return ret;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return null;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Atomically updates the value associated with {@code key} to {@code update} if the currently associated
|
|
+ * value is reference equal to {@code expect}, otherwise does nothing.
|
|
+ * @param key Specified key
|
|
+ * @param expect Expected value to check current mapped value with
|
|
+ * @param update Update value to replace mapped value with
|
|
+ * @throws NullPointerException If value is null
|
|
+ * @return If the currently mapped value is not reference equal to {@code expect}, then returns the currently mapped
|
|
+ * value. If the key is not mapped to any value, then returns {@code null}. If neither of the two cases are
|
|
+ * true, then returns {@code expect}.
|
|
+ */
|
|
+ public V replace(final long key, final V expect, final V update) {
|
|
+ Validate.notNull(expect, "Expect may not be null");
|
|
+ Validate.notNull(update, "Update may not be null");
|
|
+
|
|
+ final int hash = getHash(key);
|
|
+
|
|
+ TableEntry<V>[] table = this.table;
|
|
+ table_loop:
|
|
+ for (;;) {
|
|
+ final int index = hash & (table.length - 1);
|
|
+
|
|
+ TableEntry<V> node = getAtIndexVolatile(table, index);
|
|
+ node_loop:
|
|
+ for (;;) {
|
|
+ if (node == null) {
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ if (node.resize) {
|
|
+ table = (TableEntry<V>[])node.getValuePlain();
|
|
+ continue table_loop;
|
|
+ }
|
|
+
|
|
+ synchronized (node) {
|
|
+ if (node != (node = getAtIndexVolatile(table, index))) {
|
|
+ continue node_loop;
|
|
+ }
|
|
+
|
|
+ // plain reads are fine during synchronised access, as we are the only writer
|
|
+ for (; node != null; node = node.getNextPlain()) {
|
|
+ if (node.key == key) {
|
|
+ final V ret = node.getValuePlain();
|
|
+
|
|
+ if (ret != expect) {
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ node.setValueVolatile(update);
|
|
+ return ret;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return null;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Atomically removes the mapping for the specified key and returns the value it was associated with. If the key
|
|
+ * is not mapped to a value, then does nothing and returns {@code null}.
|
|
+ * @param key Specified key
|
|
+ * @return Old value previously associated with key, or {@code null} if none.
|
|
+ */
|
|
+ public V remove(final long key) {
|
|
+ final int hash = getHash(key);
|
|
+
|
|
+ TableEntry<V>[] table = this.table;
|
|
+ table_loop:
|
|
+ for (;;) {
|
|
+ final int index = hash & (table.length - 1);
|
|
+
|
|
+ TableEntry<V> node = getAtIndexVolatile(table, index);
|
|
+ node_loop:
|
|
+ for (;;) {
|
|
+ if (node == null) {
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ if (node.resize) {
|
|
+ table = (TableEntry<V>[])node.getValuePlain();
|
|
+ continue table_loop;
|
|
+ }
|
|
+
|
|
+ boolean removed = false;
|
|
+ V ret = null;
|
|
+
|
|
+ synchronized (node) {
|
|
+ if (node != (node = getAtIndexVolatile(table, index))) {
|
|
+ continue node_loop;
|
|
+ }
|
|
+
|
|
+ TableEntry<V> prev = null;
|
|
+
|
|
+ // plain reads are fine during synchronised access, as we are the only writer
|
|
+ for (; node != null; prev = node, node = node.getNextPlain()) {
|
|
+ if (node.key == key) {
|
|
+ ret = node.getValuePlain();
|
|
+ removed = true;
|
|
+
|
|
+ // volatile ordering ensured by addSize(), but we need release here
|
|
+ // to ensure proper ordering with reads and other writes
|
|
+ if (prev == null) {
|
|
+ setAtIndexRelease(table, index, node.getNextPlain());
|
|
+ } else {
|
|
+ prev.setNextRelease(node.getNextPlain());
|
|
+ }
|
|
+
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (removed) {
|
|
+ this.subSize(1L);
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Atomically removes the mapping for the specified key if it is mapped to {@code expect} and returns {@code expect}. If the key
|
|
+ * is not mapped to a value, then does nothing and returns {@code null}. If the key is mapped to a value that is not reference
|
|
+ * equal to {@code expect}, then returns that value.
|
|
+ * @param key Specified key
|
|
+ * @param expect Specified expected value
|
|
+ * @return The specified expected value if the key was mapped to {@code expect}. If
|
|
+ * the key is not mapped to any value, then returns {@code null}. If neither of those cases are true,
|
|
+ * then returns the current (non-null) mapped value for key.
|
|
+ */
|
|
+ public V remove(final long key, final V expect) {
|
|
+ final int hash = getHash(key);
|
|
+
|
|
+ TableEntry<V>[] table = this.table;
|
|
+ table_loop:
|
|
+ for (;;) {
|
|
+ final int index = hash & (table.length - 1);
|
|
+
|
|
+ TableEntry<V> node = getAtIndexVolatile(table, index);
|
|
+ node_loop:
|
|
+ for (;;) {
|
|
+ if (node == null) {
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ if (node.resize) {
|
|
+ table = (TableEntry<V>[])node.getValuePlain();
|
|
+ continue table_loop;
|
|
+ }
|
|
+
|
|
+ boolean removed = false;
|
|
+ V ret = null;
|
|
+
|
|
+ synchronized (node) {
|
|
+ if (node != (node = getAtIndexVolatile(table, index))) {
|
|
+ continue node_loop;
|
|
+ }
|
|
+
|
|
+ TableEntry<V> prev = null;
|
|
+
|
|
+ // plain reads are fine during synchronised access, as we are the only writer
|
|
+ for (; node != null; prev = node, node = node.getNextPlain()) {
|
|
+ if (node.key == key) {
|
|
+ ret = node.getValuePlain();
|
|
+ if (ret == expect) {
|
|
+ removed = true;
|
|
+
|
|
+ // volatile ordering ensured by addSize(), but we need release here
|
|
+ // to ensure proper ordering with reads and other writes
|
|
+ if (prev == null) {
|
|
+ setAtIndexRelease(table, index, node.getNextPlain());
|
|
+ } else {
|
|
+ prev.setNextRelease(node.getNextPlain());
|
|
+ }
|
|
+ }
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (removed) {
|
|
+ this.subSize(1L);
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Atomically removes the mapping for the specified key the predicate returns true for its currently mapped value. If the key
|
|
+ * is not mapped to a value, then does nothing and returns {@code null}.
|
|
+ *
|
|
+ * <p>
|
|
+ * This function is a "functional methods" as defined by {@link ConcurrentLong2ReferenceChainedHashTable}.
|
|
+ * </p>
|
|
+ *
|
|
+ * @param key Specified key
|
|
+ * @param predicate Specified predicate
|
|
+ * @throws NullPointerException If predicate is null
|
|
+ * @return The specified expected value if the key was mapped to {@code expect}. If
|
|
+ * the key is not mapped to any value, then returns {@code null}. If neither of those cases are true,
|
|
+ * then returns the current (non-null) mapped value for key.
|
|
+ */
|
|
+ public V removeIf(final long key, final Predicate<? super V> predicate) {
|
|
+ Validate.notNull(predicate, "Predicate may not be null");
|
|
+
|
|
+ final int hash = getHash(key);
|
|
+
|
|
+ TableEntry<V>[] table = this.table;
|
|
+ table_loop:
|
|
+ for (;;) {
|
|
+ final int index = hash & (table.length - 1);
|
|
+
|
|
+ TableEntry<V> node = getAtIndexVolatile(table, index);
|
|
+ node_loop:
|
|
+ for (;;) {
|
|
+ if (node == null) {
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ if (node.resize) {
|
|
+ table = (TableEntry<V>[])node.getValuePlain();
|
|
+ continue table_loop;
|
|
+ }
|
|
+
|
|
+ boolean removed = false;
|
|
+ V ret = null;
|
|
+
|
|
+ synchronized (node) {
|
|
+ if (node != (node = getAtIndexVolatile(table, index))) {
|
|
+ continue node_loop;
|
|
+ }
|
|
+
|
|
+ TableEntry<V> prev = null;
|
|
+
|
|
+ // plain reads are fine during synchronised access, as we are the only writer
|
|
+ for (; node != null; prev = node, node = node.getNextPlain()) {
|
|
+ if (node.key == key) {
|
|
+ ret = node.getValuePlain();
|
|
+ if (predicate.test(ret)) {
|
|
+ removed = true;
|
|
+
|
|
+ // volatile ordering ensured by addSize(), but we need release here
|
|
+ // to ensure proper ordering with reads and other writes
|
|
+ if (prev == null) {
|
|
+ setAtIndexRelease(table, index, node.getNextPlain());
|
|
+ } else {
|
|
+ prev.setNextRelease(node.getNextPlain());
|
|
+ }
|
|
+ }
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (removed) {
|
|
+ this.subSize(1L);
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * See {@link java.util.concurrent.ConcurrentMap#compute(Object, BiFunction)}
|
|
+ * <p>
|
|
+ * This function is a "functional methods" as defined by {@link ConcurrentLong2ReferenceChainedHashTable}.
|
|
+ * </p>
|
|
+ */
|
|
+ public V compute(final long key, final BiLong1Function<? super V, ? extends V> function) {
|
|
+ final int hash = getHash(key);
|
|
+
|
|
+ TableEntry<V>[] table = this.table;
|
|
+ table_loop:
|
|
+ for (;;) {
|
|
+ final int index = hash & (table.length - 1);
|
|
+
|
|
+ TableEntry<V> node = getAtIndexVolatile(table, index);
|
|
+ node_loop:
|
|
+ for (;;) {
|
|
+ V ret = null;
|
|
+ if (node == null) {
|
|
+ final TableEntry<V> insert = new TableEntry<>(key, null);
|
|
+
|
|
+ boolean added = false;
|
|
+
|
|
+ synchronized (insert) {
|
|
+ if (null == (node = compareAndExchangeAtIndexVolatile(table, index, null, insert))) {
|
|
+ try {
|
|
+ ret = function.apply(key, null);
|
|
+ } catch (final Throwable throwable) {
|
|
+ setAtIndexVolatile(table, index, null);
|
|
+ ThrowUtil.throwUnchecked(throwable);
|
|
+ // unreachable
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ if (ret == null) {
|
|
+ setAtIndexVolatile(table, index, null);
|
|
+ return ret;
|
|
+ } else {
|
|
+ // volatile ordering ensured by addSize(), but we need release here
|
|
+ // to ensure proper ordering with reads and other writes
|
|
+ insert.setValueRelease(ret);
|
|
+ added = true;
|
|
+ }
|
|
+ } // else: node != null, fall through
|
|
+ }
|
|
+
|
|
+ if (added) {
|
|
+ this.addSize(1L);
|
|
+ return ret;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (node.resize) {
|
|
+ table = (TableEntry<V>[])node.getValuePlain();
|
|
+ continue table_loop;
|
|
+ }
|
|
+
|
|
+ boolean removed = false;
|
|
+ boolean added = false;
|
|
+
|
|
+ synchronized (node) {
|
|
+ if (node != (node = getAtIndexVolatile(table, index))) {
|
|
+ continue node_loop;
|
|
+ }
|
|
+ // plain reads are fine during synchronised access, as we are the only writer
|
|
+ TableEntry<V> prev = null;
|
|
+ for (; node != null; prev = node, node = node.getNextPlain()) {
|
|
+ if (node.key == key) {
|
|
+ final V old = node.getValuePlain();
|
|
+
|
|
+ final V computed = function.apply(key, old);
|
|
+
|
|
+ if (computed != null) {
|
|
+ node.setValueVolatile(computed);
|
|
+ return computed;
|
|
+ }
|
|
+
|
|
+ // volatile ordering ensured by addSize(), but we need release here
|
|
+ // to ensure proper ordering with reads and other writes
|
|
+ if (prev == null) {
|
|
+ setAtIndexRelease(table, index, node.getNextPlain());
|
|
+ } else {
|
|
+ prev.setNextRelease(node.getNextPlain());
|
|
+ }
|
|
+
|
|
+ removed = true;
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (!removed) {
|
|
+ final V computed = function.apply(key, null);
|
|
+ if (computed != null) {
|
|
+ // volatile ordering ensured by addSize(), but we need release here
|
|
+ // to ensure proper ordering with reads and other writes
|
|
+ prev.setNextRelease(new TableEntry<>(key, computed));
|
|
+ ret = computed;
|
|
+ added = true;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (removed) {
|
|
+ this.subSize(1L);
|
|
+ }
|
|
+ if (added) {
|
|
+ this.addSize(1L);
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * See {@link java.util.concurrent.ConcurrentMap#computeIfAbsent(Object, Function)}
|
|
+ * <p>
|
|
+ * This function is a "functional methods" as defined by {@link ConcurrentLong2ReferenceChainedHashTable}.
|
|
+ * </p>
|
|
+ */
|
|
+ public V computeIfAbsent(final long key, final LongFunction<? extends V> function) {
|
|
+ final int hash = getHash(key);
|
|
+
|
|
+ TableEntry<V>[] table = this.table;
|
|
+ table_loop:
|
|
+ for (;;) {
|
|
+ final int index = hash & (table.length - 1);
|
|
+
|
|
+ TableEntry<V> node = getAtIndexVolatile(table, index);
|
|
+ node_loop:
|
|
+ for (;;) {
|
|
+ V ret = null;
|
|
+ if (node == null) {
|
|
+ final TableEntry<V> insert = new TableEntry<>(key, null);
|
|
+
|
|
+ boolean added = false;
|
|
+
|
|
+ synchronized (insert) {
|
|
+ if (null == (node = compareAndExchangeAtIndexVolatile(table, index, null, insert))) {
|
|
+ try {
|
|
+ ret = function.apply(key);
|
|
+ } catch (final Throwable throwable) {
|
|
+ setAtIndexVolatile(table, index, null);
|
|
+ ThrowUtil.throwUnchecked(throwable);
|
|
+ // unreachable
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ if (ret == null) {
|
|
+ setAtIndexVolatile(table, index, null);
|
|
+ return null;
|
|
+ } else {
|
|
+ // volatile ordering ensured by addSize(), but we need release here
|
|
+ // to ensure proper ordering with reads and other writes
|
|
+ insert.setValueRelease(ret);
|
|
+ added = true;
|
|
+ }
|
|
+ } // else: node != null, fall through
|
|
+ }
|
|
+
|
|
+ if (added) {
|
|
+ this.addSize(1L);
|
|
+ return ret;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (node.resize) {
|
|
+ table = (TableEntry<V>[])node.getValuePlain();
|
|
+ continue table_loop;
|
|
+ }
|
|
+
|
|
+ // optimise ifAbsent calls: check if first node is key before attempting lock acquire
|
|
+ if (node.key == key) {
|
|
+ ret = node.getValueVolatile();
|
|
+ if (ret != null) {
|
|
+ return ret;
|
|
+ } // else: fall back to lock to read the node
|
|
+ }
|
|
+
|
|
+ boolean added = false;
|
|
+
|
|
+ synchronized (node) {
|
|
+ if (node != (node = getAtIndexVolatile(table, index))) {
|
|
+ continue node_loop;
|
|
+ }
|
|
+ // plain reads are fine during synchronised access, as we are the only writer
|
|
+ TableEntry<V> prev = null;
|
|
+ for (; node != null; prev = node, node = node.getNextPlain()) {
|
|
+ if (node.key == key) {
|
|
+ ret = node.getValuePlain();
|
|
+ return ret;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ final V computed = function.apply(key);
|
|
+ if (computed != null) {
|
|
+ // volatile ordering ensured by addSize(), but we need release here
|
|
+ // to ensure proper ordering with reads and other writes
|
|
+ prev.setNextRelease(new TableEntry<>(key, computed));
|
|
+ ret = computed;
|
|
+ added = true;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (added) {
|
|
+ this.addSize(1L);
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * See {@link java.util.concurrent.ConcurrentMap#computeIfPresent(Object, BiFunction)}
|
|
+ * <p>
|
|
+ * This function is a "functional methods" as defined by {@link ConcurrentLong2ReferenceChainedHashTable}.
|
|
+ * </p>
|
|
+ */
|
|
+ public V computeIfPresent(final long key, final BiLong1Function<? super V, ? extends V> function) {
|
|
+ final int hash = getHash(key);
|
|
+
|
|
+ TableEntry<V>[] table = this.table;
|
|
+ table_loop:
|
|
+ for (;;) {
|
|
+ final int index = hash & (table.length - 1);
|
|
+
|
|
+ TableEntry<V> node = getAtIndexVolatile(table, index);
|
|
+ node_loop:
|
|
+ for (;;) {
|
|
+ if (node == null) {
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ if (node.resize) {
|
|
+ table = (TableEntry<V>[])node.getValuePlain();
|
|
+ continue table_loop;
|
|
+ }
|
|
+
|
|
+ boolean removed = false;
|
|
+
|
|
+ synchronized (node) {
|
|
+ if (node != (node = getAtIndexVolatile(table, index))) {
|
|
+ continue node_loop;
|
|
+ }
|
|
+ // plain reads are fine during synchronised access, as we are the only writer
|
|
+ TableEntry<V> prev = null;
|
|
+ for (; node != null; prev = node, node = node.getNextPlain()) {
|
|
+ if (node.key == key) {
|
|
+ final V old = node.getValuePlain();
|
|
+
|
|
+ final V computed = function.apply(key, old);
|
|
+
|
|
+ if (computed != null) {
|
|
+ node.setValueVolatile(computed);
|
|
+ return computed;
|
|
+ }
|
|
+
|
|
+ // volatile ordering ensured by addSize(), but we need release here
|
|
+ // to ensure proper ordering with reads and other writes
|
|
+ if (prev == null) {
|
|
+ setAtIndexRelease(table, index, node.getNextPlain());
|
|
+ } else {
|
|
+ prev.setNextRelease(node.getNextPlain());
|
|
+ }
|
|
+
|
|
+ removed = true;
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (removed) {
|
|
+ this.subSize(1L);
|
|
+ }
|
|
+
|
|
+ return null;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * See {@link java.util.concurrent.ConcurrentMap#merge(Object, Object, BiFunction)}
|
|
+ * <p>
|
|
+ * This function is a "functional methods" as defined by {@link ConcurrentLong2ReferenceChainedHashTable}.
|
|
+ * </p>
|
|
+ */
|
|
+ public V merge(final long key, final V def, final BiFunction<? super V, ? super V, ? extends V> function) {
|
|
+ Validate.notNull(def, "Default value may not be null");
|
|
+
|
|
+ final int hash = getHash(key);
|
|
+
|
|
+ TableEntry<V>[] table = this.table;
|
|
+ table_loop:
|
|
+ for (;;) {
|
|
+ final int index = hash & (table.length - 1);
|
|
+
|
|
+ TableEntry<V> node = getAtIndexVolatile(table, index);
|
|
+ node_loop:
|
|
+ for (;;) {
|
|
+ if (node == null) {
|
|
+ if (null == (node = compareAndExchangeAtIndexVolatile(table, index, null, new TableEntry<>(key, def)))) {
|
|
+ // successfully inserted
|
|
+ this.addSize(1L);
|
|
+ return def;
|
|
+ } // else: node != null, fall through
|
|
+ }
|
|
+
|
|
+ if (node.resize) {
|
|
+ table = (TableEntry<V>[])node.getValuePlain();
|
|
+ continue table_loop;
|
|
+ }
|
|
+
|
|
+ boolean removed = false;
|
|
+ boolean added = false;
|
|
+ V ret = null;
|
|
+
|
|
+ synchronized (node) {
|
|
+ if (node != (node = getAtIndexVolatile(table, index))) {
|
|
+ continue node_loop;
|
|
+ }
|
|
+ // plain reads are fine during synchronised access, as we are the only writer
|
|
+ TableEntry<V> prev = null;
|
|
+ for (; node != null; prev = node, node = node.getNextPlain()) {
|
|
+ if (node.key == key) {
|
|
+ final V old = node.getValuePlain();
|
|
+
|
|
+ final V computed = function.apply(old, def);
|
|
+
|
|
+ if (computed != null) {
|
|
+ node.setValueVolatile(computed);
|
|
+ return computed;
|
|
+ }
|
|
+
|
|
+ // volatile ordering ensured by addSize(), but we need release here
|
|
+ // to ensure proper ordering with reads and other writes
|
|
+ if (prev == null) {
|
|
+ setAtIndexRelease(table, index, node.getNextPlain());
|
|
+ } else {
|
|
+ prev.setNextRelease(node.getNextPlain());
|
|
+ }
|
|
+
|
|
+ removed = true;
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (!removed) {
|
|
+ // volatile ordering ensured by addSize(), but we need release here
|
|
+ // to ensure proper ordering with reads and other writes
|
|
+ prev.setNextRelease(new TableEntry<>(key, def));
|
|
+ ret = def;
|
|
+ added = true;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (removed) {
|
|
+ this.subSize(1L);
|
|
+ }
|
|
+ if (added) {
|
|
+ this.addSize(1L);
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Removes at least all entries currently mapped at the beginning of this call. May not remove entries added during
|
|
+ * this call. As a result, only if this map is not modified during the call, that all entries will be removed by
|
|
+ * the end of the call.
|
|
+ *
|
|
+ * <p>
|
|
+ * This function is not atomic.
|
|
+ * </p>
|
|
+ */
|
|
+ public void clear() {
|
|
+ // it is possible to optimise this to directly interact with the table,
|
|
+ // but we do need to be careful when interacting with resized tables,
|
|
+ // and the NodeIterator already does this logic
|
|
+ final NodeIterator<V> nodeIterator = new NodeIterator<>(this.table);
|
|
+
|
|
+ TableEntry<V> node;
|
|
+ while ((node = nodeIterator.findNext()) != null) {
|
|
+ this.remove(node.key);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Returns an iterator over the entries in this map. The iterator is only guaranteed to see entries that were
|
|
+ * added before the beginning of this call, but it may see entries added during.
|
|
+ */
|
|
+ public Iterator<TableEntry<V>> entryIterator() {
|
|
+ return new EntryIterator<>(this);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Returns an iterator over the keys in this map. The iterator is only guaranteed to see keys that were
|
|
+ * added before the beginning of this call, but it may see keys added during.
|
|
+ */
|
|
+ public PrimitiveIterator.OfLong keyIterator() {
|
|
+ return new KeyIterator<>(this);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Returns an iterator over the values in this map. The iterator is only guaranteed to see values that were
|
|
+ * added before the beginning of this call, but it may see values added during.
|
|
+ */
|
|
+ public Iterator<V> valueIterator() {
|
|
+ return new ValueIterator<>(this);
|
|
+ }
|
|
+
|
|
+ protected static final class EntryIterator<V> extends BaseIteratorImpl<V, TableEntry<V>> {
|
|
+
|
|
+ protected EntryIterator(final ConcurrentLong2ReferenceChainedHashTable<V> map) {
|
|
+ super(map);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public TableEntry<V> next() throws NoSuchElementException {
|
|
+ return this.nextNode();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void forEachRemaining(final Consumer<? super TableEntry<V>> action) {
|
|
+ Validate.notNull(action, "Action may not be null");
|
|
+ while (this.hasNext()) {
|
|
+ action.accept(this.next());
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ protected static final class KeyIterator<V> extends BaseIteratorImpl<V, Long> implements PrimitiveIterator.OfLong {
|
|
+
|
|
+ protected KeyIterator(final ConcurrentLong2ReferenceChainedHashTable<V> map) {
|
|
+ super(map);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public Long next() throws NoSuchElementException {
|
|
+ return Long.valueOf(this.nextNode().key);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public long nextLong() {
|
|
+ return this.nextNode().key;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void forEachRemaining(final Consumer<? super Long> action) {
|
|
+ Validate.notNull(action, "Action may not be null");
|
|
+
|
|
+ if (action instanceof LongConsumer longConsumer) {
|
|
+ this.forEachRemaining(longConsumer);
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ while (this.hasNext()) {
|
|
+ action.accept(this.next());
|
|
+ }
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void forEachRemaining(final LongConsumer action) {
|
|
+ Validate.notNull(action, "Action may not be null");
|
|
+ while (this.hasNext()) {
|
|
+ action.accept(this.nextLong());
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ protected static final class ValueIterator<V> extends BaseIteratorImpl<V, V> {
|
|
+
|
|
+ protected ValueIterator(final ConcurrentLong2ReferenceChainedHashTable<V> map) {
|
|
+ super(map);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public V next() throws NoSuchElementException {
|
|
+ return this.nextNode().getValueVolatile();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void forEachRemaining(final Consumer<? super V> action) {
|
|
+ Validate.notNull(action, "Action may not be null");
|
|
+ while (this.hasNext()) {
|
|
+ action.accept(this.next());
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ protected static abstract class BaseIteratorImpl<V, T> extends NodeIterator<V> implements Iterator<T> {
|
|
+
|
|
+ protected final ConcurrentLong2ReferenceChainedHashTable<V> map;
|
|
+ protected TableEntry<V> lastReturned;
|
|
+ protected TableEntry<V> nextToReturn;
|
|
+
|
|
+ protected BaseIteratorImpl(final ConcurrentLong2ReferenceChainedHashTable<V> map) {
|
|
+ super(map.table);
|
|
+ this.map = map;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public final boolean hasNext() {
|
|
+ if (this.nextToReturn != null) {
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ return (this.nextToReturn = this.findNext()) != null;
|
|
+ }
|
|
+
|
|
+ protected final TableEntry<V> nextNode() throws NoSuchElementException {
|
|
+ TableEntry<V> ret = this.nextToReturn;
|
|
+ if (ret != null) {
|
|
+ this.lastReturned = ret;
|
|
+ this.nextToReturn = null;
|
|
+ return ret;
|
|
+ }
|
|
+ ret = this.findNext();
|
|
+ if (ret != null) {
|
|
+ this.lastReturned = ret;
|
|
+ return ret;
|
|
+ }
|
|
+ throw new NoSuchElementException();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public final void remove() {
|
|
+ final TableEntry<V> lastReturned = this.nextToReturn;
|
|
+ if (lastReturned == null) {
|
|
+ throw new NoSuchElementException();
|
|
+ }
|
|
+ this.lastReturned = null;
|
|
+ this.map.remove(lastReturned.key);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public abstract T next() throws NoSuchElementException;
|
|
+
|
|
+ // overwritten by subclasses to avoid indirection on hasNext() and next()
|
|
+ @Override
|
|
+ public abstract void forEachRemaining(final Consumer<? super T> action);
|
|
+ }
|
|
+
|
|
+ protected static class NodeIterator<V> {
|
|
+
|
|
+ protected TableEntry<V>[] currentTable;
|
|
+ protected ResizeChain<V> resizeChain;
|
|
+ protected TableEntry<V> last;
|
|
+ protected int nextBin;
|
|
+ protected int increment;
|
|
+
|
|
+ protected NodeIterator(final TableEntry<V>[] baseTable) {
|
|
+ this.currentTable = baseTable;
|
|
+ this.increment = 1;
|
|
+ }
|
|
+
|
|
+ private TableEntry<V>[] pullResizeChain(final int index) {
|
|
+ final ResizeChain<V> resizeChain = this.resizeChain;
|
|
+ if (resizeChain == null) {
|
|
+ this.currentTable = null;
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ final ResizeChain<V> prevChain = resizeChain.prev;
|
|
+ this.resizeChain = prevChain;
|
|
+ if (prevChain == null) {
|
|
+ this.currentTable = null;
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ final TableEntry<V>[] newTable = prevChain.table;
|
|
+
|
|
+ // we recover the original index by modding by the new table length, as the increments applied to the index
|
|
+ // are a multiple of the new table's length
|
|
+ int newIdx = index & (newTable.length - 1);
|
|
+
|
|
+ // the increment is always the previous table's length
|
|
+ final ResizeChain<V> nextPrevChain = prevChain.prev;
|
|
+ final int increment;
|
|
+ if (nextPrevChain == null) {
|
|
+ increment = 1;
|
|
+ } else {
|
|
+ increment = nextPrevChain.table.length;
|
|
+ }
|
|
+
|
|
+ // done with the upper table, so we can skip the resize node
|
|
+ newIdx += increment;
|
|
+
|
|
+ this.increment = increment;
|
|
+ this.nextBin = newIdx;
|
|
+ this.currentTable = newTable;
|
|
+
|
|
+ return newTable;
|
|
+ }
|
|
+
|
|
+ private TableEntry<V>[] pushResizeChain(final TableEntry<V>[] table, final TableEntry<V> entry) {
|
|
+ final ResizeChain<V> chain = this.resizeChain;
|
|
+
|
|
+ if (chain == null) {
|
|
+ final TableEntry<V>[] nextTable = (TableEntry<V>[])entry.getValuePlain();
|
|
+
|
|
+ final ResizeChain<V> oldChain = new ResizeChain<>(table, null, null);
|
|
+ final ResizeChain<V> currChain = new ResizeChain<>(nextTable, oldChain, null);
|
|
+ oldChain.next = currChain;
|
|
+
|
|
+ this.increment = table.length;
|
|
+ this.resizeChain = currChain;
|
|
+ this.currentTable = nextTable;
|
|
+
|
|
+ return nextTable;
|
|
+ } else {
|
|
+ ResizeChain<V> currChain = chain.next;
|
|
+ if (currChain == null) {
|
|
+ final TableEntry<V>[] ret = (TableEntry<V>[])entry.getValuePlain();
|
|
+ currChain = new ResizeChain<>(ret, chain, null);
|
|
+ chain.next = currChain;
|
|
+
|
|
+ this.increment = table.length;
|
|
+ this.resizeChain = currChain;
|
|
+ this.currentTable = ret;
|
|
+
|
|
+ return ret;
|
|
+ } else {
|
|
+ this.increment = table.length;
|
|
+ this.resizeChain = currChain;
|
|
+ return this.currentTable = currChain.table;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ protected final TableEntry<V> findNext() {
|
|
+ for (;;) {
|
|
+ final TableEntry<V> last = this.last;
|
|
+ if (last != null) {
|
|
+ final TableEntry<V> next = last.getNextVolatile();
|
|
+ if (next != null) {
|
|
+ this.last = next;
|
|
+ if (next.getValuePlain() == null) {
|
|
+ // compute() node not yet available
|
|
+ continue;
|
|
+ }
|
|
+ return next;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ TableEntry<V>[] table = this.currentTable;
|
|
+
|
|
+ if (table == null) {
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ int idx = this.nextBin;
|
|
+ int increment = this.increment;
|
|
+ for (;;) {
|
|
+ if (idx >= table.length) {
|
|
+ table = this.pullResizeChain(idx);
|
|
+ idx = this.nextBin;
|
|
+ increment = this.increment;
|
|
+ if (table != null) {
|
|
+ continue;
|
|
+ } else {
|
|
+ this.last = null;
|
|
+ return null;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ final TableEntry<V> entry = getAtIndexVolatile(table, idx);
|
|
+ if (entry == null) {
|
|
+ idx += increment;
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ if (entry.resize) {
|
|
+ // push onto resize chain
|
|
+ table = this.pushResizeChain(table, entry);
|
|
+ increment = this.increment;
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ this.last = entry;
|
|
+ this.nextBin = idx + increment;
|
|
+ if (entry.getValuePlain() != null) {
|
|
+ return entry;
|
|
+ } else {
|
|
+ // compute() node not yet available
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ protected static final class ResizeChain<V> {
|
|
+
|
|
+ protected final TableEntry<V>[] table;
|
|
+ protected final ResizeChain<V> prev;
|
|
+ protected ResizeChain<V> next;
|
|
+
|
|
+ protected ResizeChain(final TableEntry<V>[] table, final ResizeChain<V> prev, final ResizeChain<V> next) {
|
|
+ this.table = table;
|
|
+ this.prev = prev;
|
|
+ this.next = next;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public static final class TableEntry<V> {
|
|
+
|
|
+ protected static final VarHandle TABLE_ENTRY_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(TableEntry[].class);
|
|
+
|
|
+ protected final boolean resize;
|
|
+
|
|
+ protected final long key;
|
|
+
|
|
+ protected volatile V value;
|
|
+ protected static final VarHandle VALUE_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "value", Object.class);
|
|
+
|
|
+ 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 V getValueVolatile() {
|
|
+ //noinspection unchecked
|
|
+ return (V)VALUE_HANDLE.getVolatile(this);
|
|
+ }
|
|
+
|
|
+ protected final void setValuePlain(final V value) {
|
|
+ VALUE_HANDLE.set(this, (Object)value);
|
|
+ }
|
|
+
|
|
+ protected final void setValueRelease(final V value) {
|
|
+ VALUE_HANDLE.setRelease(this, (Object)value);
|
|
+ }
|
|
+
|
|
+ protected final void setValueVolatile(final V value) {
|
|
+ VALUE_HANDLE.setVolatile(this, (Object)value);
|
|
+ }
|
|
+
|
|
+ protected volatile TableEntry<V> next;
|
|
+ protected static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "next", TableEntry.class);
|
|
+
|
|
+ protected final TableEntry<V> getNextPlain() {
|
|
+ //noinspection unchecked
|
|
+ return (TableEntry<V>)NEXT_HANDLE.get(this);
|
|
+ }
|
|
+
|
|
+ protected final TableEntry<V> getNextVolatile() {
|
|
+ //noinspection unchecked
|
|
+ return (TableEntry<V>)NEXT_HANDLE.getVolatile(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 final void setNextVolatile(final TableEntry<V> next) {
|
|
+ NEXT_HANDLE.setVolatile(this, next);
|
|
+ }
|
|
+
|
|
+ public TableEntry(final long key, final V value) {
|
|
+ this.resize = false;
|
|
+ this.key = key;
|
|
+ this.setValuePlain(value);
|
|
+ }
|
|
+
|
|
+ public TableEntry(final long key, final V value, final boolean resize) {
|
|
+ this.resize = resize;
|
|
+ this.key = key;
|
|
+ this.setValuePlain(value);
|
|
+ }
|
|
+
|
|
+ public long getKey() {
|
|
+ return this.key;
|
|
+ }
|
|
+
|
|
+ public V getValue() {
|
|
+ return this.getValueVolatile();
|
|
+ }
|
|
+ }
|
|
+}
|
|
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..83965350d292ccf42a34520d84dcda3f88146cff
|
|
--- /dev/null
|
|
+++ b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRHashTable.java
|
|
@@ -0,0 +1,1656 @@
|
|
+package ca.spottedleaf.concurrentutil.map;
|
|
+
|
|
+import ca.spottedleaf.concurrentutil.util.CollectionUtil;
|
|
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
|
|
+import ca.spottedleaf.concurrentutil.util.HashUtil;
|
|
+import ca.spottedleaf.concurrentutil.util.IntegerUtil;
|
|
+import ca.spottedleaf.concurrentutil.util.Validate;
|
|
+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);
|
|
+ }
|
|
+
|
|
+ protected static <K, V> TableEntry<K, V> getAtIndexOpaque(final TableEntry<K, V>[] table, final int index) {
|
|
+ // noinspection unchecked
|
|
+ return (TableEntry<K, V>)TableEntry.TABLE_ENTRY_ARRAY_HANDLE.getOpaque(table, index);
|
|
+ }
|
|
+
|
|
+ protected static <K, V> void setAtIndexRelease(final TableEntry<K, V>[] table, final int index, final TableEntry<K, V> value) {
|
|
+ TableEntry.TABLE_ENTRY_ARRAY_HANDLE.setRelease(table, index, value);
|
|
+ }
|
|
+
|
|
+ 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 = getAtIndexOpaque(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();
|
|
+ return HashUtil.mix(hash);
|
|
+ }
|
|
+
|
|
+ // 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<?, ?> other)) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ 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 = getAtIndexOpaque(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 = getAtIndexOpaque(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("SWMRHashTable:{");
|
|
+
|
|
+ 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<Map.Entry<K, V>> iterator() {
|
|
+ return new EntryIterator<>(this.getTableAcquire(), this);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ @Override
|
|
+ public void forEach(final Consumer<? super Map.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 = getAtIndexOpaque(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 = getAtIndexOpaque(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 = getAtIndexOpaque(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 = getAtIndexOpaque(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 = getAtIndexOpaque(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 KeySet<K, V> keyset;
|
|
+ protected ValueCollection<K, V> values;
|
|
+ protected EntrySet<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);
|
|
+ setAtIndexRelease(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 */
|
|
+
|
|
+ setAtIndexRelease(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 Map.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 */
|
|
+
|
|
+ setAtIndexRelease(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;
|
|
+ }
|
|
+
|
|
+ setAtIndexRelease(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))) {
|
|
+ setAtIndexRelease(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) {
|
|
+ setAtIndexRelease(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) {
|
|
+ setAtIndexRelease(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) {
|
|
+ setAtIndexRelease(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) {
|
|
+ setAtIndexRelease(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) {
|
|
+ setAtIndexRelease(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) {
|
|
+ setAtIndexRelease(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 static final VarHandle TABLE_ENTRY_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(TableEntry[].class);
|
|
+
|
|
+ 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;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public K getKey() {
|
|
+ return this.key;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public V getValue() {
|
|
+ return this.getValueAcquire();
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public V setValue(final V value) {
|
|
+ throw new UnsupportedOperationException();
|
|
+ }
|
|
+
|
|
+ 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<?, ?> other)) {
|
|
+ return false;
|
|
+ }
|
|
+ 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 = getAtIndexOpaque(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 = getAtIndexOpaque(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<?, ?> entry)) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ 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<Map.Entry<K, V>> iterator() {
|
|
+ return new EntryIterator<>(this.map.getTableAcquire(), this.map);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void forEach(final Consumer<? super Map.Entry<K, V>> action) {
|
|
+ this.map.forEach(action);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean contains(final Object object) {
|
|
+ if (!(object instanceof Map.Entry<?, ?> entry)) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ 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..bb301a9f4e3ac919552eef68afc73569d50674db
|
|
--- /dev/null
|
|
+++ b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRLong2ObjectHashTable.java
|
|
@@ -0,0 +1,674 @@
|
|
+package ca.spottedleaf.concurrentutil.map;
|
|
+
|
|
+import ca.spottedleaf.concurrentutil.function.BiLongObjectConsumer;
|
|
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
|
|
+import ca.spottedleaf.concurrentutil.util.HashUtil;
|
|
+import ca.spottedleaf.concurrentutil.util.IntegerUtil;
|
|
+import ca.spottedleaf.concurrentutil.util.Validate;
|
|
+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);
|
|
+ }
|
|
+
|
|
+ protected static <V> TableEntry<V> getAtIndexOpaque(final TableEntry<V>[] table, final int index) {
|
|
+ // noinspection unchecked
|
|
+ return (TableEntry<V>)TableEntry.TABLE_ENTRY_ARRAY_HANDLE.getOpaque(table, index);
|
|
+ }
|
|
+
|
|
+ protected static <V> void setAtIndexRelease(final TableEntry<V>[] table, final int index, final TableEntry<V> value) {
|
|
+ TableEntry.TABLE_ENTRY_ARRAY_HANDLE.setRelease(table, index, value);
|
|
+ }
|
|
+
|
|
+ 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 = getAtIndexOpaque(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)HashUtil.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<?> other)) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ 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 = getAtIndexOpaque(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 = getAtIndexOpaque(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 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 = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
|
|
+ action.accept(curr);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@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 = getAtIndexOpaque(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 = getAtIndexOpaque(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 = getAtIndexOpaque(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);
|
|
+ setAtIndexRelease(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) {
|
|
+ setAtIndexRelease(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;
|
|
+ }
|
|
+
|
|
+ protected final V remove(final long key, final int hash, final V expect) {
|
|
+ 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) {
|
|
+ final V val = head.value;
|
|
+ if (val == expect || val.equals(expect)) {
|
|
+ setAtIndexRelease(table, index, head.getNextPlain());
|
|
+ this.removeFromSize(1);
|
|
+
|
|
+ return head.getValuePlain();
|
|
+ } else {
|
|
+ return null;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ for (TableEntry<V> curr = head.getNextPlain(), prev = head; curr != null; prev = curr, curr = curr.getNextPlain()) {
|
|
+ if (key == curr.key) {
|
|
+ final V val = curr.value;
|
|
+ if (val == expect || val.equals(expect)) {
|
|
+ prev.setNextRelease(curr.getNextPlain());
|
|
+ this.removeFromSize(1);
|
|
+
|
|
+ return curr.getValuePlain();
|
|
+ } else {
|
|
+ return null;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@inheritDoc}
|
|
+ */
|
|
+ public V remove(final long key) {
|
|
+ return this.remove(key, SWMRLong2ObjectHashTable.getHash(key));
|
|
+ }
|
|
+
|
|
+ public boolean remove(final long key, final V expect) {
|
|
+ return this.remove(key, SWMRLong2ObjectHashTable.getHash(key), expect) != null;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * {@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 static final VarHandle TABLE_ENTRY_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(TableEntry[].class);
|
|
+
|
|
+ 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();
|
|
+ }
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/scheduler/SchedulerThreadPool.java b/src/main/java/ca/spottedleaf/concurrentutil/scheduler/SchedulerThreadPool.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..8197ccb1c4e5878dbd8007b5fb514640765ec8e4
|
|
--- /dev/null
|
|
+++ b/src/main/java/ca/spottedleaf/concurrentutil/scheduler/SchedulerThreadPool.java
|
|
@@ -0,0 +1,558 @@
|
|
+package ca.spottedleaf.concurrentutil.scheduler;
|
|
+
|
|
+import ca.spottedleaf.concurrentutil.set.LinkedSortedSet;
|
|
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
|
|
+import ca.spottedleaf.concurrentutil.util.TimeUtil;
|
|
+import java.lang.invoke.VarHandle;
|
|
+import java.util.BitSet;
|
|
+import java.util.Comparator;
|
|
+import java.util.PriorityQueue;
|
|
+import java.util.concurrent.ThreadFactory;
|
|
+import java.util.concurrent.atomic.AtomicInteger;
|
|
+import java.util.concurrent.atomic.AtomicLong;
|
|
+import java.util.concurrent.locks.LockSupport;
|
|
+import java.util.function.BooleanSupplier;
|
|
+
|
|
+public class SchedulerThreadPool {
|
|
+
|
|
+ public static final long DEADLINE_NOT_SET = Long.MIN_VALUE;
|
|
+
|
|
+ private static final Comparator<SchedulableTick> TICK_COMPARATOR_BY_TIME = (final SchedulableTick t1, final SchedulableTick t2) -> {
|
|
+ final int timeCompare = TimeUtil.compareTimes(t1.scheduledStart, t2.scheduledStart);
|
|
+ if (timeCompare != 0) {
|
|
+ return timeCompare;
|
|
+ }
|
|
+
|
|
+ return Long.compare(t1.id, t2.id);
|
|
+ };
|
|
+
|
|
+ private final TickThreadRunner[] runners;
|
|
+ private final Thread[] threads;
|
|
+ private final LinkedSortedSet<SchedulableTick> awaiting = new LinkedSortedSet<>(TICK_COMPARATOR_BY_TIME);
|
|
+ private final PriorityQueue<SchedulableTick> queued = new PriorityQueue<>(TICK_COMPARATOR_BY_TIME);
|
|
+ private final BitSet idleThreads;
|
|
+
|
|
+ private final Object scheduleLock = new Object();
|
|
+
|
|
+ private volatile boolean halted;
|
|
+
|
|
+ /**
|
|
+ * Creates, but does not start, a scheduler thread pool with the specified number of threads
|
|
+ * created using the specified thread factory.
|
|
+ * @param threads Specified number of threads
|
|
+ * @param threadFactory Specified thread factory
|
|
+ * @see #start()
|
|
+ */
|
|
+ public SchedulerThreadPool(final int threads, final ThreadFactory threadFactory) {
|
|
+ final BitSet idleThreads = new BitSet(threads);
|
|
+ for (int i = 0; i < threads; ++i) {
|
|
+ idleThreads.set(i);
|
|
+ }
|
|
+ this.idleThreads = idleThreads;
|
|
+
|
|
+ final TickThreadRunner[] runners = new TickThreadRunner[threads];
|
|
+ final Thread[] t = new Thread[threads];
|
|
+ for (int i = 0; i < threads; ++i) {
|
|
+ runners[i] = new TickThreadRunner(i, this);
|
|
+ t[i] = threadFactory.newThread(runners[i]);
|
|
+ }
|
|
+
|
|
+ this.threads = t;
|
|
+ this.runners = runners;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Starts the threads in this pool.
|
|
+ */
|
|
+ public void start() {
|
|
+ for (final Thread thread : this.threads) {
|
|
+ thread.start();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Attempts to prevent further execution of tasks, optionally waiting for the scheduler threads to die.
|
|
+ *
|
|
+ * @param sync Whether to wait for the scheduler threads to die.
|
|
+ * @param maxWaitNS The maximum time, in ns, to wait for the scheduler threads to die.
|
|
+ * @return {@code true} if sync was false, or if sync was true and the scheduler threads died before the timeout.
|
|
+ * Otherwise, returns {@code false} if the time elapsed exceeded the maximum wait time.
|
|
+ */
|
|
+ public boolean halt(final boolean sync, final long maxWaitNS) {
|
|
+ this.halted = true;
|
|
+ for (final Thread thread : this.threads) {
|
|
+ // force response to halt
|
|
+ LockSupport.unpark(thread);
|
|
+ }
|
|
+ final long time = System.nanoTime();
|
|
+ if (sync) {
|
|
+ // start at 10 * 0.5ms -> 5ms
|
|
+ for (long failures = 9L;; failures = ConcurrentUtil.linearLongBackoff(failures, 500_000L, 50_000_000L)) {
|
|
+ boolean allDead = true;
|
|
+ for (final Thread thread : this.threads) {
|
|
+ if (thread.isAlive()) {
|
|
+ allDead = false;
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+ if (allDead) {
|
|
+ return true;
|
|
+ }
|
|
+ if ((System.nanoTime() - time) >= maxWaitNS) {
|
|
+ return false;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Returns an array of the underlying scheduling threads.
|
|
+ */
|
|
+ public Thread[] getThreads() {
|
|
+ return this.threads.clone();
|
|
+ }
|
|
+
|
|
+ private void insertFresh(final SchedulableTick task) {
|
|
+ final TickThreadRunner[] runners = this.runners;
|
|
+
|
|
+ final int firstIdleThread = this.idleThreads.nextSetBit(0);
|
|
+
|
|
+ if (firstIdleThread != -1) {
|
|
+ // push to idle thread
|
|
+ this.idleThreads.clear(firstIdleThread);
|
|
+ final TickThreadRunner runner = runners[firstIdleThread];
|
|
+ task.awaitingLink = this.awaiting.addLast(task);
|
|
+ runner.acceptTask(task);
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ // try to replace the last awaiting task
|
|
+ final SchedulableTick last = this.awaiting.last();
|
|
+
|
|
+ if (last != null && TICK_COMPARATOR_BY_TIME.compare(task, last) < 0) {
|
|
+ // need to replace the last task
|
|
+ this.awaiting.pollLast();
|
|
+ last.awaitingLink = null;
|
|
+ task.awaitingLink = this.awaiting.addLast(task);
|
|
+ // need to add task to queue to be picked up later
|
|
+ this.queued.add(last);
|
|
+
|
|
+ final TickThreadRunner runner = last.ownedBy;
|
|
+ runner.replaceTask(task);
|
|
+
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ // add to queue, will be picked up later
|
|
+ this.queued.add(task);
|
|
+ }
|
|
+
|
|
+ private void takeTask(final TickThreadRunner runner, final SchedulableTick tick) {
|
|
+ if (!this.awaiting.remove(tick.awaitingLink)) {
|
|
+ throw new IllegalStateException("Task is not in awaiting");
|
|
+ }
|
|
+ tick.awaitingLink = null;
|
|
+ }
|
|
+
|
|
+ private SchedulableTick returnTask(final TickThreadRunner runner, final SchedulableTick reschedule) {
|
|
+ if (reschedule != null) {
|
|
+ this.queued.add(reschedule);
|
|
+ }
|
|
+ final SchedulableTick ret = this.queued.poll();
|
|
+ if (ret == null) {
|
|
+ this.idleThreads.set(runner.id);
|
|
+ } else {
|
|
+ ret.awaitingLink = this.awaiting.addLast(ret);
|
|
+ }
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Schedules the specified task to be executed on this thread pool.
|
|
+ * @param task Specified task
|
|
+ * @throws IllegalStateException If the task is already scheduled
|
|
+ * @see SchedulableTick
|
|
+ */
|
|
+ public void schedule(final SchedulableTick task) {
|
|
+ synchronized (this.scheduleLock) {
|
|
+ if (!task.tryMarkScheduled()) {
|
|
+ throw new IllegalStateException("Task " + task + " is already scheduled or cancelled");
|
|
+ }
|
|
+
|
|
+ task.schedulerOwnedBy = this;
|
|
+
|
|
+ this.insertFresh(task);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Updates the tasks scheduled start to the maximum of its current scheduled start and the specified
|
|
+ * new start. If the task is not scheduled, returns {@code false}. Otherwise, returns whether the
|
|
+ * scheduled start was updated. Undefined behavior of the specified task is scheduled in another executor.
|
|
+ * @param task Specified task
|
|
+ * @param newStart Specified new start
|
|
+ */
|
|
+ public boolean updateTickStartToMax(final SchedulableTick task, final long newStart) {
|
|
+ synchronized (this.scheduleLock) {
|
|
+ if (TimeUtil.compareTimes(newStart, task.getScheduledStart()) <= 0) {
|
|
+ return false;
|
|
+ }
|
|
+ if (this.queued.remove(task)) {
|
|
+ task.setScheduledStart(newStart);
|
|
+ this.queued.add(task);
|
|
+ return true;
|
|
+ }
|
|
+ if (task.awaitingLink != null) {
|
|
+ this.awaiting.remove(task.awaitingLink);
|
|
+ task.awaitingLink = null;
|
|
+
|
|
+ // re-queue task
|
|
+ task.setScheduledStart(newStart);
|
|
+ this.queued.add(task);
|
|
+
|
|
+ // now we need to replace the task the runner was waiting for
|
|
+ final TickThreadRunner runner = task.ownedBy;
|
|
+ final SchedulableTick replace = this.queued.poll();
|
|
+
|
|
+ // replace cannot be null, since we have added a task to queued
|
|
+ if (replace != task) {
|
|
+ runner.replaceTask(replace);
|
|
+ }
|
|
+
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ return false;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Returns {@code null} if the task is not scheduled, returns {@code TRUE} if the task was cancelled
|
|
+ * and was queued to execute, returns {@code FALSE} if the task was cancelled but was executing.
|
|
+ */
|
|
+ public Boolean tryRetire(final SchedulableTick task) {
|
|
+ if (task.schedulerOwnedBy != this) {
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ synchronized (this.scheduleLock) {
|
|
+ if (this.queued.remove(task)) {
|
|
+ // cancelled, and no runner owns it - so return
|
|
+ return Boolean.TRUE;
|
|
+ }
|
|
+ if (task.awaitingLink != null) {
|
|
+ this.awaiting.remove(task.awaitingLink);
|
|
+ task.awaitingLink = null;
|
|
+ // here we need to replace the task the runner was waiting for
|
|
+ final TickThreadRunner runner = task.ownedBy;
|
|
+ final SchedulableTick replace = this.queued.poll();
|
|
+
|
|
+ if (replace == null) {
|
|
+ // nothing to replace with, set to idle
|
|
+ this.idleThreads.set(runner.id);
|
|
+ runner.forceIdle();
|
|
+ } else {
|
|
+ runner.replaceTask(replace);
|
|
+ }
|
|
+
|
|
+ return Boolean.TRUE;
|
|
+ }
|
|
+
|
|
+ // could not find it in queue
|
|
+ return task.tryMarkCancelled() ? Boolean.FALSE : null;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Indicates that intermediate tasks are available to be executed by the task.
|
|
+ * <p>
|
|
+ * Note: currently a no-op
|
|
+ * </p>
|
|
+ * @param task The specified task
|
|
+ * @see SchedulableTick
|
|
+ */
|
|
+ public void notifyTasks(final SchedulableTick task) {
|
|
+ // Not implemented
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Represents a tickable task that can be scheduled into a {@link SchedulerThreadPool}.
|
|
+ * <p>
|
|
+ * A tickable task is expected to run on a fixed interval, which is determined by
|
|
+ * the {@link SchedulerThreadPool}.
|
|
+ * </p>
|
|
+ * <p>
|
|
+ * A tickable task can have intermediate tasks that can be executed before its tick method is ran. Instead of
|
|
+ * the {@link SchedulerThreadPool} parking in-between ticks, the scheduler will instead drain
|
|
+ * intermediate tasks from scheduled tasks. The parsing of intermediate tasks allows the scheduler to take
|
|
+ * advantage of downtime to reduce the intermediate task load from tasks once they begin ticking.
|
|
+ * </p>
|
|
+ * <p>
|
|
+ * It is guaranteed that {@link #runTick()} and {@link #runTasks(BooleanSupplier)} are never
|
|
+ * invoked in parallel.
|
|
+ * It is required that when intermediate tasks are scheduled, that {@link SchedulerThreadPool#notifyTasks(SchedulableTick)}
|
|
+ * is invoked for any scheduled task - otherwise, {@link #runTasks(BooleanSupplier)} may not be invoked to
|
|
+ * parse intermediate tasks.
|
|
+ * </p>
|
|
+ */
|
|
+ public static abstract class SchedulableTick {
|
|
+ private static final AtomicLong ID_GENERATOR = new AtomicLong();
|
|
+ public final long id = ID_GENERATOR.getAndIncrement();
|
|
+
|
|
+ private static final int SCHEDULE_STATE_NOT_SCHEDULED = 0;
|
|
+ private static final int SCHEDULE_STATE_SCHEDULED = 1;
|
|
+ private static final int SCHEDULE_STATE_CANCELLED = 2;
|
|
+
|
|
+ private final AtomicInteger scheduled = new AtomicInteger();
|
|
+ private SchedulerThreadPool schedulerOwnedBy;
|
|
+ private long scheduledStart = DEADLINE_NOT_SET;
|
|
+ private TickThreadRunner ownedBy;
|
|
+
|
|
+ private LinkedSortedSet.Link<SchedulableTick> awaitingLink;
|
|
+
|
|
+ private boolean tryMarkScheduled() {
|
|
+ return this.scheduled.compareAndSet(SCHEDULE_STATE_NOT_SCHEDULED, SCHEDULE_STATE_SCHEDULED);
|
|
+ }
|
|
+
|
|
+ private boolean tryMarkCancelled() {
|
|
+ return this.scheduled.compareAndSet(SCHEDULE_STATE_SCHEDULED, SCHEDULE_STATE_CANCELLED);
|
|
+ }
|
|
+
|
|
+ private boolean isScheduled() {
|
|
+ return this.scheduled.get() == SCHEDULE_STATE_SCHEDULED;
|
|
+ }
|
|
+
|
|
+ protected final long getScheduledStart() {
|
|
+ return this.scheduledStart;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * If this task is scheduled, then this may only be invoked during {@link #runTick()},
|
|
+ * and {@link #runTasks(BooleanSupplier)}
|
|
+ */
|
|
+ protected final void setScheduledStart(final long value) {
|
|
+ this.scheduledStart = value;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Executes the tick.
|
|
+ * <p>
|
|
+ * It is the callee's responsibility to invoke {@link #setScheduledStart(long)} to adjust the start of
|
|
+ * the next tick.
|
|
+ * </p>
|
|
+ * @return {@code true} if the task should continue to be scheduled, {@code false} otherwise.
|
|
+ */
|
|
+ public abstract boolean runTick();
|
|
+
|
|
+ /**
|
|
+ * Returns whether this task has any intermediate tasks that can be executed.
|
|
+ */
|
|
+ public abstract boolean hasTasks();
|
|
+
|
|
+ /**
|
|
+ * Returns {@code null} if this task should not be scheduled, otherwise returns
|
|
+ * {@code Boolean.TRUE} if there are more intermediate tasks to execute and
|
|
+ * {@code Boolean.FALSE} if there are no more intermediate tasks to execute.
|
|
+ */
|
|
+ public abstract Boolean runTasks(final BooleanSupplier canContinue);
|
|
+
|
|
+ @Override
|
|
+ public String toString() {
|
|
+ return "SchedulableTick:{" +
|
|
+ "class=" + this.getClass().getName() + "," +
|
|
+ "scheduled_state=" + this.scheduled.get() + ","
|
|
+ + "}";
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private static final class TickThreadRunner implements Runnable {
|
|
+
|
|
+ /**
|
|
+ * There are no tasks in this thread's runqueue, so it is parked.
|
|
+ * <p>
|
|
+ * stateTarget = null
|
|
+ * </p>
|
|
+ */
|
|
+ private static final int STATE_IDLE = 0;
|
|
+
|
|
+ /**
|
|
+ * The runner is waiting to tick a task, as it has no intermediate tasks to execute.
|
|
+ * <p>
|
|
+ * stateTarget = the task awaiting tick
|
|
+ * </p>
|
|
+ */
|
|
+ private static final int STATE_AWAITING_TICK = 1;
|
|
+
|
|
+ /**
|
|
+ * The runner is executing a tick for one of the tasks that was in its runqueue.
|
|
+ * <p>
|
|
+ * stateTarget = the task being ticked
|
|
+ * </p>
|
|
+ */
|
|
+ private static final int STATE_EXECUTING_TICK = 2;
|
|
+
|
|
+ public final int id;
|
|
+ public final SchedulerThreadPool scheduler;
|
|
+
|
|
+ private volatile Thread thread;
|
|
+ private volatile TickThreadRunnerState state = new TickThreadRunnerState(null, STATE_IDLE);
|
|
+ private static final VarHandle STATE_HANDLE = ConcurrentUtil.getVarHandle(TickThreadRunner.class, "state", TickThreadRunnerState.class);
|
|
+
|
|
+ private void setStatePlain(final TickThreadRunnerState state) {
|
|
+ STATE_HANDLE.set(this, state);
|
|
+ }
|
|
+
|
|
+ private void setStateOpaque(final TickThreadRunnerState state) {
|
|
+ STATE_HANDLE.setOpaque(this, state);
|
|
+ }
|
|
+
|
|
+ private void setStateVolatile(final TickThreadRunnerState state) {
|
|
+ STATE_HANDLE.setVolatile(this, state);
|
|
+ }
|
|
+
|
|
+ private static record TickThreadRunnerState(SchedulableTick stateTarget, int state) {}
|
|
+
|
|
+ public TickThreadRunner(final int id, final SchedulerThreadPool scheduler) {
|
|
+ this.id = id;
|
|
+ this.scheduler = scheduler;
|
|
+ }
|
|
+
|
|
+ private Thread getRunnerThread() {
|
|
+ return this.thread;
|
|
+ }
|
|
+
|
|
+ private void acceptTask(final SchedulableTick task) {
|
|
+ if (task.ownedBy != null) {
|
|
+ throw new IllegalStateException("Already owned by another runner");
|
|
+ }
|
|
+ task.ownedBy = this;
|
|
+ final TickThreadRunnerState state = this.state;
|
|
+ if (state.state != STATE_IDLE) {
|
|
+ throw new IllegalStateException("Cannot accept task in state " + state);
|
|
+ }
|
|
+ this.setStateVolatile(new TickThreadRunnerState(task, STATE_AWAITING_TICK));
|
|
+ LockSupport.unpark(this.getRunnerThread());
|
|
+ }
|
|
+
|
|
+ private void replaceTask(final SchedulableTick task) {
|
|
+ final TickThreadRunnerState state = this.state;
|
|
+ if (state.state != STATE_AWAITING_TICK) {
|
|
+ throw new IllegalStateException("Cannot replace task in state " + state);
|
|
+ }
|
|
+ if (task.ownedBy != null) {
|
|
+ throw new IllegalStateException("Already owned by another runner");
|
|
+ }
|
|
+ task.ownedBy = this;
|
|
+
|
|
+ state.stateTarget.ownedBy = null;
|
|
+
|
|
+ this.setStateVolatile(new TickThreadRunnerState(task, STATE_AWAITING_TICK));
|
|
+ LockSupport.unpark(this.getRunnerThread());
|
|
+ }
|
|
+
|
|
+ private void forceIdle() {
|
|
+ final TickThreadRunnerState state = this.state;
|
|
+ if (state.state != STATE_AWAITING_TICK) {
|
|
+ throw new IllegalStateException("Cannot replace task in state " + state);
|
|
+ }
|
|
+ state.stateTarget.ownedBy = null;
|
|
+ this.setStateOpaque(new TickThreadRunnerState(null, STATE_IDLE));
|
|
+ // no need to unpark
|
|
+ }
|
|
+
|
|
+ private boolean takeTask(final TickThreadRunnerState state, final SchedulableTick task) {
|
|
+ synchronized (this.scheduler.scheduleLock) {
|
|
+ if (this.state != state) {
|
|
+ return false;
|
|
+ }
|
|
+ this.setStatePlain(new TickThreadRunnerState(task, STATE_EXECUTING_TICK));
|
|
+ this.scheduler.takeTask(this, task);
|
|
+ return true;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private void returnTask(final SchedulableTick task, final boolean reschedule) {
|
|
+ synchronized (this.scheduler.scheduleLock) {
|
|
+ task.ownedBy = null;
|
|
+
|
|
+ final SchedulableTick newWait = this.scheduler.returnTask(this, reschedule && task.isScheduled() ? task : null);
|
|
+ if (newWait == null) {
|
|
+ this.setStatePlain(new TickThreadRunnerState(null, STATE_IDLE));
|
|
+ } else {
|
|
+ if (newWait.ownedBy != null) {
|
|
+ throw new IllegalStateException("Already owned by another runner");
|
|
+ }
|
|
+ newWait.ownedBy = this;
|
|
+ this.setStatePlain(new TickThreadRunnerState(newWait, STATE_AWAITING_TICK));
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void run() {
|
|
+ this.thread = Thread.currentThread();
|
|
+
|
|
+ main_state_loop:
|
|
+ for (;;) {
|
|
+ final TickThreadRunnerState startState = this.state;
|
|
+ final int startStateType = startState.state;
|
|
+ final SchedulableTick startStateTask = startState.stateTarget;
|
|
+
|
|
+ if (this.scheduler.halted) {
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ switch (startStateType) {
|
|
+ case STATE_IDLE: {
|
|
+ while (this.state.state == STATE_IDLE) {
|
|
+ LockSupport.park();
|
|
+ if (this.scheduler.halted) {
|
|
+ return;
|
|
+ }
|
|
+ }
|
|
+ continue main_state_loop;
|
|
+ }
|
|
+
|
|
+ case STATE_AWAITING_TICK: {
|
|
+ final long deadline = startStateTask.getScheduledStart();
|
|
+ for (;;) {
|
|
+ if (this.state != startState) {
|
|
+ continue main_state_loop;
|
|
+ }
|
|
+ final long diff = deadline - System.nanoTime();
|
|
+ if (diff <= 0L) {
|
|
+ break;
|
|
+ }
|
|
+ LockSupport.parkNanos(startState, diff);
|
|
+ if (this.scheduler.halted) {
|
|
+ return;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (!this.takeTask(startState, startStateTask)) {
|
|
+ continue main_state_loop;
|
|
+ }
|
|
+
|
|
+ // TODO exception handling
|
|
+ final boolean reschedule = startStateTask.runTick();
|
|
+
|
|
+ this.returnTask(startStateTask, reschedule);
|
|
+
|
|
+ continue main_state_loop;
|
|
+ }
|
|
+
|
|
+ case STATE_EXECUTING_TICK: {
|
|
+ throw new IllegalStateException("Tick execution must be set by runner thread, not by any other thread");
|
|
+ }
|
|
+
|
|
+ default: {
|
|
+ throw new IllegalStateException("Unknown state: " + startState);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+}
|
|
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/set/LinkedSortedSet.java b/src/main/java/ca/spottedleaf/concurrentutil/set/LinkedSortedSet.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..212bc9ae2fc7d37d4a089a2921b00de1e97f7cc1
|
|
--- /dev/null
|
|
+++ b/src/main/java/ca/spottedleaf/concurrentutil/set/LinkedSortedSet.java
|
|
@@ -0,0 +1,272 @@
|
|
+package ca.spottedleaf.concurrentutil.set;
|
|
+
|
|
+import java.util.Comparator;
|
|
+import java.util.Iterator;
|
|
+import java.util.NoSuchElementException;
|
|
+
|
|
+public final class LinkedSortedSet<E> implements Iterable<E> {
|
|
+
|
|
+ public final Comparator<? super E> comparator;
|
|
+
|
|
+ protected Link<E> head;
|
|
+ protected Link<E> tail;
|
|
+
|
|
+ public LinkedSortedSet() {
|
|
+ this((Comparator)Comparator.naturalOrder());
|
|
+ }
|
|
+
|
|
+ public LinkedSortedSet(final Comparator<? super E> comparator) {
|
|
+ this.comparator = comparator;
|
|
+ }
|
|
+
|
|
+ public void clear() {
|
|
+ this.head = this.tail = null;
|
|
+ }
|
|
+
|
|
+ public boolean isEmpty() {
|
|
+ return this.head == null;
|
|
+ }
|
|
+
|
|
+ public E first() {
|
|
+ final Link<E> head = this.head;
|
|
+ return head == null ? null : head.element;
|
|
+ }
|
|
+
|
|
+ public E last() {
|
|
+ final Link<E> tail = this.tail;
|
|
+ return tail == null ? null : tail.element;
|
|
+ }
|
|
+
|
|
+ public boolean containsFirst(final E element) {
|
|
+ final Comparator<? super E> comparator = this.comparator;
|
|
+ for (Link<E> curr = this.head; curr != null; curr = curr.next) {
|
|
+ if (comparator.compare(element, curr.element) == 0) {
|
|
+ return true;
|
|
+ }
|
|
+ }
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ public boolean containsLast(final E element) {
|
|
+ final Comparator<? super E> comparator = this.comparator;
|
|
+ for (Link<E> curr = this.tail; curr != null; curr = curr.prev) {
|
|
+ if (comparator.compare(element, curr.element) == 0) {
|
|
+ return true;
|
|
+ }
|
|
+ }
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ private void removeNode(final Link<E> node) {
|
|
+ final Link<E> prev = node.prev;
|
|
+ final Link<E> next = node.next;
|
|
+
|
|
+ // help GC
|
|
+ node.element = null;
|
|
+ node.prev = null;
|
|
+ node.next = null;
|
|
+
|
|
+ if (prev == null) {
|
|
+ this.head = next;
|
|
+ } else {
|
|
+ prev.next = next;
|
|
+ }
|
|
+
|
|
+ if (next == null) {
|
|
+ this.tail = prev;
|
|
+ } else {
|
|
+ next.prev = prev;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public boolean remove(final Link<E> link) {
|
|
+ if (link.element == null) {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ this.removeNode(link);
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ public boolean removeFirst(final E element) {
|
|
+ final Comparator<? super E> comparator = this.comparator;
|
|
+ for (Link<E> curr = this.head; curr != null; curr = curr.next) {
|
|
+ if (comparator.compare(element, curr.element) == 0) {
|
|
+ this.removeNode(curr);
|
|
+ return true;
|
|
+ }
|
|
+ }
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ public boolean removeLast(final E element) {
|
|
+ final Comparator<? super E> comparator = this.comparator;
|
|
+ for (Link<E> curr = this.tail; curr != null; curr = curr.prev) {
|
|
+ if (comparator.compare(element, curr.element) == 0) {
|
|
+ this.removeNode(curr);
|
|
+ return true;
|
|
+ }
|
|
+ }
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public Iterator<E> iterator() {
|
|
+ return new Iterator<>() {
|
|
+ private Link<E> next = LinkedSortedSet.this.head;
|
|
+
|
|
+ @Override
|
|
+ public boolean hasNext() {
|
|
+ return this.next != null;
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public E next() {
|
|
+ final Link<E> next = this.next;
|
|
+ if (next == null) {
|
|
+ throw new NoSuchElementException();
|
|
+ }
|
|
+ this.next = next.next;
|
|
+ return next.element;
|
|
+ }
|
|
+ };
|
|
+ }
|
|
+
|
|
+ public E pollFirst() {
|
|
+ final Link<E> head = this.head;
|
|
+ if (head == null) {
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ final E ret = head.element;
|
|
+ final Link<E> next = head.next;
|
|
+
|
|
+ // unlink head
|
|
+ this.head = next;
|
|
+ if (next == null) {
|
|
+ this.tail = null;
|
|
+ } else {
|
|
+ next.prev = null;
|
|
+ }
|
|
+
|
|
+ // help GC
|
|
+ head.element = null;
|
|
+ head.next = null;
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ public E pollLast() {
|
|
+ final Link<E> tail = this.tail;
|
|
+ if (tail == null) {
|
|
+ return null;
|
|
+ }
|
|
+
|
|
+ final E ret = tail.element;
|
|
+ final Link<E> prev = tail.prev;
|
|
+
|
|
+ // unlink tail
|
|
+ this.tail = prev;
|
|
+ if (prev == null) {
|
|
+ this.head = null;
|
|
+ } else {
|
|
+ prev.next = null;
|
|
+ }
|
|
+
|
|
+ // help GC
|
|
+ tail.element = null;
|
|
+ tail.prev = null;
|
|
+
|
|
+ return ret;
|
|
+ }
|
|
+
|
|
+ public Link<E> addLast(final E element) {
|
|
+ final Comparator<? super E> comparator = this.comparator;
|
|
+
|
|
+ Link<E> curr = this.tail;
|
|
+ if (curr != null) {
|
|
+ int compare;
|
|
+
|
|
+ while ((compare = comparator.compare(element, curr.element)) < 0) {
|
|
+ Link<E> prev = curr;
|
|
+ curr = curr.prev;
|
|
+ if (curr != null) {
|
|
+ continue;
|
|
+ }
|
|
+ return this.head = prev.prev = new Link<>(element, null, prev);
|
|
+ }
|
|
+
|
|
+ if (compare != 0) {
|
|
+ // insert after curr
|
|
+ final Link<E> next = curr.next;
|
|
+ final Link<E> insert = new Link<>(element, curr, next);
|
|
+ curr.next = insert;
|
|
+
|
|
+ if (next == null) {
|
|
+ this.tail = insert;
|
|
+ } else {
|
|
+ next.prev = insert;
|
|
+ }
|
|
+ return insert;
|
|
+ }
|
|
+
|
|
+ return null;
|
|
+ } else {
|
|
+ return this.head = this.tail = new Link<>(element);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public Link<E> addFirst(final E element) {
|
|
+ final Comparator<? super E> comparator = this.comparator;
|
|
+
|
|
+ Link<E> curr = this.head;
|
|
+ if (curr != null) {
|
|
+ int compare;
|
|
+
|
|
+ while ((compare = comparator.compare(element, curr.element)) > 0) {
|
|
+ Link<E> prev = curr;
|
|
+ curr = curr.next;
|
|
+ if (curr != null) {
|
|
+ continue;
|
|
+ }
|
|
+ return this.tail = prev.next = new Link<>(element, prev, null);
|
|
+ }
|
|
+
|
|
+ if (compare != 0) {
|
|
+ // insert before curr
|
|
+ final Link<E> prev = curr.prev;
|
|
+ final Link<E> insert = new Link<>(element, prev, curr);
|
|
+ curr.prev = insert;
|
|
+
|
|
+ if (prev == null) {
|
|
+ this.head = insert;
|
|
+ } else {
|
|
+ prev.next = insert;
|
|
+ }
|
|
+ return insert;
|
|
+ }
|
|
+
|
|
+ return null;
|
|
+ } else {
|
|
+ return this.head = this.tail = new Link<>(element);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ public static final class Link<E> {
|
|
+ private E element;
|
|
+ private Link<E> prev;
|
|
+ private Link<E> next;
|
|
+
|
|
+ private Link() {}
|
|
+
|
|
+ private Link(final E element) {
|
|
+ this.element = element;
|
|
+ }
|
|
+
|
|
+ private Link(final E element, final Link<E> prev, final Link<E> next) {
|
|
+ this.element = element;
|
|
+ this.prev = prev;
|
|
+ this.next = next;
|
|
+ }
|
|
+ }
|
|
+}
|
|
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/HashUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/HashUtil.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..2b9f36211d1cbb4fcf1457c0a83592499e9aa23b
|
|
--- /dev/null
|
|
+++ b/src/main/java/ca/spottedleaf/concurrentutil/util/HashUtil.java
|
|
@@ -0,0 +1,111 @@
|
|
+package ca.spottedleaf.concurrentutil.util;
|
|
+
|
|
+public final class HashUtil {
|
|
+
|
|
+ // Copied from fastutil HashCommon
|
|
+
|
|
+ /** 2<sup>32</sup> · φ, φ = (√5 − 1)/2. */
|
|
+ private static final int INT_PHI = 0x9E3779B9;
|
|
+ /** The reciprocal of {@link #INT_PHI} modulo 2<sup>32</sup>. */
|
|
+ private static final int INV_INT_PHI = 0x144cbc89;
|
|
+ /** 2<sup>64</sup> · φ, φ = (√5 − 1)/2. */
|
|
+ private static final long LONG_PHI = 0x9E3779B97F4A7C15L;
|
|
+ /** The reciprocal of {@link #LONG_PHI} modulo 2<sup>64</sup>. */
|
|
+ private static final long INV_LONG_PHI = 0xf1de83e19937733dL;
|
|
+
|
|
+ /** Avalanches the bits of an integer by applying the finalisation step of MurmurHash3.
|
|
+ *
|
|
+ * <p>This method implements the finalisation step of Austin Appleby's <a href="http://code.google.com/p/smhasher/">MurmurHash3</a>.
|
|
+ * Its purpose is to avalanche the bits of the argument to within 0.25% bias.
|
|
+ *
|
|
+ * @param x an integer.
|
|
+ * @return a hash value with good avalanching properties.
|
|
+ */
|
|
+ // additional note: this function is a bijection onto all integers
|
|
+ public static int murmurHash3(int x) {
|
|
+ x ^= x >>> 16;
|
|
+ x *= 0x85ebca6b;
|
|
+ x ^= x >>> 13;
|
|
+ x *= 0xc2b2ae35;
|
|
+ x ^= x >>> 16;
|
|
+ return x;
|
|
+ }
|
|
+
|
|
+
|
|
+ /** Avalanches the bits of a long integer by applying the finalisation step of MurmurHash3.
|
|
+ *
|
|
+ * <p>This method implements the finalisation step of Austin Appleby's <a href="http://code.google.com/p/smhasher/">MurmurHash3</a>.
|
|
+ * Its purpose is to avalanche the bits of the argument to within 0.25% bias.
|
|
+ *
|
|
+ * @param x a long integer.
|
|
+ * @return a hash value with good avalanching properties.
|
|
+ */
|
|
+ // additional note: this function is a bijection onto all longs
|
|
+ public static long murmurHash3(long x) {
|
|
+ x ^= x >>> 33;
|
|
+ x *= 0xff51afd7ed558ccdL;
|
|
+ x ^= x >>> 33;
|
|
+ x *= 0xc4ceb9fe1a85ec53L;
|
|
+ x ^= x >>> 33;
|
|
+ return x;
|
|
+ }
|
|
+
|
|
+ /** Quickly mixes the bits of an integer.
|
|
+ *
|
|
+ * <p>This method mixes the bits of the argument by multiplying by the golden ratio and
|
|
+ * xorshifting the result. It is borrowed from <a href="https://github.com/leventov/Koloboke">Koloboke</a>, and
|
|
+ * it has slightly worse behaviour than {@link #murmurHash3(int)} (in open-addressing hash tables the average number of probes
|
|
+ * is slightly larger), but it's much faster.
|
|
+ *
|
|
+ * @param x an integer.
|
|
+ * @return a hash value obtained by mixing the bits of {@code x}.
|
|
+ * @see #invMix(int)
|
|
+ */
|
|
+ // additional note: this function is a bijection onto all integers
|
|
+ public static int mix(final int x) {
|
|
+ final int h = x * INT_PHI;
|
|
+ return h ^ (h >>> 16);
|
|
+ }
|
|
+
|
|
+ /** The inverse of {@link #mix(int)}. This method is mainly useful to create unit tests.
|
|
+ *
|
|
+ * @param x an integer.
|
|
+ * @return a value that passed through {@link #mix(int)} would give {@code x}.
|
|
+ */
|
|
+ // additional note: this function is a bijection onto all integers
|
|
+ public static int invMix(final int x) {
|
|
+ return (x ^ x >>> 16) * INV_INT_PHI;
|
|
+ }
|
|
+
|
|
+ /** Quickly mixes the bits of a long integer.
|
|
+ *
|
|
+ * <p>This method mixes the bits of the argument by multiplying by the golden ratio and
|
|
+ * xorshifting twice the result. It is borrowed from <a href="https://github.com/leventov/Koloboke">Koloboke</a>, and
|
|
+ * it has slightly worse behaviour than {@link #murmurHash3(long)} (in open-addressing hash tables the average number of probes
|
|
+ * is slightly larger), but it's much faster.
|
|
+ *
|
|
+ * @param x a long integer.
|
|
+ * @return a hash value obtained by mixing the bits of {@code x}.
|
|
+ */
|
|
+ // additional note: this function is a bijection onto all longs
|
|
+ public static long mix(final long x) {
|
|
+ long h = x * LONG_PHI;
|
|
+ h ^= h >>> 32;
|
|
+ return h ^ (h >>> 16);
|
|
+ }
|
|
+
|
|
+ /** The inverse of {@link #mix(long)}. This method is mainly useful to create unit tests.
|
|
+ *
|
|
+ * @param x a long integer.
|
|
+ * @return a value that passed through {@link #mix(long)} would give {@code x}.
|
|
+ */
|
|
+ // additional note: this function is a bijection onto all longs
|
|
+ public static long invMix(long x) {
|
|
+ x ^= x >>> 32;
|
|
+ x ^= x >>> 16;
|
|
+ return (x ^ x >>> 32) * INV_LONG_PHI;
|
|
+ }
|
|
+
|
|
+
|
|
+ private HashUtil() {}
|
|
+}
|
|
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/IntPairUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/IntPairUtil.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..4e61c477a56e645228d5a2015c26816954d17bf8
|
|
--- /dev/null
|
|
+++ b/src/main/java/ca/spottedleaf/concurrentutil/util/IntPairUtil.java
|
|
@@ -0,0 +1,46 @@
|
|
+package ca.spottedleaf.concurrentutil.util;
|
|
+
|
|
+public final class IntPairUtil {
|
|
+
|
|
+ /**
|
|
+ * Packs the specified integers into one long value.
|
|
+ */
|
|
+ public static long key(final int left, final int right) {
|
|
+ return ((long)right << 32) | (left & 0xFFFFFFFFL);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Retrieves the left packed integer from the key
|
|
+ */
|
|
+ public static int left(final long key) {
|
|
+ return (int)key;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Retrieves the right packed integer from the key
|
|
+ */
|
|
+ public static int right(final long key) {
|
|
+ return (int)(key >>> 32);
|
|
+ }
|
|
+
|
|
+ public static String toString(final long key) {
|
|
+ return "{left:" + left(key) + ", right:" + right(key) + "}";
|
|
+ }
|
|
+
|
|
+ public static String toString(final long[] array, final int from, final int to) {
|
|
+ final StringBuilder ret = new StringBuilder();
|
|
+ ret.append("[");
|
|
+
|
|
+ for (int i = from; i < to; ++i) {
|
|
+ if (i != from) {
|
|
+ ret.append(", ");
|
|
+ }
|
|
+ ret.append(toString(array[i]));
|
|
+ }
|
|
+
|
|
+ ret.append("]");
|
|
+ return ret.toString();
|
|
+ }
|
|
+
|
|
+ private IntPairUtil() {}
|
|
+}
|
|
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/IntegerUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/IntegerUtil.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..77699c5fa9681f9ec7aa05cbb50eb60828e193ab
|
|
--- /dev/null
|
|
+++ b/src/main/java/ca/spottedleaf/concurrentutil/util/IntegerUtil.java
|
|
@@ -0,0 +1,176 @@
|
|
+package ca.spottedleaf.concurrentutil.util;
|
|
+
|
|
+public final class IntegerUtil {
|
|
+
|
|
+ public static final int HIGH_BIT_U32 = Integer.MIN_VALUE;
|
|
+ public static final long HIGH_BIT_U64 = Long.MIN_VALUE;
|
|
+
|
|
+ public static int ceilLog2(final int value) {
|
|
+ return Integer.SIZE - Integer.numberOfLeadingZeros(value - 1); // see doc of numberOfLeadingZeros
|
|
+ }
|
|
+
|
|
+ public static long ceilLog2(final long value) {
|
|
+ return Long.SIZE - Long.numberOfLeadingZeros(value - 1); // see doc of numberOfLeadingZeros
|
|
+ }
|
|
+
|
|
+ public static int floorLog2(final int value) {
|
|
+ // xor is optimized subtract for 2^n -1
|
|
+ // note that (2^n -1) - k = (2^n -1) ^ k for k <= (2^n - 1)
|
|
+ return (Integer.SIZE - 1) ^ Integer.numberOfLeadingZeros(value); // see doc of numberOfLeadingZeros
|
|
+ }
|
|
+
|
|
+ public static int floorLog2(final long value) {
|
|
+ // xor is optimized subtract for 2^n -1
|
|
+ // note that (2^n -1) - k = (2^n -1) ^ k for k <= (2^n - 1)
|
|
+ return (Long.SIZE - 1) ^ Long.numberOfLeadingZeros(value); // see doc of numberOfLeadingZeros
|
|
+ }
|
|
+
|
|
+ public static int roundCeilLog2(final int value) {
|
|
+ // optimized variant of 1 << (32 - leading(val - 1))
|
|
+ // given
|
|
+ // 1 << n = HIGH_BIT_32 >>> (31 - n) for n [0, 32)
|
|
+ // 1 << (32 - leading(val - 1)) = HIGH_BIT_32 >>> (31 - (32 - leading(val - 1)))
|
|
+ // HIGH_BIT_32 >>> (31 - (32 - leading(val - 1)))
|
|
+ // HIGH_BIT_32 >>> (31 - 32 + leading(val - 1))
|
|
+ // HIGH_BIT_32 >>> (-1 + leading(val - 1))
|
|
+ return HIGH_BIT_U32 >>> (Integer.numberOfLeadingZeros(value - 1) - 1);
|
|
+ }
|
|
+
|
|
+ public static long roundCeilLog2(final long value) {
|
|
+ // see logic documented above
|
|
+ return HIGH_BIT_U64 >>> (Long.numberOfLeadingZeros(value - 1) - 1);
|
|
+ }
|
|
+
|
|
+ public static int roundFloorLog2(final int value) {
|
|
+ // optimized variant of 1 << (31 - leading(val))
|
|
+ // given
|
|
+ // 1 << n = HIGH_BIT_32 >>> (31 - n) for n [0, 32)
|
|
+ // 1 << (31 - leading(val)) = HIGH_BIT_32 >> (31 - (31 - leading(val)))
|
|
+ // HIGH_BIT_32 >> (31 - (31 - leading(val)))
|
|
+ // HIGH_BIT_32 >> (31 - 31 + leading(val))
|
|
+ return HIGH_BIT_U32 >>> Integer.numberOfLeadingZeros(value);
|
|
+ }
|
|
+
|
|
+ public static long roundFloorLog2(final long value) {
|
|
+ // see logic documented above
|
|
+ return HIGH_BIT_U64 >>> Long.numberOfLeadingZeros(value);
|
|
+ }
|
|
+
|
|
+ public static boolean isPowerOfTwo(final int n) {
|
|
+ // 2^n has one bit
|
|
+ // note: this rets true for 0 still
|
|
+ return IntegerUtil.getTrailingBit(n) == n;
|
|
+ }
|
|
+
|
|
+ public static boolean isPowerOfTwo(final long n) {
|
|
+ // 2^n has one bit
|
|
+ // note: this rets true for 0 still
|
|
+ return IntegerUtil.getTrailingBit(n) == n;
|
|
+ }
|
|
+
|
|
+ public static int getTrailingBit(final int n) {
|
|
+ return -n & n;
|
|
+ }
|
|
+
|
|
+ public static long getTrailingBit(final long n) {
|
|
+ return -n & n;
|
|
+ }
|
|
+
|
|
+ public static int trailingZeros(final int n) {
|
|
+ return Integer.numberOfTrailingZeros(n);
|
|
+ }
|
|
+
|
|
+ public static int trailingZeros(final long n) {
|
|
+ return Long.numberOfTrailingZeros(n);
|
|
+ }
|
|
+
|
|
+ // from hacker's delight (signed division magic value)
|
|
+ public static int getDivisorMultiple(final long numbers) {
|
|
+ return (int)(numbers >>> 32);
|
|
+ }
|
|
+
|
|
+ // from hacker's delight (signed division magic value)
|
|
+ public static int getDivisorShift(final long numbers) {
|
|
+ return (int)numbers;
|
|
+ }
|
|
+
|
|
+ // copied from hacker's delight (signed division magic value)
|
|
+ // http://www.hackersdelight.org/hdcodetxt/magic.c.txt
|
|
+ public static long getDivisorNumbers(final int d) {
|
|
+ final int ad = branchlessAbs(d);
|
|
+
|
|
+ if (ad < 2) {
|
|
+ throw new IllegalArgumentException("|number| must be in [2, 2^31 -1], not: " + d);
|
|
+ }
|
|
+
|
|
+ final int two31 = 0x80000000;
|
|
+ final long mask = 0xFFFFFFFFL; // mask for enforcing unsigned behaviour
|
|
+
|
|
+ /*
|
|
+ Signed usage:
|
|
+ int number;
|
|
+ long magic = getDivisorNumbers(div);
|
|
+ long mul = magic >>> 32;
|
|
+ int sign = number >> 31;
|
|
+ int result = (int)(((long)number * mul) >>> magic) - sign;
|
|
+ */
|
|
+ /*
|
|
+ Unsigned usage: (note: fails for input > Integer.MAX_VALUE, only use when input < Integer.MAX_VALUE to avoid sign calculation)
|
|
+ int number;
|
|
+ long magic = getDivisorNumbers(div);
|
|
+ long mul = magic >>> 32;
|
|
+ int result = (int)(((long)number * mul) >>> magic);
|
|
+ */
|
|
+
|
|
+ int p = 31;
|
|
+
|
|
+ // all these variables are UNSIGNED!
|
|
+ int t = two31 + (d >>> 31);
|
|
+ int anc = t - 1 - (int)((t & mask)%ad);
|
|
+ int q1 = (int)((two31 & mask)/(anc & mask));
|
|
+ int r1 = two31 - q1*anc;
|
|
+ int q2 = (int)((two31 & mask)/(ad & mask));
|
|
+ int r2 = two31 - q2*ad;
|
|
+ int delta;
|
|
+
|
|
+ do {
|
|
+ p = p + 1;
|
|
+ q1 = 2*q1; // Update q1 = 2**p/|nc|.
|
|
+ r1 = 2*r1; // Update r1 = rem(2**p, |nc|).
|
|
+ if ((r1 & mask) >= (anc & mask)) {// (Must be an unsigned comparison here)
|
|
+ q1 = q1 + 1;
|
|
+ r1 = r1 - anc;
|
|
+ }
|
|
+ q2 = 2*q2; // Update q2 = 2**p/|d|.
|
|
+ r2 = 2*r2; // Update r2 = rem(2**p, |d|).
|
|
+ if ((r2 & mask) >= (ad & mask)) {// (Must be an unsigned comparison here)
|
|
+ q2 = q2 + 1;
|
|
+ r2 = r2 - ad;
|
|
+ }
|
|
+ delta = ad - r2;
|
|
+ } while ((q1 & mask) < (delta & mask) || (q1 == delta && r1 == 0));
|
|
+
|
|
+ int magicNum = q2 + 1;
|
|
+ if (d < 0) {
|
|
+ magicNum = -magicNum;
|
|
+ }
|
|
+ int shift = p;
|
|
+ return ((long)magicNum << 32) | shift;
|
|
+ }
|
|
+
|
|
+ public static int branchlessAbs(final int val) {
|
|
+ // -n = -1 ^ n + 1
|
|
+ final int mask = val >> (Integer.SIZE - 1); // -1 if < 0, 0 if >= 0
|
|
+ return (mask ^ val) - mask; // if val < 0, then (0 ^ val) - 0 else (-1 ^ val) + 1
|
|
+ }
|
|
+
|
|
+ public static long branchlessAbs(final long val) {
|
|
+ // -n = -1 ^ n + 1
|
|
+ final long mask = val >> (Long.SIZE - 1); // -1 if < 0, 0 if >= 0
|
|
+ return (mask ^ val) - mask; // if val < 0, then (0 ^ val) - 0 else (-1 ^ val) + 1
|
|
+ }
|
|
+
|
|
+ private IntegerUtil() {
|
|
+ throw new RuntimeException();
|
|
+ }
|
|
+}
|
|
\ No newline at end of file
|
|
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/ThrowUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/ThrowUtil.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..a3a8b5c6795c4d116e094e4c910553416f565b93
|
|
--- /dev/null
|
|
+++ b/src/main/java/ca/spottedleaf/concurrentutil/util/ThrowUtil.java
|
|
@@ -0,0 +1,11 @@
|
|
+package ca.spottedleaf.concurrentutil.util;
|
|
+
|
|
+public final class ThrowUtil {
|
|
+
|
|
+ private ThrowUtil() {}
|
|
+
|
|
+ public static <T extends Throwable> void throwUnchecked(final Throwable thr) throws T {
|
|
+ throw (T)thr;
|
|
+ }
|
|
+
|
|
+}
|
|
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/TimeUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/TimeUtil.java
|
|
new file mode 100644
|
|
index 0000000000000000000000000000000000000000..63688716244066581d5b505703576e3340e3baf3
|
|
--- /dev/null
|
|
+++ b/src/main/java/ca/spottedleaf/concurrentutil/util/TimeUtil.java
|
|
@@ -0,0 +1,60 @@
|
|
+package ca.spottedleaf.concurrentutil.util;
|
|
+
|
|
+public final class TimeUtil {
|
|
+
|
|
+ /*
|
|
+ * The comparator is not a valid comparator for every long value. To prove where it is valid, see below.
|
|
+ *
|
|
+ * For reflexivity, we have that x - x = 0. We then have that for any long value x that
|
|
+ * compareTimes(x, x) == 0, as expected.
|
|
+ *
|
|
+ * For symmetry, we have that x - y = -(y - x) except for when y - x = Long.MIN_VALUE.
|
|
+ * So, the difference between any times x and y must not be equal to Long.MIN_VALUE.
|
|
+ *
|
|
+ * As for the transitive relation, consider we have x,y such that x - y = a > 0 and z such that
|
|
+ * y - z = b > 0. Then, we will have that the x - z > 0 is equivalent to a + b > 0. For long values,
|
|
+ * this holds as long as a + b <= Long.MAX_VALUE.
|
|
+ *
|
|
+ * Also consider we have x, y such that x - y = a < 0 and z such that y - z = b < 0. Then, we will have
|
|
+ * that x - z < 0 is equivalent to a + b < 0. For long values, this holds as long as a + b >= -Long.MAX_VALUE.
|
|
+ *
|
|
+ * Thus, the comparator is only valid for timestamps such that abs(c - d) <= Long.MAX_VALUE for all timestamps
|
|
+ * c and d.
|
|
+ */
|
|
+
|
|
+ /**
|
|
+ * This function is appropriate to be used as a {@link java.util.Comparator} between two timestamps, which
|
|
+ * indicates whether the timestamps represented by t1, t2 that t1 is before, equal to, or after t2.
|
|
+ */
|
|
+ public static int compareTimes(final long t1, final long t2) {
|
|
+ final long diff = t1 - t2;
|
|
+
|
|
+ // HD, Section 2-7
|
|
+ return (int) ((diff >> 63) | (-diff >>> 63));
|
|
+ }
|
|
+
|
|
+ public static long getGreatestTime(final long t1, final long t2) {
|
|
+ final long diff = t1 - t2;
|
|
+ return diff < 0L ? t2 : t1;
|
|
+ }
|
|
+
|
|
+ public static long getLeastTime(final long t1, final long t2) {
|
|
+ final long diff = t1 - t2;
|
|
+ return diff > 0L ? t2 : t1;
|
|
+ }
|
|
+
|
|
+ public static long clampTime(final long value, final long min, final long max) {
|
|
+ final long diffMax = value - max;
|
|
+ final long diffMin = value - min;
|
|
+
|
|
+ if (diffMax > 0L) {
|
|
+ return max;
|
|
+ }
|
|
+ if (diffMin < 0L) {
|
|
+ return min;
|
|
+ }
|
|
+ return value;
|
|
+ }
|
|
+
|
|
+ private TimeUtil() {}
|
|
+}
|
|
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();
|
|
+ }
|
|
+}
|