From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Spottedleaf
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.
+ *
+ * Note that this queue breaks the specification laid out by {@link Collection}, see {@link #preventAdds()} and {@link Collection#add(Object)}.
+ *
+ *
+ * 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.
+ *
+ * @param Type of element in this queue.
+ */
+public class MultiThreadedQueue implements Queue {
+
+ protected volatile LinkedNode head; /* Always non-null, high chance of being the actual head */
+
+ protected volatile LinkedNode 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 newHead) {
+ HEAD_HANDLE.set(this, newHead);
+ }
+
+ protected final void setHeadOpaque(final LinkedNode newHead) {
+ HEAD_HANDLE.setOpaque(this, newHead);
+ }
+
+ @SuppressWarnings("unchecked")
+ protected final LinkedNode getHeadPlain() {
+ return (LinkedNode)HEAD_HANDLE.get(this);
+ }
+
+ @SuppressWarnings("unchecked")
+ protected final LinkedNode getHeadOpaque() {
+ return (LinkedNode)HEAD_HANDLE.getOpaque(this);
+ }
+
+ @SuppressWarnings("unchecked")
+ protected final LinkedNode getHeadAcquire() {
+ return (LinkedNode)HEAD_HANDLE.getAcquire(this);
+ }
+
+ /* tail */
+
+ protected final void setTailPlain(final LinkedNode newTail) {
+ TAIL_HANDLE.set(this, newTail);
+ }
+
+ protected final void setTailOpaque(final LinkedNode newTail) {
+ TAIL_HANDLE.setOpaque(this, newTail);
+ }
+
+ @SuppressWarnings("unchecked")
+ protected final LinkedNode getTailPlain() {
+ return (LinkedNode)TAIL_HANDLE.get(this);
+ }
+
+ @SuppressWarnings("unchecked")
+ protected final LinkedNode getTailOpaque() {
+ return (LinkedNode)TAIL_HANDLE.getOpaque(this);
+ }
+
+ /**
+ * Constructs a {@code MultiThreadedQueue}, initially empty.
+ *
+ * The returned object may not be published without synchronization.
+ *
+ */
+ public MultiThreadedQueue() {
+ final LinkedNode value = new LinkedNode<>(null, null);
+ this.setHeadPlain(value);
+ this.setTailPlain(value);
+ }
+
+ /**
+ * Constructs a {@code MultiThreadedQueue}, initially containing all elements in the specified {@code collection}.
+ *
+ * The returned object may not be published without synchronization.
+ *
+ * @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 value = new LinkedNode<>(null, null);
+ this.setHeadPlain(value);
+ this.setTailPlain(value);
+ return;
+ }
+
+ final LinkedNode head = new LinkedNode<>(Validate.notNull(elements.next(), "Null element"), null);
+ LinkedNode tail = head;
+
+ while (elements.hasNext()) {
+ final LinkedNode 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}
+ *
+ * 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.
+ *
+ */
+ @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 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}
+ *
+ * This method may also return {@code false} to indicate an element was not added if this queue is add-blocked.
+ *
+ */
+ @Override
+ public boolean offer(final E element) {
+ Validate.notNull(element, "Null element");
+
+ final LinkedNode node = new LinkedNode<>(element, null);
+
+ return this.appendList(node, node);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public E peek() {
+ for (LinkedNode head = this.getHeadOpaque(), curr = head;;) {
+ final LinkedNode 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}.
+ *
+ * The predicate may be invoked multiple or no times in this call.
+ *
+ * @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 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.
+ *
+ * This function is MT-Safe.
+ *
+ * @return {@code true} if the queue was modified to prevent additions, {@code false} if it already prevented additions.
+ */
+ public boolean preventAdds() {
+ final LinkedNode 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.
+ *
+ * This function is not MT-Safe.
+ *
+ */
+ public void allowAdds() {
+ LinkedNode 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.
+ *
+ * This function is MT-Safe, however it should not be used with {@link #allowAdds()}.
+ *
+ * @return {@code true} if the queue was previously add-locked, {@code false} otherwise.
+ */
+ public boolean tryAllowAdds() {
+ LinkedNode 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.
+ *
+ * This function is MT-Safe.
+ *
+ * @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 append = new LinkedNode<>(element, null);
+
+ for (LinkedNode 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 next = curr.getNextVolatile();
+
+ for (int i = 0; i < failures; ++i) {
+ ConcurrentUtil.backoff();
+ }
+
+ if (next == null) {
+ final LinkedNode 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 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 tail = this.getTailOpaque();;) {
+ LinkedNode 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.
+ *
+ * This function is MT-Safe.
+ *
+ * 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 head = this.getHeadOpaque(), curr = head;;) {
+ final E currentVal = curr.getElementVolatile();
+ final LinkedNode 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 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 curr = this.getHeadOpaque();;) {
+ final LinkedNode 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 curr = this.getHeadOpaque();;) {
+ final LinkedNode 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 curr = this.getHeadOpaque();;) {
+ final LinkedNode 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 curr = this.getHeadOpaque();;) {
+ final LinkedNode 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 ret = new ArrayList<>();
+
+ for (LinkedNode curr = this.getHeadOpaque();;) {
+ final LinkedNode 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[] toArray(final T[] array) {
+ final List ret = new ArrayList<>();
+
+ for (LinkedNode curr = this.getHeadOpaque();;) {
+ final LinkedNode 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[] toArray(final IntFunction generator) {
+ Validate.notNull(generator, "Null generator");
+
+ final List ret = new ArrayList<>();
+
+ for (LinkedNode curr = this.getHeadOpaque();;) {
+ final LinkedNode 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 curr = this.getHeadOpaque();; ++totalEntries) {
+ final LinkedNode 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 head = new LinkedNode<>(Validate.notNull(elements.next(), "Null element"), null);
+ LinkedNode tail = head;
+
+ while (elements.hasNext()) {
+ final LinkedNode 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 head = new LinkedNode<>(Validate.notNull(items[off], "Null element"), null);
+ LinkedNode tail = head;
+
+ for (int i = 1; i < len; ++i) {
+ final LinkedNode 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 iterator() {
+ return new LinkedIterator<>(this.getHeadOpaque());
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * 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.
+ *
+ */
+ @Override
+ public int size() {
+ int size = 0;
+
+ /* Volatile is required to synchronize with the write to the first element */
+ for (LinkedNode curr = this.getHeadOpaque();;) {
+ final LinkedNode 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 curr = this.getHeadOpaque();;) {
+ final LinkedNode 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 predicate) {
+ Validate.notNull(predicate, "Null predicate");
+
+ for (LinkedNode curr = this.getHeadOpaque();;) {
+ final LinkedNode 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 curr = this.getHeadOpaque();;) {
+ final LinkedNode 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 head, final LinkedNode tail) {
+ int failures = 0;
+
+ for (LinkedNode 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 next = curr.getNextVolatile();
+
+ for (int i = 0; i < failures; ++i) {
+ ConcurrentUtil.backoff();
+ }
+
+ if (next == null || next == curr) {
+ final LinkedNode 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 head, final LinkedNode tail) {
+ int failures = 0;
+
+ for (LinkedNode 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 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 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 predicate) {
+ int failures = 0;
+ for (LinkedNode head = this.getHeadOpaque(), curr = head;;) {
+ // volatile here synchronizes-with writes to element
+ final LinkedNode 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 head = this.getHeadOpaque(), curr = head;;) {
+ final LinkedNode 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()}.
+ *
+ * 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.
+ *
+ * @param consumer The consumer to accept the elements.
+ * @return The total number of elements drained.
+ */
+ public int drain(final Consumer 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()}.
+ *
+ * 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.
+ *
+ *
+ * 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.
+ *
+ * @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 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()}.
+ *
+ * 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.
+ *
+ *
+ * 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.
+ *
+ * @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 consumer, final boolean preventAdds, final Consumer 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 head = this.getHeadAcquire(); /* Required to synchronize with the write to the first element field */
+ LinkedNode curr = head;
+
+ for (;;) {
+ /* Volatile acquires with the write to the element field */
+ final E currentVal = curr.getElementPlain();
+ LinkedNode 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 spliterator() { // TODO implement
+ return Spliterators.spliterator(this, Spliterator.CONCURRENT |
+ Spliterator.NONNULL | Spliterator.ORDERED);
+ }
+
+ protected static final class LinkedNode {
+
+ protected volatile Object element;
+ protected volatile LinkedNode 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 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 getNextPlain() {
+ return (LinkedNode)NEXT_HANDLE.get(this);
+ }
+
+ @SuppressWarnings("unchecked")
+ protected final LinkedNode getNextOpaque() {
+ return (LinkedNode)NEXT_HANDLE.getOpaque(this);
+ }
+
+ @SuppressWarnings("unchecked")
+ protected final LinkedNode getNextAcquire() {
+ return (LinkedNode)NEXT_HANDLE.getAcquire(this);
+ }
+
+ @SuppressWarnings("unchecked")
+ protected final LinkedNode getNextVolatile() {
+ return (LinkedNode)NEXT_HANDLE.getVolatile(this);
+ }
+
+ protected final void setNextPlain(final LinkedNode next) {
+ NEXT_HANDLE.set(this, next);
+ }
+
+ protected final void setNextVolatile(final LinkedNode next) {
+ NEXT_HANDLE.setVolatile(this, next);
+ }
+
+ @SuppressWarnings("unchecked")
+ protected final LinkedNode compareExchangeNextVolatile(final LinkedNode expect, final LinkedNode set) {
+ return (LinkedNode)NEXT_HANDLE.compareAndExchange(this, expect, set);
+ }
+ }
+
+ protected static final class LinkedIterator implements Iterator {
+
+ protected LinkedNode curr; /* last returned by next() */
+ protected LinkedNode next; /* next to return from next() */
+ protected E nextElement; /* cached to avoid a race condition with removing or polling */
+
+ protected LinkedIterator(final LinkedNode start) {
+ /* setup nextElement and next */
+ for (LinkedNode curr = start;;) {
+ final LinkedNode 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 curr = this.next;;) {
+ final LinkedNode 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 {
+
+ // always non-null
+ protected LinkedNode head;
+
+ // always non-null
+ protected LinkedNode tail;
+
+ /* IMPL NOTE: Leave hashCode and equals to their defaults */
+
+ public SRSWLinkedQueue() {
+ final LinkedNode dummy = new LinkedNode<>(null, null);
+ this.head = this.tail = dummy;
+ }
+
+ /**
+ * Must be the reader thread.
+ *
+ *
+ * Returns, without removing, the first element of this queue.
+ *
+ * @return Returns, without removing, the first element of this queue.
+ */
+ public E peekFirst() {
+ LinkedNode 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.
+ *
+ *
+ * Returns and removes the first element of this queue.
+ *
+ * @return Returns and removes the first element of this queue.
+ */
+ public E poll() {
+ LinkedNode 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 next = head.getNextAcquire();
+ this.head = next == null ? head : next;
+
+ return ret;
+ }
+
+ /**
+ * Must be the writer thread.
+ *
+ *
+ * Adds the element to the end of the queue.
+ *
+ *
+ * @throws NullPointerException If the provided element is null
+ */
+ public void addLast(final E element) {
+ Validate.notNull(element, "Provided element cannot be null");
+ final LinkedNode append = new LinkedNode<>(element, null);
+
+ this.tail.setNextRelease(append);
+ this.tail = append;
+ }
+
+ protected static final class LinkedNode {
+
+ protected volatile Object element;
+ protected volatile LinkedNode 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 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 getNextPlain() {
+ return (LinkedNode)NEXT_HANDLE.get(this);
+ }
+
+ @SuppressWarnings("unchecked")
+ protected final LinkedNode getNextAcquire() {
+ return (LinkedNode)NEXT_HANDLE.getAcquire(this);
+ }
+
+ protected final void setNextPlain(final LinkedNode next) {
+ NEXT_HANDLE.set(this, next);
+ }
+
+ protected final void setNextRelease(final LinkedNode 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 {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(Completable.class);
+
+ private final MultiThreadedQueue> 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 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 waiter;
+ while ((waiter = this.waiters.pollOrBlockAdds()) != null) {
+ this.completeWaiter(waiter, result, throwable);
+ }
+ }
+
+ private void completeWaiter(final BiConsumer 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 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 waiter;
+
+ private CancellableImpl(final BiConsumer 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.
+ *
+ *
+ * The implementation supports tracking task executions using {@link #getTotalTasksScheduled()} and
+ * {@link #getTotalTasksExecuted()}, and optionally shutting down the executor using {@link #shutdown()}
+ *
+ *
+ *
+ * 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
+ * queue a task or create a task. A queued task is one which will eventually be executed,
+ * and a created 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.
+ *
+ */
+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()}
+ *
+ * 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.
+ *
+ *
+ * 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.
+ *
+ *
+ * Note: Interruptions to the the current thread have no effect. Interrupt status is also not affected by this call.
+ *
+ *
+ * @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}.
+ *
+ * WARNING: This function is not suitable for waiting until a deadline!
+ * Use {@link #executeUntil(long)} or {@link #executeConditionally(BooleanSupplier, long)} instead.
+ *
+ */
+ 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.
+ *
+ * This operation is atomic with respect to other shutdown calls
+ *
+ *
+ * After this call has completed, regardless of return value, this queue will be shutdown.
+ *
+ *
+ * @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.
+ *
+ * Exceptions thrown from the runnable will be rethrown.
+ *
+ *
+ * @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.
+ *
+ * If there is a task with priority {@link PrioritisedExecutor.Priority#BLOCKING} available, then that such task is executed.
+ *
+ *
+ * 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.
+ *
+ *
+ * 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.
+ *
+ *
+ * @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.
+ *
+ * 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.
+ *
+ */
+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.
+ *
+ * This function is MT-Safe.
+ *
+ * @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)}.
+ *
+ * This is not safe to call with {@link #close(boolean, boolean)} if wait = true
, in which case
+ * the waiting thread may block indefinitely.
+ *
+ *
+ * This function is MT-Safe.
+ *
+ * @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 queues = new TreeSet<>(PrioritisedPoolExecutorImpl.comparator());
+ private final String name;
+ private final long queueMaxHoldTime;
+
+ private final ReferenceOpenHashSet nonShutdownQueues = new ReferenceOpenHashSet<>();
+ private final ReferenceOpenHashSet 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 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 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 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 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 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 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 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 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 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 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 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[] 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[] queues = this.queues;
+ synchronized (queues) {
+ final int max = minPriority.priority;
+ for (int i = 0; i <= max; ++i) {
+ final ArrayDeque 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 {
+
+ 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 {
+
+ 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 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 {
+
+ 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..6abee91e0d83c6a172e890bbda304a512cf790a1
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/concurrentutil/map/ConcurrentLong2ReferenceChainedHashTable.java
@@ -0,0 +1,1681 @@
+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.
+ *
+ * Happens-before relationship
+ *
+ * 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.
+ *
+ *
+ * Atomicity of functional methods
+ *
+ * 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.
+ *
+ *
+ * @param
+ * @see java.util.concurrent.ConcurrentMap
+ */
+public class ConcurrentLong2ReferenceChainedHashTable {
+
+ 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[] 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[])new TableEntry[tableSize];
+ }
+
+ public static ConcurrentLong2ReferenceChainedHashTable createWithCapacity(final int capacity) {
+ return createWithCapacity(capacity, DEFAULT_LOAD_FACTOR);
+ }
+
+ public static ConcurrentLong2ReferenceChainedHashTable createWithCapacity(final int capacity, final float loadFactor) {
+ return new ConcurrentLong2ReferenceChainedHashTable<>(capacity, loadFactor);
+ }
+
+ public static ConcurrentLong2ReferenceChainedHashTable createWithExpected(final int expected) {
+ return createWithExpected(expected, DEFAULT_LOAD_FACTOR);
+ }
+
+ public static ConcurrentLong2ReferenceChainedHashTable 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 TableEntry getAtIndexVolatile(final TableEntry[] table, final int index) {
+ //noinspection unchecked
+ return (TableEntry)TableEntry.TABLE_ENTRY_ARRAY_HANDLE.getVolatile(table, index);
+ }
+
+ protected static void setAtIndexRelease(final TableEntry[] table, final int index, final TableEntry value) {
+ TableEntry.TABLE_ENTRY_ARRAY_HANDLE.setRelease(table, index, value);
+ }
+
+ protected static void setAtIndexVolatile(final TableEntry[] table, final int index, final TableEntry value) {
+ TableEntry.TABLE_ENTRY_ARRAY_HANDLE.setVolatile(table, index, value);
+ }
+
+ protected static TableEntry compareAndExchangeAtIndexVolatile(final TableEntry[] table, final int index,
+ final TableEntry expect, final TableEntry update) {
+ //noinspection unchecked
+ return (TableEntry)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 getNode(final long key) {
+ final int hash = getHash(key);
+
+ TableEntry[] table = this.table;
+ for (;;) {
+ TableEntry node = getAtIndexVolatile(table, hash & (table.length - 1));
+
+ if (node == null) {
+ // node == null
+ return node;
+ }
+
+ if (node.resize) {
+ table = (TableEntry[])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 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 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 iterator = new NodeIterator<>(this.table);
+
+ TableEntry 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[] newTable = new TableEntry[capacity];
+ // noinspection unchecked
+ final TableEntry resizeNode = new TableEntry<>(0L, (V)newTable, true);
+
+ // transfer nodes from old table
+
+ // does not need to be volatile read, just plain
+ final TableEntry[] 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[] work = new TableEntry[1 << capDiffShift]; // typically, capDiffShift = 1
+
+ for (int i = 0, len = oldTable.length; i < len; ++i) {
+ TableEntry 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 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 curr = binNode; curr != null; curr = curr.getNextPlain()) {
+ final int newTableIdx = getHash(curr.key) & (capacity - 1);
+ final int workIdx = newTableIdx >>> capOldShift;
+
+ final TableEntry replace = new TableEntry<>(curr.key, curr.getValuePlain());
+
+ final TableEntry 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[] table = this.table;
+ table_loop:
+ for (;;) {
+ final int index = hash & (table.length - 1);
+
+ TableEntry 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[])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 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[] table = this.table;
+ table_loop:
+ for (;;) {
+ final int index = hash & (table.length - 1);
+
+ TableEntry 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[])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 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[] table = this.table;
+ table_loop:
+ for (;;) {
+ final int index = hash & (table.length - 1);
+
+ TableEntry node = getAtIndexVolatile(table, index);
+ node_loop:
+ for (;;) {
+ if (node == null) {
+ return null;
+ }
+
+ if (node.resize) {
+ table = (TableEntry[])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[] table = this.table;
+ table_loop:
+ for (;;) {
+ final int index = hash & (table.length - 1);
+
+ TableEntry node = getAtIndexVolatile(table, index);
+ node_loop:
+ for (;;) {
+ if (node == null) {
+ return null;
+ }
+
+ if (node.resize) {
+ table = (TableEntry[])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[] table = this.table;
+ table_loop:
+ for (;;) {
+ final int index = hash & (table.length - 1);
+
+ TableEntry node = getAtIndexVolatile(table, index);
+ node_loop:
+ for (;;) {
+ if (node == null) {
+ return null;
+ }
+
+ if (node.resize) {
+ table = (TableEntry[])node.getValuePlain();
+ continue table_loop;
+ }
+
+ boolean removed = false;
+ V ret = null;
+
+ synchronized (node) {
+ if (node != (node = getAtIndexVolatile(table, index))) {
+ continue node_loop;
+ }
+
+ TableEntry 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[] table = this.table;
+ table_loop:
+ for (;;) {
+ final int index = hash & (table.length - 1);
+
+ TableEntry node = getAtIndexVolatile(table, index);
+ node_loop:
+ for (;;) {
+ if (node == null) {
+ return null;
+ }
+
+ if (node.resize) {
+ table = (TableEntry[])node.getValuePlain();
+ continue table_loop;
+ }
+
+ boolean removed = false;
+ V ret = null;
+
+ synchronized (node) {
+ if (node != (node = getAtIndexVolatile(table, index))) {
+ continue node_loop;
+ }
+
+ TableEntry 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}.
+ *
+ *
+ * This function is a "functional methods" as defined by {@link ConcurrentLong2ReferenceChainedHashTable}.
+ *
+ *
+ * @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[] table = this.table;
+ table_loop:
+ for (;;) {
+ final int index = hash & (table.length - 1);
+
+ TableEntry node = getAtIndexVolatile(table, index);
+ node_loop:
+ for (;;) {
+ if (node == null) {
+ return null;
+ }
+
+ if (node.resize) {
+ table = (TableEntry[])node.getValuePlain();
+ continue table_loop;
+ }
+
+ boolean removed = false;
+ V ret = null;
+
+ synchronized (node) {
+ if (node != (node = getAtIndexVolatile(table, index))) {
+ continue node_loop;
+ }
+
+ TableEntry 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)}
+ *
+ * This function is a "functional methods" as defined by {@link ConcurrentLong2ReferenceChainedHashTable}.
+ *
+ */
+ public V compute(final long key, final BiLong1Function super V, ? extends V> function) {
+ final int hash = getHash(key);
+
+ TableEntry[] table = this.table;
+ table_loop:
+ for (;;) {
+ final int index = hash & (table.length - 1);
+
+ TableEntry node = getAtIndexVolatile(table, index);
+ node_loop:
+ for (;;) {
+ V ret = null;
+ if (node == null) {
+ final TableEntry 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[])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 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)}
+ *
+ * This function is a "functional methods" as defined by {@link ConcurrentLong2ReferenceChainedHashTable}.
+ *
+ */
+ public V computeIfAbsent(final long key, final LongFunction extends V> function) {
+ final int hash = getHash(key);
+
+ TableEntry[] table = this.table;
+ table_loop:
+ for (;;) {
+ final int index = hash & (table.length - 1);
+
+ TableEntry node = getAtIndexVolatile(table, index);
+ node_loop:
+ for (;;) {
+ V ret = null;
+ if (node == null) {
+ final TableEntry 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[])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 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)}
+ *
+ * This function is a "functional methods" as defined by {@link ConcurrentLong2ReferenceChainedHashTable}.
+ *
+ */
+ public V computeIfPresent(final long key, final BiLong1Function super V, ? extends V> function) {
+ final int hash = getHash(key);
+
+ TableEntry[] table = this.table;
+ table_loop:
+ for (;;) {
+ final int index = hash & (table.length - 1);
+
+ TableEntry node = getAtIndexVolatile(table, index);
+ node_loop:
+ for (;;) {
+ if (node == null) {
+ return null;
+ }
+
+ if (node.resize) {
+ table = (TableEntry[])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 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)}
+ *
+ * This function is a "functional methods" as defined by {@link ConcurrentLong2ReferenceChainedHashTable}.
+ *
+ */
+ 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[] table = this.table;
+ table_loop:
+ for (;;) {
+ final int index = hash & (table.length - 1);
+
+ TableEntry 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[])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 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.
+ *
+ *
+ * This function is not atomic.
+ *
+ */
+ 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 nodeIterator = new NodeIterator<>(this.table);
+
+ TableEntry 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> 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 valueIterator() {
+ return new ValueIterator<>(this);
+ }
+
+ protected static final class EntryIterator extends BaseIteratorImpl> {
+
+ protected EntryIterator(final ConcurrentLong2ReferenceChainedHashTable map) {
+ super(map);
+ }
+
+ @Override
+ public TableEntry next() throws NoSuchElementException {
+ return this.nextNode();
+ }
+
+ @Override
+ public void forEachRemaining(final Consumer super TableEntry> action) {
+ Validate.notNull(action, "Action may not be null");
+ while (this.hasNext()) {
+ action.accept(this.next());
+ }
+ }
+ }
+
+ protected static final class KeyIterator extends BaseIteratorImpl implements PrimitiveIterator.OfLong {
+
+ protected KeyIterator(final ConcurrentLong2ReferenceChainedHashTable 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 extends BaseIteratorImpl {
+
+ protected ValueIterator(final ConcurrentLong2ReferenceChainedHashTable 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 extends NodeIterator implements Iterator {
+
+ protected final ConcurrentLong2ReferenceChainedHashTable map;
+ protected TableEntry lastReturned;
+ protected TableEntry nextToReturn;
+
+ protected BaseIteratorImpl(final ConcurrentLong2ReferenceChainedHashTable 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 nextNode() throws NoSuchElementException {
+ TableEntry 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 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 {
+
+ protected TableEntry[] currentTable;
+ protected ResizeChain resizeChain;
+ protected TableEntry last;
+ protected int nextBin;
+ protected int increment;
+
+ protected NodeIterator(final TableEntry[] baseTable) {
+ this.currentTable = baseTable;
+ this.increment = 1;
+ }
+
+ private TableEntry[] pullResizeChain(final int index) {
+ final ResizeChain resizeChain = this.resizeChain;
+ if (resizeChain == null) {
+ this.currentTable = null;
+ return null;
+ }
+ final TableEntry[] newTable = resizeChain.table;
+ if (newTable == null) {
+ this.currentTable = null;
+ return null;
+ }
+
+ // the increment is a multiple of table.length, so we can undo the increments on idx by taking the
+ // mod
+ int newIdx = index & (newTable.length - 1);
+
+ final ResizeChain newChain = this.resizeChain = resizeChain.prev;
+ final TableEntry[] prevTable = newChain.table;
+ final int increment;
+ if (prevTable == null) {
+ increment = 1;
+ } else {
+ increment = prevTable.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[] pushResizeChain(final TableEntry[] table, final TableEntry entry) {
+ final ResizeChain chain = this.resizeChain;
+
+ if (chain == null) {
+ final TableEntry[] nextTable = (TableEntry[])entry.getValuePlain();
+
+ final ResizeChain oldChain = new ResizeChain<>(table, null, null);
+ final ResizeChain currChain = new ResizeChain<>(nextTable, oldChain, null);
+ oldChain.next = currChain;
+
+ this.increment = table.length;
+ this.resizeChain = currChain;
+ this.currentTable = nextTable;
+
+ return nextTable;
+ } else {
+ ResizeChain currChain = chain.next;
+ if (currChain == null) {
+ final TableEntry[] ret = (TableEntry[])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 findNext() {
+ for (;;) {
+ final TableEntry last = this.last;
+ if (last != null) {
+ final TableEntry next = last.getNextVolatile();
+ if (next != null) {
+ this.last = next;
+ if (next.getValuePlain() == null) {
+ // compute() node not yet available
+ continue;
+ }
+ return next;
+ }
+ }
+
+ TableEntry[] 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 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 {
+
+ protected final TableEntry[] table;
+ protected final ResizeChain prev;
+ protected ResizeChain next;
+
+ protected ResizeChain(final TableEntry[] table, final ResizeChain prev, final ResizeChain next) {
+ this.table = table;
+ this.next = next;
+ this.prev = prev;
+ }
+ }
+ }
+
+ public static final class TableEntry {
+
+ 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 next;
+ protected static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "next", TableEntry.class);
+
+ protected final TableEntry getNextPlain() {
+ //noinspection unchecked
+ return (TableEntry)NEXT_HANDLE.get(this);
+ }
+
+ protected final TableEntry getNextVolatile() {
+ //noinspection unchecked
+ return (TableEntry)NEXT_HANDLE.getVolatile(this);
+ }
+
+ protected final void setNextPlain(final TableEntry next) {
+ NEXT_HANDLE.set(this, next);
+ }
+
+ protected final void setNextRelease(final TableEntry next) {
+ NEXT_HANDLE.setRelease(this, next);
+ }
+
+ protected final void setNextVolatile(final TableEntry 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;
+
+/**
+ *
+ * Note: Not really tested, use at your own risk.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ *
+ * Subclasses should override {@link #clone()} to return correct instances of this class.
+ *
+ * @param {@inheritDoc}
+ * @param {@inheritDoc}
+ */
+public class SWMRHashTable implements Map, Iterable> {
+
+ protected int size;
+
+ protected TableEntry[] 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[] getTablePlain() {
+ //noinspection unchecked
+ return (TableEntry[])TABLE_HANDLE.get(this);
+ }
+
+ protected final TableEntry[] getTableAcquire() {
+ //noinspection unchecked
+ return (TableEntry[])TABLE_HANDLE.getAcquire(this);
+ }
+
+ protected final void setTablePlain(final TableEntry[] table) {
+ TABLE_HANDLE.set(this, table);
+ }
+
+ protected final void setTableRelease(final TableEntry[] 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[] 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 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 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 other) {
+ this(Math.max(Validate.notNull(other, "Null map").size(), capacity), loadFactor);
+ this.putAll(other);
+ }
+
+ protected static TableEntry getAtIndexOpaque(final TableEntry[] table, final int index) {
+ // noinspection unchecked
+ return (TableEntry)TableEntry.TABLE_ENTRY_ARRAY_HANDLE.getOpaque(table, index);
+ }
+
+ protected static void setAtIndexRelease(final TableEntry[] table, final int index, final TableEntry 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 getEntryForOpaque(final K key) {
+ final int hash = SWMRHashTable.getHash(key);
+ final TableEntry[] table = this.getTableAcquire();
+
+ for (TableEntry 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 getEntryForPlain(final K key) {
+ final int hash = SWMRHashTable.getHash(key);
+ final TableEntry[] table = this.getTablePlain();
+
+ for (TableEntry 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[] table = this.getTableAcquire();
+
+ for (int i = 0, len = table.length; i < len; ++i) {
+ for (TableEntry 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[] table = this.getTableAcquire();
+
+ for (int i = 0, len = table.length; i < len; ++i) {
+ for (TableEntry 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 clone() {
+ return new SWMRHashTable<>(this.getTableAcquire().length, this.loadFactor, this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Iterator> iterator() {
+ return new EntryIterator<>(this.getTableAcquire(), this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void forEach(final Consumer super Map.Entry> action) {
+ Validate.notNull(action, "Null action");
+
+ final TableEntry[] table = this.getTableAcquire();
+ for (int i = 0, len = table.length; i < len; ++i) {
+ for (TableEntry 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[] table = this.getTableAcquire();
+ for (int i = 0, len = table.length; i < len; ++i) {
+ for (TableEntry 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[] table = this.getTableAcquire();
+ for (int i = 0, len = table.length; i < len; ++i) {
+ for (TableEntry 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[] table = this.getTableAcquire();
+ for (int i = 0, len = table.length; i < len; ++i) {
+ for (TableEntry 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 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 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[] table = this.getTableAcquire();
+ for (int i = 0, len = table.length; i < len; ++i) {
+ for (TableEntry 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 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 keyset;
+ protected ValueCollection values;
+ protected EntrySet entrySet;
+
+ @Override
+ public Set keySet() {
+ return this.keyset == null ? this.keyset = new KeySet<>(this) : this.keyset;
+ }
+
+ @Override
+ public Collection values() {
+ return this.values == null ? this.values = new ValueCollection<>(this) : this.values;
+ }
+
+ @Override
+ public Set> 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[] 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[] newTable = new TableEntry[newCapacity];
+ final int indexMask = newCapacity - 1;
+
+ for (int i = 0, len = table.length; i < len; ++i) {
+ for (TableEntry 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 insert = new TableEntry<>(hash, entry.key, entry.getValuePlain());
+
+ final TableEntry 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[] table = this.getTablePlain();
+ final int hash = SWMRHashTable.getHash(key);
+ final int index = hash & (table.length - 1);
+
+ final TableEntry head = table[index];
+ if (head == null) {
+ final TableEntry insert = new TableEntry<>(hash, key, value);
+ setAtIndexRelease(table, index, insert);
+ this.addToSize(1);
+ return null;
+ }
+
+ for (TableEntry 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 next = curr.getNextPlain();
+ if (next != null) {
+ curr = next;
+ continue;
+ }
+
+ final TableEntry 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 predicate) {
+ Validate.notNull(predicate, "Null predicate");
+
+ int removed = 0;
+
+ final TableEntry[] table = this.getTablePlain();
+
+ bin_iteration_loop:
+ for (int i = 0, len = table.length; i < len; ++i) {
+ TableEntry 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 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> predicate) {
+ Validate.notNull(predicate, "Null predicate");
+
+ int removed = 0;
+
+ final TableEntry[] table = this.getTablePlain();
+
+ bin_iteration_loop:
+ for (int i = 0, len = table.length; i < len; ++i) {
+ TableEntry 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 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[] table = this.getTablePlain();
+ final int hash = SWMRHashTable.getHash(key);
+ final int index = hash & (table.length - 1);
+
+ final TableEntry 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 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[] table = this.getTablePlain();
+ final int index = (table.length - 1) & hash;
+
+ final TableEntry 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 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 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 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[] table = this.getTablePlain();
+ for (int i = 0, len = table.length; i < len; ++i) {
+ for (TableEntry 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}
+ *
+ * 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.
+ *
+ */
+ @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[] table = this.getTablePlain();
+ final int index = hash & (table.length - 1);
+
+ for (TableEntry 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 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[] table = this.getTablePlain();
+ final int index = hash & (table.length - 1);
+
+ for (TableEntry 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[] table = this.getTablePlain();
+ final int index = hash & (table.length - 1);
+
+ for (TableEntry 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 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[] table = this.getTablePlain();
+ final int index = hash & (table.length - 1);
+
+ for (TableEntry curr = table[index], prev = null;;prev = curr, curr = curr.getNextPlain()) {
+ if (curr == null) {
+ final TableEntry 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 implements Map.Entry {
+
+ 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