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..f4415f782b32fed25da98e44b172f717c4d46e34 --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/collection/MultiThreadedQueue.java @@ -0,0 +1,1402 @@ +package ca.spottedleaf.concurrentutil.collection; + +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.concurrentutil.util.Validate; +import java.lang.invoke.VarHandle; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Queue; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.function.Consumer; +import java.util.function.IntFunction; +import java.util.function.Predicate; + +/** + * MT-Safe linked first in first out ordered queue. + * + * This queue should out-perform {@link java.util.concurrent.ConcurrentLinkedQueue} in high-contention reads/writes, and is + * not any slower in lower contention reads/writes. + *

+ * 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 collection) { + final Iterator 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; + } + } + } + } + + /** + * 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 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 collection) { + return this.addAll((Iterable)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 iterable) { + Validate.notNull(iterable, "Null iterable"); + + final Iterator 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 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..597659f38aa816646dcda4ca39c002b6d9f9a792 --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/collection/SRSWLinkedQueue.java @@ -0,0 +1,148 @@ +package ca.spottedleaf.concurrentutil.collection; + +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.concurrentutil.util.Validate; +import java.lang.invoke.VarHandle; +import java.util.ConcurrentModificationException; + +/** + * Single reader thread single writer thread queue. The reader side of the queue is ordered by acquire semantics, + * and the writer side of the queue is ordered by release semantics. + */ +// TODO test +public class SRSWLinkedQueue { + + // 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..a1ad3308f9c3545a604b635896259a1cd3382b2a --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/completable/Completable.java @@ -0,0 +1,98 @@ +package ca.spottedleaf.concurrentutil.completable; + +import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; +import ca.spottedleaf.concurrentutil.executor.Cancellable; +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import com.mojang.logging.LogUtils; +import org.slf4j.Logger; +import java.util.function.BiConsumer; + +public final class Completable { + + private static final Logger LOGGER = LogUtils.getLogger(); + + 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; + } + + 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); + } + } + + 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..8c452b0988da4725762d543f6bee09915c328ae6 --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/BaseExecutor.java @@ -0,0 +1,198 @@ +package ca.spottedleaf.concurrentutil.executor; + +import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import java.util.function.BooleanSupplier; + +public interface BaseExecutor { + + /** + * Returns whether every task scheduled to this queue has been removed and executed or cancelled. If no tasks have been queued, + * returns {@code true}. + * + * @return {@code true} if all tasks that have been queued have finished executing or no tasks have been queued, {@code false} otherwise. + */ + public default boolean haveAllTasksExecuted() { + // order is important + // if new tasks are scheduled between the reading of these variables, scheduled is guaranteed to be higher - + // so our check fails, and we try again + final long completed = this.getTotalTasksExecuted(); + final long scheduled = this.getTotalTasksScheduled(); + + return completed == scheduled; + } + + /** + * Returns the number of tasks that have been scheduled or execute or are pending to be scheduled. + */ + public long getTotalTasksScheduled(); + + /** + * Returns the number of tasks that have fully been executed. + */ + public long getTotalTasksExecuted(); + + + /** + * Waits until this queue has had all of its tasks executed (NOT removed). See {@link #haveAllTasksExecuted()} + *

+ * 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 cal. + *

+ * + * @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. + *

+ * 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 + */ + 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}. + */ + public default void executeConditionally(final BooleanSupplier condition, final long deadline) { + long failures = 0; + // double check deadline; we don't know how expensive the condition is + while ((System.nanoTime() < deadline) && !condition.getAsBoolean() && (System.nanoTime() < deadline)) { + if (this.executeTask()) { + failures = failures >>> 2; + } else { + failures = ConcurrentUtil.linearLongBackoffDeadline(failures, 100_000L, 10_000_000L, deadline); // 100us, 10ms + } + } + } + + /** + * Waits and executes tasks until {@code System.nanoTime() >= deadline}. + */ + public default void executeUntil(final long deadline) { + long failures = 0; + while (System.nanoTime() < deadline) { + if (this.executeTask()) { + failures = failures >>> 2; + } else { + failures = ConcurrentUtil.linearLongBackoffDeadline(failures, 100_000L, 10_000_000L, deadline); // 100us, 10ms + } + } + } + + /** + * Prevent further additions to this queue. Attempts to add after this call has completed (potentially during) will + * result in {@link IllegalStateException} being thrown. + *

+ * 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 + */ + public default boolean shutdown() throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /** + * Returns whether this queue has shut down. Effectively, whether new tasks will be rejected - this method + * does not indicate whether all of the tasks scheduled have been executed. + * @return Returns whether this queue has shut down. + */ + public default boolean isShutdown() { + return false; + } + + public static interface BaseTask extends Cancellable { + + /** + * Causes a lazily queued task to become queued or executed + * + * @throws IllegalStateException If the backing queue has shutdown + * @return {@code true} If the task was queued, {@code false} if the task was already queued/cancelled/executed + */ + public boolean queue(); + + /** + * Forces this task to be marked as completed. + * + * @return {@code true} if the task was cancelled, {@code false} if the task has already completed or is being completed. + */ + @Override + public boolean cancel(); + + /** + * Executes this task. This will also mark the task as completing. + *

+ * 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..e5d8ff730ba9d83efc2d80782de313a718bf55b3 --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedExecutor.java @@ -0,0 +1,246 @@ +package ca.spottedleaf.concurrentutil.executor.standard; + +import ca.spottedleaf.concurrentutil.executor.BaseExecutor; + +public interface PrioritisedExecutor extends BaseExecutor { + + public static enum Priority { + + /** + * Priority value indicating the task has completed or is being completed. + * This priority cannot be used to schedule tasks. + */ + COMPLETING(-1), + + /** + * Absolute highest priority, should only be used for when a task is blocking a time-critical thread. + */ + BLOCKING(), + + /** + * Should only be used for urgent but not time-critical tasks. + */ + HIGHEST(), + + /** + * Two priorities above normal. + */ + HIGHER(), + + /** + * One priority above normal. + */ + HIGH(), + + /** + * Default priority. + */ + NORMAL(), + + /** + * One priority below normal. + */ + LOW(), + + /** + * Two priorities below normal. + */ + LOWER(), + + /** + * Use for tasks that should eventually execute, but are not needed to. + */ + LOWEST(), + + /** + * Use for tasks that can be delayed indefinitely. + */ + IDLE(); + + // returns whether the priority can be scheduled + public static boolean isValidPriority(final Priority priority) { + return priority != null && priority != Priority.COMPLETING; + } + + // returns the higher priority of the two + public static PrioritisedExecutor.Priority max(final Priority p1, final Priority p2) { + return p1.isHigherOrEqualPriority(p2) ? p1 : p2; + } + + // returns the lower priroity of the two + public static PrioritisedExecutor.Priority min(final Priority p1, final Priority p2) { + return p1.isLowerOrEqualPriority(p2) ? p1 : p2; + } + + public boolean isHigherOrEqualPriority(final Priority than) { + return this.priority <= than.priority; + } + + public boolean isHigherPriority(final Priority than) { + return this.priority < than.priority; + } + + public boolean isLowerOrEqualPriority(final Priority than) { + return this.priority >= than.priority; + } + + public boolean isLowerPriority(final Priority than) { + return this.priority > than.priority; + } + + public boolean isHigherOrEqualPriority(final int than) { + return this.priority <= than; + } + + public boolean isHigherPriority(final int than) { + return this.priority < than; + } + + public boolean isLowerOrEqualPriority(final int than) { + return this.priority >= than; + } + + public boolean isLowerPriority(final int than) { + return this.priority > than; + } + + public static boolean isHigherOrEqualPriority(final int priority, final int than) { + return priority <= than; + } + + public static boolean isHigherPriority(final int priority, final int than) { + return priority < than; + } + + public static boolean isLowerOrEqualPriority(final int priority, final int than) { + return priority >= than; + } + + public static boolean isLowerPriority(final int priority, final int than) { + return priority > than; + } + + static final PrioritisedExecutor.Priority[] PRIORITIES = PrioritisedExecutor.Priority.values(); + + /** includes special priorities */ + public static final int TOTAL_PRIORITIES = PRIORITIES.length; + + public static final int TOTAL_SCHEDULABLE_PRIORITIES = TOTAL_PRIORITIES - 1; + + public static PrioritisedExecutor.Priority getPriority(final int priority) { + return PRIORITIES[priority + 1]; + } + + private static int priorityCounter; + + private static int nextCounter() { + return priorityCounter++; + } + + public final int priority; + + Priority() { + this(nextCounter()); + } + + Priority(final int priority) { + this.priority = priority; + } + } + + /** + * Queues or executes a task at {@link Priority#NORMAL} priority. + * @param task The task to run. + * + * @throws IllegalStateException If this queue has shutdown. + * @throws NullPointerException If the task is null + * @return {@code null} if the current thread immediately executed the task, else returns the prioritised task + * associated with the parameter + */ + public default PrioritisedTask queueRunnable(final Runnable task) { + return this.queueRunnable(task, PrioritisedExecutor.Priority.NORMAL); + } + + /** + * Queues or executes a task. + * + * @param task The task to run. + * @param priority The priority for the task. + * + * @throws IllegalStateException If this queue has shutdown. + * @throws NullPointerException If the task is null + * @throws IllegalArgumentException If the priority is invalid. + * @return {@code null} if the current thread immediately executed the task, else returns the prioritised task + * associated with the parameter + */ + public PrioritisedTask queueRunnable(final Runnable task, final PrioritisedExecutor.Priority priority); + + /** + * Creates, but does not execute or queue the task. The task must later be queued via {@link BaseExecutor.BaseTask#queue()}. + * + * @param task The task to run. + * + * @throws IllegalStateException If this queue has shutdown. + * @throws NullPointerException If the task is null + * @throws IllegalArgumentException If the priority is invalid. + * @throws UnsupportedOperationException If this executor does not support lazily queueing tasks + * @return The prioritised task associated with the parameters + */ + public default PrioritisedExecutor.PrioritisedTask createTask(final Runnable task) { + return this.createTask(task, PrioritisedExecutor.Priority.NORMAL); + } + + /** + * Creates, but does not execute or queue the task. The task must later be queued via {@link BaseExecutor.BaseTask#queue()}. + * + * @param task The task to run. + * @param priority The priority for the task. + * + * @throws IllegalStateException If this queue has shutdown. + * @throws NullPointerException If the task is null + * @throws IllegalArgumentException If the priority is invalid. + * @throws UnsupportedOperationException If this executor does not support lazily queueing tasks + * @return The prioritised task associated with the parameters + */ + public PrioritisedExecutor.PrioritisedTask createTask(final Runnable task, final PrioritisedExecutor.Priority priority); + + public static interface PrioritisedTask extends BaseTask { + + /** + * Returns the current priority. Note that {@link PrioritisedExecutor.Priority#COMPLETING} will be returned + * if this task is completing or has completed. + */ + public PrioritisedExecutor.Priority getPriority(); + + /** + * Attempts to set this task's priority level to the level specified. + * + * @param priority Specified priority level. + * + * @throws IllegalArgumentException If the priority is invalid + * @return {@code true} if successful, {@code false} if this task is completing or has completed or the queue + * this task was scheduled on was shutdown, or if the priority was already at the specified level. + */ + public boolean setPriority(final PrioritisedExecutor.Priority priority); + + /** + * Attempts to raise the priority to the priority level specified. + * + * @param priority Priority specified + * + * @throws IllegalArgumentException If the priority is invalid + * @return {@code false} if the current task is completing, {@code true} if the priority was raised to the specified level or was already at the specified level or higher. + */ + public boolean raisePriority(final PrioritisedExecutor.Priority priority); + + /** + * Attempts to lower the priority to the priority level specified. + * + * @param priority Priority specified + * + * @throws IllegalArgumentException If the priority is invalid + * @return {@code false} if the current task is completing, {@code true} if the priority was lowered to the specified level or was already at the specified level or lower. + */ + public boolean lowerPriority(final PrioritisedExecutor.Priority priority); + } +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedQueueExecutorThread.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedQueueExecutorThread.java new file mode 100644 index 0000000000000000000000000000000000000000..91fe0f7049122f62f05ba09c24cba5d758340cff --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedQueueExecutorThread.java @@ -0,0 +1,297 @@ +package ca.spottedleaf.concurrentutil.executor.standard; + +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import com.mojang.logging.LogUtils; +import org.slf4j.Logger; +import java.lang.invoke.VarHandle; +import java.util.concurrent.locks.LockSupport; + +/** + * Thread which will continuously drain from a specified queue. + *

+ * 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 = LogUtils.getLogger(); + + protected final PrioritisedExecutor queue; + + protected volatile boolean threadShutdown; + + protected static final VarHandle THREAD_PARKED_HANDLE = ConcurrentUtil.getVarHandle(PrioritisedQueueExecutorThread.class, "threadParked", boolean.class); + protected volatile boolean threadParked; + + protected volatile boolean halted; + + protected final long spinWaitTime; + + static final long DEFAULT_SPINWAIT_TIME = (long)(0.1e6);// 0.1ms + + public PrioritisedQueueExecutorThread(final PrioritisedExecutor queue) { + this(queue, DEFAULT_SPINWAIT_TIME); // 0.1ms + } + + public PrioritisedQueueExecutorThread(final PrioritisedExecutor queue, final long spinWaitTime) { // in ns + this.queue = queue; + this.spinWaitTime = spinWaitTime; + } + + @Override + public void run() { + final long spinWaitTime = this.spinWaitTime; + + main_loop: + for (;;) { + this.pollTasks(); + + // spinwait + + final long start = System.nanoTime(); + + for (;;) { + // If we are interrupted for any reason, park() will always return immediately. Clear so that we don't needlessly use cpu in such an event. + Thread.interrupted(); + Thread.yield(); + LockSupport.parkNanos("Spinwaiting on tasks", 10_000L); // 10us + + if (this.pollTasks()) { + // restart loop, found tasks + continue main_loop; + } + + if (this.handleClose()) { + return; // we're done + } + + if ((System.nanoTime() - start) >= spinWaitTime) { + break; + } + } + + if (this.handleClose()) { + return; + } + + this.setThreadParkedVolatile(true); + + // We need to parse here to avoid a race condition where a thread queues a task before we set parked to true + // (i.e it will not notify us) + if (this.pollTasks()) { + this.setThreadParkedVolatile(false); + continue; + } + + if (this.handleClose()) { + return; + } + + // we don't need to check parked before sleeping, but we do need to check parked in a do-while loop + // LockSupport.park() can fail for any reason + while (this.getThreadParkedVolatile()) { + Thread.interrupted(); + LockSupport.park("Waiting on tasks"); + } + } + } + + protected boolean pollTasks() { + boolean ret = false; + + for (;;) { + if (this.halted) { + break; + } + try { + if (!this.queue.executeTask()) { + break; + } + ret = true; + } catch (final ThreadDeath death) { + throw death; // goodbye world... + } catch (final Throwable throwable) { + LOGGER.error("Exception thrown from prioritized runnable task in thread '" + this.getName() + "'", throwable); + } + } + + return ret; + } + + protected boolean handleClose() { + if (this.threadShutdown) { + this.pollTasks(); // this ensures we've emptied the queue + return true; + } + return false; + } + + /** + * Notify this thread that a task has been added to its queue + * @return {@code true} if this thread was waiting for tasks, {@code false} if it is executing tasks + */ + public boolean notifyTasks() { + if (this.getThreadParkedVolatile() && this.exchangeThreadParkedVolatile(false)) { + LockSupport.unpark(this); + return true; + } + return false; + } + + @Override + public PrioritisedTask createTask(final Runnable task, final Priority priority) { + final PrioritisedExecutor.PrioritisedTask queueTask = this.queue.createTask(task, priority); + + // need to override queue() to notify us of tasks + return new PrioritisedTask() { + @Override + public Priority getPriority() { + return queueTask.getPriority(); + } + + @Override + public boolean setPriority(final Priority priority) { + return queueTask.setPriority(priority); + } + + @Override + public boolean raisePriority(final Priority priority) { + return queueTask.raisePriority(priority); + } + + @Override + public boolean lowerPriority(final Priority priority) { + return queueTask.lowerPriority(priority); + } + + @Override + public boolean queue() { + final boolean ret = queueTask.queue(); + if (ret) { + PrioritisedQueueExecutorThread.this.notifyTasks(); + } + return ret; + } + + @Override + public boolean cancel() { + return queueTask.cancel(); + } + + @Override + public boolean execute() { + return queueTask.execute(); + } + }; + } + + @Override + public PrioritisedExecutor.PrioritisedTask queueRunnable(final Runnable task, final PrioritisedExecutor.Priority priority) { + final PrioritisedExecutor.PrioritisedTask ret = this.queue.queueRunnable(task, priority); + + this.notifyTasks(); + + return ret; + } + + @Override + public boolean haveAllTasksExecuted() { + return this.queue.haveAllTasksExecuted(); + } + + @Override + public long getTotalTasksExecuted() { + return this.queue.getTotalTasksExecuted(); + } + + @Override + public long getTotalTasksScheduled() { + return this.queue.getTotalTasksScheduled(); + } + + /** + * {@inheritDoc} + * @throws IllegalStateException If the current thread is {@code this} thread, or the underlying queue throws this exception. + */ + @Override + public void waitUntilAllExecuted() throws IllegalStateException { + if (Thread.currentThread() == this) { + throw new IllegalStateException("Cannot block on our own queue"); + } + this.queue.waitUntilAllExecuted(); + } + + /** + * {@inheritDoc} + * @throws IllegalStateException Always + */ + @Override + public boolean executeTask() throws IllegalStateException { + throw new IllegalStateException(); + } + + /** + * Closes this queue executor's queue. Optionally waits for all tasks in queue to be executed if {@code wait} is true. + *

+ * 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..26fa2caa18a9194e57574a4a7fa9f7a4265740e0 --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadPool.java @@ -0,0 +1,579 @@ +package ca.spottedleaf.concurrentutil.executor.standard; + +import com.mojang.logging.LogUtils; +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; +import org.slf4j.Logger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.TreeSet; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiConsumer; + +public final class PrioritisedThreadPool { + + private static final Logger LOGGER = LogUtils.getLogger(); + + protected final PrioritisedThread[] threads; + protected final TreeSet queues = new TreeSet<>(PrioritisedPoolExecutorImpl.comparator()); + protected final String name; + protected final long queueMaxHoldTime; + + protected final ReferenceOpenHashSet nonShutdownQueues = new ReferenceOpenHashSet<>(); + protected final ReferenceOpenHashSet activeQueues = new ReferenceOpenHashSet<>(); + + protected boolean shutdown; + + protected long schedulingIdGenerator; + + protected static final long DEFAULT_QUEUE_HOLD_TIME = (long)(5.0e6); + + public PrioritisedThreadPool(final String name, final int threads) { + this(name, threads, null); + } + + public PrioritisedThreadPool(final String name, final int threads, final BiConsumer threadModifier) { + this(name, threads, threadModifier, DEFAULT_QUEUE_HOLD_TIME); // 5ms + } + + 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(); + } + } + + public Thread[] getThreads() { + return Arrays.copyOf(this.threads, this.threads.length, Thread[].class); + } + + public PrioritisedPoolExecutor createExecutor(final String name, final int parallelism) { + synchronized (this.nonShutdownQueues) { + if (this.shutdown) { + throw new IllegalStateException("Queue is shutdown: " + this.toString()); + } + final PrioritisedPoolExecutorImpl ret = new PrioritisedPoolExecutorImpl(this, name, Math.min(Math.max(1, parallelism), this.threads.length)); + + this.nonShutdownQueues.add(ret); + + synchronized (this.activeQueues) { + this.activeQueues.add(ret); + } + + return ret; + } + } + + /** + * Prevents creation of new queues, shutdowns all non-shutdown queues if specified + */ + public void halt(final boolean shutdownQueues) { + synchronized (this.nonShutdownQueues) { + this.shutdown = true; + } + if (shutdownQueues) { + final ArrayList queuesToShutdown; + synchronized (this.nonShutdownQueues) { + this.shutdown = true; + queuesToShutdown = new ArrayList<>(this.nonShutdownQueues); + } + + for (final PrioritisedPoolExecutorImpl queue : queuesToShutdown) { + queue.shutdown(); + } + } + + + for (final PrioritisedThread thread : this.threads) { + // can't kill queue, queue is null + thread.halt(false); + } + } + + /** + * Waits until all threads in this pool have shutdown, or until the specified time has passed. + * @param msToWait Maximum time to wait. + * @return {@code false} if the maximum time passed, {@code true} otherwise. + */ + public boolean join(final long msToWait) { + try { + return this.join(msToWait, false); + } catch (final InterruptedException ex) { + throw new IllegalStateException(ex); + } + } + + /** + * Waits until all threads in this pool have shutdown, or until the specified time has passed. + * @param msToWait Maximum time to wait. + * @return {@code false} if the maximum time passed, {@code true} otherwise. + * @throws InterruptedException If this thread is interrupted. + */ + public boolean joinInterruptable(final long msToWait) throws InterruptedException { + return this.join(msToWait, true); + } + + protected final boolean join(final long msToWait, final boolean interruptable) throws InterruptedException { + final long nsToWait = msToWait * (1000 * 1000); + final long start = System.nanoTime(); + final long deadline = start + nsToWait; + boolean interrupted = false; + try { + for (final PrioritisedThread thread : this.threads) { + for (;;) { + if (!thread.isAlive()) { + break; + } + final long current = System.nanoTime(); + if (current >= deadline) { + return false; + } + + try { + thread.join(Math.max(1L, (deadline - current) / (1000 * 1000))); + } catch (final InterruptedException ex) { + if (interruptable) { + throw ex; + } + interrupted = true; + } + } + } + + return true; + } finally { + if (interrupted) { + Thread.currentThread().interrupt(); + } + } + } + + public void shutdown(final boolean wait) { + final ArrayList 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 boolean isQueued; + + public PrioritisedPoolExecutorImpl(final PrioritisedThreadPool pool, final String name, final int maximumExecutors) { + this.pool = pool; + this.name = name; + this.maximumExecutors = maximumExecutors; + } + + public static Comparator comparator() { + return (final PrioritisedPoolExecutorImpl p1, final PrioritisedPoolExecutorImpl p2) -> { + if (p1 == p2) { + return 0; + } + + // prefer higher priority + final int priorityCompare = p1.scheduledPriority.ordinal() - p2.scheduledPriority.ordinal(); + if (priorityCompare != 0) { + return priorityCompare; + } + + // try to spread out the executors so that each can have threads executing + final int executorCompare = p1.concurrentExecutors - p2.concurrentExecutors; + if (executorCompare != 0) { + return executorCompare; + } + + // if all else fails here we just choose whichever executor was queued first + return Long.compare(p1.schedulingId, p2.schedulingId); + }; + } + + private boolean isHalted; + + @Override + public void halt() { + final PrioritisedThreadPool pool = this.pool; + final TreeSet 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..b71404be2c82f7db35272b367af861e94d6c73d3 --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadedTaskQueue.java @@ -0,0 +1,378 @@ +package ca.spottedleaf.concurrentutil.executor.standard; + +import java.util.ArrayDeque; +import java.util.concurrent.atomic.AtomicLong; + +public class PrioritisedThreadedTaskQueue implements PrioritisedExecutor { + + protected final ArrayDeque[] queues = new ArrayDeque[Priority.TOTAL_SCHEDULABLE_PRIORITIES]; { + for (int i = 0; i < Priority.TOTAL_SCHEDULABLE_PRIORITIES; ++i) { + this.queues[i] = new ArrayDeque<>(); + } + } + + // Use AtomicLong to separate from the queue field, we don't want false sharing here. + protected final AtomicLong totalScheduledTasks = new AtomicLong(); + protected final AtomicLong totalCompletedTasks = new AtomicLong(); + + // this is here to prevent failures to queue stalling flush() calls (as the schedule calls would increment totalScheduledTasks without this check) + protected volatile boolean hasShutdown; + + protected long taskIdGenerator = 0; + + @Override + public PrioritisedExecutor.PrioritisedTask queueRunnable(final Runnable task, final PrioritisedExecutor.Priority priority) throws IllegalStateException, IllegalArgumentException { + if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Priority " + priority + " is invalid"); + } + if (task == null) { + throw new NullPointerException("Task cannot be null"); + } + + if (this.hasShutdown) { + // prevent us from stalling flush() calls by incrementing scheduled tasks when we really didn't schedule something + throw new IllegalStateException("Queue has shutdown"); + } + + final PrioritisedTask ret; + + synchronized (this.queues) { + if (this.hasShutdown) { + throw new IllegalStateException("Queue has shutdown"); + } + this.getAndAddTotalScheduledTasksVolatile(1L); + + ret = new PrioritisedTask(this.taskIdGenerator++, task, priority, this); + + this.queues[ret.priority.priority].add(ret); + + // call priority change callback (note: only after we successfully queue!) + this.priorityChange(ret, null, priority); + } + + return ret; + } + + @Override + public PrioritisedExecutor.PrioritisedTask createTask(final Runnable task, final Priority priority) { + if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Priority " + priority + " is invalid"); + } + if (task == null) { + throw new NullPointerException("Task cannot be null"); + } + + return new PrioritisedTask(task, priority, this); + } + + @Override + public long getTotalTasksScheduled() { + return this.totalScheduledTasks.get(); + } + + @Override + public long getTotalTasksExecuted() { + return this.totalCompletedTasks.get(); + } + + // callback method for subclasses to override + // from is null when a task is immediately created + protected void priorityChange(final PrioritisedTask task, final Priority from, final Priority to) {} + + /** + * Polls the highest priority task currently available. {@code null} if none. This will mark the + * returned task as completed. + */ + protected PrioritisedTask poll() { + return this.poll(Priority.IDLE); + } + + protected PrioritisedTask poll(final PrioritisedExecutor.Priority minPriority) { + final ArrayDeque[] 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 PrioritisedExecutor.Priority priority; + + protected PrioritisedTask(final long id, final Runnable runnable, final PrioritisedExecutor.Priority priority, final PrioritisedThreadedTaskQueue queue) { + if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + + this.priority = priority; + this.runnable = runnable; + this.queue = queue; + this.id = id; + } + + protected PrioritisedTask(final Runnable runnable, final PrioritisedExecutor.Priority priority, final PrioritisedThreadedTaskQueue queue) { + if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + + this.priority = priority; + this.runnable = runnable; + this.queue = queue; + this.id = NOT_SCHEDULED_ID; + } + + @Override + public boolean queue() { + if (this.queue.hasShutdown) { + throw new IllegalStateException("Queue has shutdown"); + } + + synchronized (this.queue.queues) { + if (this.queue.hasShutdown) { + throw new IllegalStateException("Queue has shutdown"); + } + + final PrioritisedExecutor.Priority priority = this.priority; + if (priority == PrioritisedExecutor.Priority.COMPLETING) { + return false; + } + + if (this.id != NOT_SCHEDULED_ID) { + return false; + } + + this.queue.getAndAddTotalScheduledTasksVolatile(1L); + this.id = this.queue.taskIdGenerator++; + this.queue.queues[priority.priority].add(this); + + this.queue.priorityChange(this, null, priority); + + return true; + } + } + + protected boolean trySetCompleting(final int minPriority) { + final PrioritisedExecutor.Priority oldPriority = this.priority; + if (oldPriority != PrioritisedExecutor.Priority.COMPLETING && oldPriority.isHigherOrEqualPriority(minPriority)) { + this.priority = PrioritisedExecutor.Priority.COMPLETING; + if (this.id != NOT_SCHEDULED_ID) { + this.queue.priorityChange(this, oldPriority, PrioritisedExecutor.Priority.COMPLETING); + } + return true; + } + + return false; + } + + @Override + public PrioritisedExecutor.Priority getPriority() { + return this.priority; + } + + @Override + public boolean setPriority(final PrioritisedExecutor.Priority priority) { + if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + synchronized (this.queue.queues) { + final PrioritisedExecutor.Priority curr = this.priority; + + if (curr == PrioritisedExecutor.Priority.COMPLETING) { + return false; + } + + if (curr == priority) { + return true; + } + + this.priority = priority; + if (this.id != NOT_SCHEDULED_ID) { + this.queue.queues[priority.priority].add(this); + + // call priority change callback + this.queue.priorityChange(this, curr, priority); + } + } + + return true; + } + + @Override + public boolean raisePriority(final PrioritisedExecutor.Priority priority) { + if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + + synchronized (this.queue.queues) { + final PrioritisedExecutor.Priority curr = this.priority; + + if (curr == PrioritisedExecutor.Priority.COMPLETING) { + return false; + } + + if (curr.isHigherOrEqualPriority(priority)) { + return true; + } + + this.priority = priority; + if (this.id != NOT_SCHEDULED_ID) { + this.queue.queues[priority.priority].add(this); + + // call priority change callback + this.queue.priorityChange(this, curr, priority); + } + } + + return true; + } + + @Override + public boolean lowerPriority(final PrioritisedExecutor.Priority priority) { + if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + + synchronized (this.queue.queues) { + final PrioritisedExecutor.Priority curr = this.priority; + + if (curr == PrioritisedExecutor.Priority.COMPLETING) { + return false; + } + + if (curr.isLowerOrEqualPriority(priority)) { + return true; + } + + this.priority = priority; + if (this.id != NOT_SCHEDULED_ID) { + this.queue.queues[priority.priority].add(this); + + // call priority change callback + this.queue.priorityChange(this, curr, priority); + } + } + + return true; + } + + @Override + public boolean cancel() { + final long id; + synchronized (this.queue.queues) { + final Priority oldPriority = this.priority; + if (oldPriority == PrioritisedExecutor.Priority.COMPLETING) { + return false; + } + + this.priority = PrioritisedExecutor.Priority.COMPLETING; + // call priority change callback + if ((id = this.id) != NOT_SCHEDULED_ID) { + this.queue.priorityChange(this, oldPriority, PrioritisedExecutor.Priority.COMPLETING); + } + } + this.runnable = null; + if (id != NOT_SCHEDULED_ID) { + this.queue.getAndAddTotalCompletedTasksVolatile(1L); + } + return true; + } + + protected void executeInternal() { + try { + final Runnable execute = this.runnable; + this.runnable = null; + execute.run(); + } finally { + if (this.id != NOT_SCHEDULED_ID) { + this.queue.getAndAddTotalCompletedTasksVolatile(1L); + } + } + } + + @Override + public boolean execute() { + synchronized (this.queue.queues) { + final Priority oldPriority = this.priority; + if (oldPriority == PrioritisedExecutor.Priority.COMPLETING) { + return false; + } + + this.priority = PrioritisedExecutor.Priority.COMPLETING; + // call priority change callback + if (this.id != NOT_SCHEDULED_ID) { + this.queue.priorityChange(this, oldPriority, PrioritisedExecutor.Priority.COMPLETING); + } + } + + this.executeInternal(); + return true; + } + } +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRHashTable.java b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRHashTable.java new file mode 100644 index 0000000000000000000000000000000000000000..a037bb57bedc0cde6b979f5c1f9669678fa7bd16 --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRHashTable.java @@ -0,0 +1,1673 @@ +package ca.spottedleaf.concurrentutil.map; + +import ca.spottedleaf.concurrentutil.util.ArrayUtil; +import ca.spottedleaf.concurrentutil.util.CollectionUtil; +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.concurrentutil.util.Validate; +import io.papermc.paper.util.IntegerUtil; +import java.lang.invoke.VarHandle; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.BiPredicate; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.IntFunction; +import java.util.function.Predicate; + +/** + *

+ * 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); + } + + 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 = ArrayUtil.getOpaque(table, hash & (table.length - 1)); curr != null; curr = curr.getNextOpaque()) { + if (hash == curr.hash && (key == curr.key || curr.key.equals(key))) { + return curr; + } + } + + return null; + } + + protected final TableEntry 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(); + // inlined IntegerUtil#hash0 + hash *= 0x36935555; + hash ^= hash >>> 16; + return hash; + } + + static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash + static final int spread(int h) { + return (h ^ (h >>> 16)) & HASH_BITS; + } + + // rets -1 if capacity*loadFactor is too large + protected static int getTargetCapacity(final int capacity, final float loadFactor) { + final double ret = (double)capacity * (double)loadFactor; + if (Double.isInfinite(ret) || ret >= ((double)Integer.MAX_VALUE)) { + return -1; + } + + return (int)ret; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + /* Make no attempt to deal with concurrent modifications */ + if (!(obj instanceof Map)) { + return false; + } + final Map other = (Map)obj; + + if (this.size() != other.size()) { + return false; + } + + final TableEntry[] table = this.getTableAcquire(); + + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + final V value = curr.getValueAcquire(); + + final Object otherValue = other.get(curr.key); + if (otherValue == null || (value != otherValue && value.equals(otherValue))) { + return false; + } + } + } + + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + /* Make no attempt to deal with concurrent modifications */ + int hash = 0; + final TableEntry[] table = this.getTableAcquire(); + + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + hash += curr.hashCode(); + } + } + + return hash; + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(64); + builder.append("SingleWriterMultiReaderHashMap:{"); + + this.forEach((final K key, final V value) -> { + builder.append("{key: \"").append(key).append("\", value: \"").append(value).append("\"}"); + }); + + return builder.append('}').toString(); + } + + /** + * {@inheritDoc} + */ + @Override + public SWMRHashTable 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> action) { + Validate.notNull(action, "Null action"); + + final TableEntry[] table = this.getTableAcquire(); + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + action.accept(curr); + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public void forEach(final BiConsumer action) { + Validate.notNull(action, "Null action"); + + final TableEntry[] table = this.getTableAcquire(); + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + final V value = curr.getValueAcquire(); + + action.accept(curr.key, value); + } + } + } + + /** + * Provides the specified consumer with all keys contained within this map. + * @param action The specified consumer. + */ + public void forEachKey(final Consumer action) { + Validate.notNull(action, "Null action"); + + final TableEntry[] table = this.getTableAcquire(); + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + action.accept(curr.key); + } + } + } + + /** + * Provides the specified consumer with all values contained within this map. Equivalent to {@code map.values().forEach(Consumer)}. + * @param action The specified consumer. + */ + public void forEachValue(final Consumer action) { + Validate.notNull(action, "Null action"); + + final TableEntry[] table = this.getTableAcquire(); + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + final V value = curr.getValueAcquire(); + + action.accept(value); + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public V get(final Object key) { + Validate.notNull(key, "Null key"); + + //noinspection unchecked + final TableEntry 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 = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + final V currVal = curr.getValueAcquire(); + if (currVal == value || currVal.equals(value)) { + return true; + } + } + } + + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public V getOrDefault(final Object key, final V defaultValue) { + Validate.notNull(key, "Null key"); + + //noinspection unchecked + final TableEntry entry = this.getEntryForOpaque((K)key); + + return entry == null ? defaultValue : entry.getValueAcquire(); + } + + /** + * {@inheritDoc} + */ + @Override + public int size() { + return this.getSizeAcquire(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isEmpty() { + return this.getSizeAcquire() == 0; + } + + protected Set keyset; + protected Collection values; + protected Set> 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); + ArrayUtil.setRelease(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 */ + + ArrayUtil.setRelease(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> 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 */ + + ArrayUtil.setRelease(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; + } + + ArrayUtil.setRelease(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))) { + ArrayUtil.setRelease(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 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 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 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) { + ArrayUtil.setRelease(table, index, insert); + } else { + prev.setNextRelease(insert); + } + + this.addToSize(1); + + return newVal; + } + + if (curr.hash == hash && (curr.key == key || curr.key.equals(key))) { + final V newVal = remappingFunction.apply(key, curr.getValuePlain()); + + if (newVal != null) { + curr.setValueRelease(newVal); + return newVal; + } + + if (prev == null) { + ArrayUtil.setRelease(table, index, curr.getNextPlain()); + } else { + prev.setNextRelease(curr.getNextPlain()); + } + + this.removeFromSize(1); + + return null; + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public V computeIfPresent(final K key, final BiFunction 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) { + ArrayUtil.setRelease(table, index, curr.getNextPlain()); + } else { + prev.setNextRelease(curr.getNextPlain()); + } + + this.removeFromSize(1); + + return null; + } + + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public V computeIfAbsent(final K key, final Function 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) { + ArrayUtil.setRelease(table, index, insert); + } else { + prev.setNextRelease(insert); + } + + this.addToSize(1); + + return newVal; + } + } + + /** + * {@inheritDoc} + */ + @Override + public V merge(final K key, final V value, final BiFunction 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) { + ArrayUtil.setRelease(table, index, insert); + } else { + prev.setNextRelease(insert); + } + + this.addToSize(1); + + return value; + } + + if (curr.hash == hash && (curr.key == key || curr.key.equals(key))) { + final V newVal = remappingFunction.apply(curr.getValuePlain(), value); + + if (newVal != null) { + curr.setValueRelease(newVal); + return newVal; + } + + if (prev == null) { + ArrayUtil.setRelease(table, index, curr.getNextPlain()); + } else { + prev.setNextRelease(curr.getNextPlain()); + } + + this.removeFromSize(1); + + return null; + } + } + } + + protected static final class TableEntry implements Map.Entry { + + protected final int hash; + protected final K key; + protected V value; + + protected TableEntry next; + + protected static final VarHandle VALUE_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "value", Object.class); + protected static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "next", TableEntry.class); + + /* value */ + + protected final V getValuePlain() { + //noinspection unchecked + return (V)VALUE_HANDLE.get(this); + } + + protected final V getValueAcquire() { + //noinspection unchecked + return (V)VALUE_HANDLE.getAcquire(this); + } + + protected final void setValueRelease(final V to) { + VALUE_HANDLE.setRelease(this, to); + } + + /* next */ + + protected final TableEntry getNextPlain() { + //noinspection unchecked + return (TableEntry)NEXT_HANDLE.get(this); + } + + protected final TableEntry getNextOpaque() { + //noinspection unchecked + return (TableEntry)NEXT_HANDLE.getOpaque(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 TableEntry(final int hash, final K key, final V value) { + this.hash = hash; + this.key = key; + this.value = value; + } + + /** + * {@inheritDoc} + */ + @Override + public K getKey() { + return this.key; + } + + /** + * {@inheritDoc} + */ + @Override + public V getValue() { + return this.getValueAcquire(); + } + + /** + * {@inheritDoc} + */ + @Override + public V setValue(final V value) { + if (value == null) { + throw new NullPointerException(); + } + + final V curr = this.getValuePlain(); + + this.setValueRelease(value); + return curr; + } + + protected static int hash(final Object key, final Object value) { + return key.hashCode() ^ (value == null ? 0 : value.hashCode()); + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return hash(this.key, this.getValueAcquire()); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + + if (!(obj instanceof Map.Entry)) { + return false; + } + final Map.Entry other = (Map.Entry)obj; + final Object otherKey = other.getKey(); + final Object otherValue = other.getValue(); + + final K thisKey = this.getKey(); + final V thisVal = this.getValueAcquire(); + return (thisKey == otherKey || thisKey.equals(otherKey)) && + (thisVal == otherValue || thisVal.equals(otherValue)); + } + } + + + protected static abstract class TableEntryIterator implements Iterator { + + protected final TableEntry[] table; + protected final SWMRHashTable map; + + /* bin which our current element resides on */ + protected int tableIndex; + + protected TableEntry currEntry; /* curr entry, null if no more to iterate or if curr was removed or if we've just init'd */ + protected TableEntry nextEntry; /* may not be on the same bin as currEntry */ + + protected TableEntryIterator(final TableEntry[] table, final SWMRHashTable map) { + this.table = table; + this.map = map; + int tableIndex = 0; + for (int len = table.length; tableIndex < len; ++tableIndex) { + final TableEntry entry = ArrayUtil.getOpaque(table, tableIndex); + if (entry != null) { + this.nextEntry = entry; + this.tableIndex = tableIndex + 1; + return; + } + } + this.tableIndex = tableIndex; + } + + @Override + public boolean hasNext() { + return this.nextEntry != null; + } + + protected final TableEntry advanceEntry() { + final TableEntry[] table = this.table; + final int tableLength = table.length; + int tableIndex = this.tableIndex; + final TableEntry curr = this.nextEntry; + if (curr == null) { + return null; + } + + this.currEntry = curr; + + // set up nextEntry + + // find next in chain + TableEntry next = curr.getNextOpaque(); + + if (next != null) { + this.nextEntry = next; + return curr; + } + + // nothing in chain, so find next available bin + for (;tableIndex < tableLength; ++tableIndex) { + next = ArrayUtil.getOpaque(table, tableIndex); + if (next != null) { + this.nextEntry = next; + this.tableIndex = tableIndex + 1; + return curr; + } + } + + this.nextEntry = null; + this.tableIndex = tableIndex; + return curr; + } + + @Override + public void remove() { + final TableEntry curr = this.currEntry; + if (curr == null) { + throw new IllegalStateException(); + } + + this.map.remove(curr.key, curr.hash); + + this.currEntry = null; + } + } + + protected static final class ValueIterator extends TableEntryIterator { + + protected ValueIterator(final TableEntry[] table, final SWMRHashTable map) { + super(table, map); + } + + @Override + public V next() { + final TableEntry entry = this.advanceEntry(); + + if (entry == null) { + throw new NoSuchElementException(); + } + + return entry.getValueAcquire(); + } + } + + protected static final class KeyIterator extends TableEntryIterator { + + protected KeyIterator(final TableEntry[] table, final SWMRHashTable map) { + super(table, map); + } + + @Override + public K next() { + final TableEntry curr = this.advanceEntry(); + + if (curr == null) { + throw new NoSuchElementException(); + } + + return curr.key; + } + } + + protected static final class EntryIterator extends TableEntryIterator> { + + protected EntryIterator(final TableEntry[] table, final SWMRHashTable map) { + super(table, map); + } + + @Override + public Map.Entry next() { + final TableEntry curr = this.advanceEntry(); + + if (curr == null) { + throw new NoSuchElementException(); + } + + return curr; + } + } + + protected static abstract class ViewCollection implements Collection { + + protected final SWMRHashTable map; + + protected ViewCollection(final SWMRHashTable map) { + this.map = map; + } + + @Override + public boolean add(final T element) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(final Collection collections) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeAll(final Collection collection) { + Validate.notNull(collection, "Null collection"); + + boolean modified = false; + for (final Object element : collection) { + modified |= this.remove(element); + } + return modified; + } + + @Override + public int size() { + return this.map.size(); + } + + @Override + public boolean isEmpty() { + return this.size() == 0; + } + + @Override + public void clear() { + this.map.clear(); + } + + @Override + public boolean containsAll(final Collection collection) { + Validate.notNull(collection, "Null collection"); + + for (final Object element : collection) { + if (!this.contains(element)) { + return false; + } + } + + return true; + } + + @Override + public Object[] toArray() { + final List list = new ArrayList<>(this.size()); + + this.forEach(list::add); + + return list.toArray(); + } + + @Override + public E[] toArray(final E[] array) { + final List list = new ArrayList<>(this.size()); + + this.forEach(list::add); + + return list.toArray(array); + } + + @Override + public E[] toArray(final IntFunction generator) { + final List list = new ArrayList<>(this.size()); + + this.forEach(list::add); + + return list.toArray(generator); + } + + @Override + public int hashCode() { + int hash = 0; + for (final T element : this) { + hash += element == null ? 0 : element.hashCode(); + } + return hash; + } + + @Override + public Spliterator spliterator() { // TODO implement + return Spliterators.spliterator(this, Spliterator.NONNULL); + } + } + + protected static abstract class ViewSet extends ViewCollection implements Set { + + protected ViewSet(final SWMRHashTable map) { + super(map); + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + + if (!(obj instanceof Set)) { + return false; + } + + final Set other = (Set)obj; + if (other.size() != this.size()) { + return false; + } + + return this.containsAll(other); + } + } + + protected static final class EntrySet extends ViewSet> implements Set> { + + protected EntrySet(final SWMRHashTable map) { + super(map); + } + + @Override + public boolean remove(final Object object) { + if (!(object instanceof Map.Entry)) { + return false; + } + final Map.Entry entry = (Map.Entry)object; + + final Object key; + final Object value; + + try { + key = entry.getKey(); + value = entry.getValue(); + } catch (final IllegalStateException ex) { + return false; + } + + return this.map.remove(key, value); + } + + @Override + public boolean removeIf(final Predicate> filter) { + Validate.notNull(filter, "Null filter"); + + return this.map.removeEntryIf(filter) != 0; + } + + @Override + public boolean retainAll(final Collection collection) { + Validate.notNull(collection, "Null collection"); + + return this.map.removeEntryIf((final Map.Entry entry) -> { + return !collection.contains(entry); + }) != 0; + } + + @Override + public Iterator> iterator() { + return new EntryIterator<>(this.map.getTableAcquire(), this.map); + } + + @Override + public void forEach(final Consumer> action) { + this.map.forEach(action); + } + + @Override + public boolean contains(final Object object) { + if (!(object instanceof Map.Entry)) { + return false; + } + final Map.Entry entry = (Map.Entry)object; + + final Object key; + final Object value; + + try { + key = entry.getKey(); + value = entry.getValue(); + } catch (final IllegalStateException ex) { + return false; + } + + return this.map.contains(key, value); + } + + @Override + public String toString() { + return CollectionUtil.toString(this, "SWMRHashTableEntrySet"); + } + } + + protected static final class KeySet extends ViewSet { + + protected KeySet(final SWMRHashTable map) { + super(map); + } + + @Override + public Iterator iterator() { + return new KeyIterator<>(this.map.getTableAcquire(), this.map); + } + + @Override + public void forEach(final Consumer action) { + Validate.notNull(action, "Null action"); + + this.map.forEachKey(action); + } + + @Override + public boolean contains(final Object key) { + Validate.notNull(key, "Null key"); + + return this.map.containsKey(key); + } + + @Override + public boolean remove(final Object key) { + Validate.notNull(key, "Null key"); + + return this.map.remove(key) != null; + } + + @Override + public boolean retainAll(final Collection collection) { + Validate.notNull(collection, "Null collection"); + + return this.map.removeIf((final K key, final V value) -> { + return !collection.contains(key); + }) != 0; + } + + @Override + public boolean removeIf(final Predicate filter) { + Validate.notNull(filter, "Null filter"); + + return this.map.removeIf((final K key, final V value) -> { + return filter.test(key); + }) != 0; + } + + @Override + public String toString() { + return CollectionUtil.toString(this, "SWMRHashTableKeySet"); + } + } + + protected static final class ValueCollection extends ViewSet implements Collection { + + protected ValueCollection(final SWMRHashTable map) { + super(map); + } + + @Override + public Iterator iterator() { + return new ValueIterator<>(this.map.getTableAcquire(), this.map); + } + + @Override + public void forEach(final Consumer action) { + Validate.notNull(action, "Null action"); + + this.map.forEachValue(action); + } + + @Override + public boolean contains(final Object object) { + Validate.notNull(object, "Null object"); + + return this.map.containsValue(object); + } + + @Override + public boolean remove(final Object object) { + Validate.notNull(object, "Null object"); + + final Iterator itr = this.iterator(); + while (itr.hasNext()) { + final V val = itr.next(); + if (val == object || val.equals(object)) { + itr.remove(); + return true; + } + } + + return false; + } + + @Override + public boolean removeIf(final Predicate filter) { + Validate.notNull(filter, "Null filter"); + + return this.map.removeIf((final K key, final V value) -> { + return filter.test(value); + }) != 0; + } + + @Override + public boolean retainAll(final Collection collection) { + Validate.notNull(collection, "Null collection"); + + return this.map.removeIf((final K key, final V value) -> { + return !collection.contains(value); + }) != 0; + } + + @Override + public String toString() { + return CollectionUtil.toString(this, "SWMRHashTableValues"); + } + } +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRLong2ObjectHashTable.java b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRLong2ObjectHashTable.java new file mode 100644 index 0000000000000000000000000000000000000000..1e98f778ffa0a7bb00ebccaaa8bde075183e41f0 --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRLong2ObjectHashTable.java @@ -0,0 +1,672 @@ +package ca.spottedleaf.concurrentutil.map; + +import ca.spottedleaf.concurrentutil.util.ArrayUtil; +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.concurrentutil.util.Validate; +import io.papermc.paper.util.IntegerUtil; +import java.lang.invoke.VarHandle; +import java.util.Arrays; +import java.util.function.Consumer; +import java.util.function.LongConsumer; + +// trimmed down version of SWMRHashTable +public class SWMRLong2ObjectHashTable { + + protected int size; + + protected TableEntry[] table; + + protected final float loadFactor; + + protected static final VarHandle SIZE_HANDLE = ConcurrentUtil.getVarHandle(SWMRLong2ObjectHashTable.class, "size", int.class); + protected static final VarHandle TABLE_HANDLE = ConcurrentUtil.getVarHandle(SWMRLong2ObjectHashTable.class, "table", TableEntry[].class); + + /* size */ + + protected final int getSizePlain() { + return (int)SIZE_HANDLE.get(this); + } + + protected final int getSizeOpaque() { + return (int)SIZE_HANDLE.getOpaque(this); + } + + protected final int getSizeAcquire() { + return (int)SIZE_HANDLE.getAcquire(this); + } + + protected final void setSizePlain(final int value) { + SIZE_HANDLE.set(this, value); + } + + protected final void setSizeOpaque(final int value) { + SIZE_HANDLE.setOpaque(this, value); + } + + protected final void setSizeRelease(final int value) { + SIZE_HANDLE.setRelease(this, value); + } + + /* table */ + + protected final TableEntry[] 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 SWMRLong2ObjectHashTable() { + this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR); + } + + /** + * Constructs this map with the specified capacity and load factor of {@code 0.75f}. + * @param capacity specified initial capacity, > 0 + */ + public SWMRLong2ObjectHashTable(final int capacity) { + this(capacity, DEFAULT_LOAD_FACTOR); + } + + /** + * Constructs this map with the specified capacity and load factor. + * @param capacity specified capacity, > 0 + * @param loadFactor specified load factor, > 0 && finite + */ + public SWMRLong2ObjectHashTable(final int capacity, final float loadFactor) { + final int tableSize = getCapacityFor(capacity); + + if (loadFactor <= 0.0 || !Float.isFinite(loadFactor)) { + throw new IllegalArgumentException("Invalid load factor: " + loadFactor); + } + + //noinspection unchecked + final TableEntry[] table = new TableEntry[tableSize]; + this.setTablePlain(table); + + if (tableSize == MAXIMUM_CAPACITY) { + this.threshold = -1; + } else { + this.threshold = getTargetCapacity(tableSize, loadFactor); + } + + this.loadFactor = loadFactor; + } + + /** + * Constructs this map with a capacity of {@code 16} or the specified map's size, whichever is larger, and + * with a load factor of {@code 0.75f}. + * All of the specified map's entries are copied into this map. + * @param other The specified map. + */ + public SWMRLong2ObjectHashTable(final SWMRLong2ObjectHashTable other) { + this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR, other); + } + + /** + * Constructs this map with a minimum capacity of the specified capacity or the specified map's size, whichever is larger, and + * with a load factor of {@code 0.75f}. + * All of the specified map's entries are copied into this map. + * @param capacity specified capacity, > 0 + * @param other The specified map. + */ + public SWMRLong2ObjectHashTable(final int capacity, final SWMRLong2ObjectHashTable other) { + this(capacity, DEFAULT_LOAD_FACTOR, other); + } + + /** + * Constructs this map with a min capacity of the specified capacity or the specified map's size, whichever is larger, and + * with the specified load factor. + * All of the specified map's entries are copied into this map. + * @param capacity specified capacity, > 0 + * @param loadFactor specified load factor, > 0 && finite + * @param other The specified map. + */ + public SWMRLong2ObjectHashTable(final int capacity, final float loadFactor, final SWMRLong2ObjectHashTable other) { + this(Math.max(Validate.notNull(other, "Null map").size(), capacity), loadFactor); + this.putAll(other); + } + + public final float getLoadFactor() { + return this.loadFactor; + } + + protected static int getCapacityFor(final int capacity) { + if (capacity <= 0) { + throw new IllegalArgumentException("Invalid capacity: " + capacity); + } + if (capacity >= MAXIMUM_CAPACITY) { + return MAXIMUM_CAPACITY; + } + return IntegerUtil.roundCeilLog2(capacity); + } + + /** Callers must still use acquire when reading the value of the entry. */ + protected final TableEntry getEntryForOpaque(final long key) { + final int hash = SWMRLong2ObjectHashTable.getHash(key); + final TableEntry[] table = this.getTableAcquire(); + + for (TableEntry curr = ArrayUtil.getOpaque(table, hash & (table.length - 1)); curr != null; curr = curr.getNextOpaque()) { + if (key == curr.key) { + return curr; + } + } + + return null; + } + + protected final TableEntry getEntryForPlain(final long key) { + final int hash = SWMRLong2ObjectHashTable.getHash(key); + final TableEntry[] table = this.getTablePlain(); + + for (TableEntry curr = table[hash & (table.length - 1)]; curr != null; curr = curr.getNextPlain()) { + if (key == curr.key) { + return curr; + } + } + + return null; + } + + /* MT-Safe */ + + /** must be deterministic given a key */ + protected static int getHash(final long key) { + return (int)it.unimi.dsi.fastutil.HashCommon.mix(key); + } + + // rets -1 if capacity*loadFactor is too large + protected static int getTargetCapacity(final int capacity, final float loadFactor) { + final double ret = (double)capacity * (double)loadFactor; + if (Double.isInfinite(ret) || ret >= ((double)Integer.MAX_VALUE)) { + return -1; + } + + return (int)ret; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + /* Make no attempt to deal with concurrent modifications */ + if (!(obj instanceof SWMRLong2ObjectHashTable)) { + return false; + } + final SWMRLong2ObjectHashTable other = (SWMRLong2ObjectHashTable)obj; + + if (this.size() != other.size()) { + return false; + } + + final TableEntry[] table = this.getTableAcquire(); + + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + final V value = curr.getValueAcquire(); + + final Object otherValue = other.get(curr.key); + if (otherValue == null || (value != otherValue && value.equals(otherValue))) { + return false; + } + } + } + + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + /* Make no attempt to deal with concurrent modifications */ + int hash = 0; + final TableEntry[] table = this.getTableAcquire(); + + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + hash += curr.hashCode(); + } + } + + return hash; + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(64); + builder.append("SingleWriterMultiReaderHashMap:{"); + + this.forEach((final long key, final V value) -> { + builder.append("{key: \"").append(key).append("\", value: \"").append(value).append("\"}"); + }); + + return builder.append('}').toString(); + } + + /** + * {@inheritDoc} + */ + @Override + public SWMRLong2ObjectHashTable clone() { + return new SWMRLong2ObjectHashTable<>(this.getTableAcquire().length, this.loadFactor, this); + } + + /** + * {@inheritDoc} + */ + public void forEach(final Consumer> action) { + Validate.notNull(action, "Null action"); + + final TableEntry[] table = this.getTableAcquire(); + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + action.accept(curr); + } + } + } + + @FunctionalInterface + public static interface BiLongObjectConsumer { + public void accept(final long key, final V value); + } + + /** + * {@inheritDoc} + */ + public void forEach(final BiLongObjectConsumer action) { + Validate.notNull(action, "Null action"); + + final TableEntry[] table = this.getTableAcquire(); + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + final V value = curr.getValueAcquire(); + + action.accept(curr.key, value); + } + } + } + + /** + * Provides the specified consumer with all keys contained within this map. + * @param action The specified consumer. + */ + public void forEachKey(final LongConsumer action) { + Validate.notNull(action, "Null action"); + + final TableEntry[] table = this.getTableAcquire(); + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + action.accept(curr.key); + } + } + } + + /** + * Provides the specified consumer with all values contained within this map. Equivalent to {@code map.values().forEach(Consumer)}. + * @param action The specified consumer. + */ + public void forEachValue(final Consumer action) { + Validate.notNull(action, "Null action"); + + final TableEntry[] table = this.getTableAcquire(); + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + final V value = curr.getValueAcquire(); + + action.accept(value); + } + } + } + + /** + * {@inheritDoc} + */ + public V get(final long key) { + final TableEntry entry = this.getEntryForOpaque(key); + return entry == null ? null : entry.getValueAcquire(); + } + + /** + * {@inheritDoc} + */ + public boolean containsKey(final long key) { + // note: we need to use getValueAcquire, so that the reads from this map are ordered by acquire semantics + return this.get(key) != null; + } + + /** + * {@inheritDoc} + */ + public V getOrDefault(final long key, final V defaultValue) { + final TableEntry entry = this.getEntryForOpaque(key); + + return entry == null ? defaultValue : entry.getValueAcquire(); + } + + /** + * {@inheritDoc} + */ + public int size() { + return this.getSizeAcquire(); + } + + /** + * {@inheritDoc} + */ + public boolean isEmpty() { + return this.getSizeAcquire() == 0; + } + + /* Non-MT-Safe */ + + protected int threshold; + + protected final void checkResize(final int minCapacity) { + if (minCapacity <= this.threshold || this.threshold < 0) { + return; + } + + final TableEntry[] 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 long key = entry.key; + final int hash = SWMRLong2ObjectHashTable.getHash(key); + final int index = hash & indexMask; + + /* we need to create a new entry since there could be reading threads */ + final TableEntry insert = new TableEntry<>(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; + } + + protected final V put(final long key, final V value, final boolean onlyIfAbsent) { + final TableEntry[] table = this.getTablePlain(); + final int hash = SWMRLong2ObjectHashTable.getHash(key); + final int index = hash & (table.length - 1); + + final TableEntry head = table[index]; + if (head == null) { + final TableEntry insert = new TableEntry<>(key, value); + ArrayUtil.setRelease(table, index, insert); + this.addToSize(1); + return null; + } + + for (TableEntry curr = head;;) { + if (key == curr.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<>(key, value); + + curr.setNextRelease(insert); + this.addToSize(1); + return null; + } + } + + /** + * {@inheritDoc} + */ + public V put(final long key, final V value) { + Validate.notNull(value, "Null value"); + + return this.put(key, value, false); + } + + /** + * {@inheritDoc} + */ + public V putIfAbsent(final long key, final V value) { + Validate.notNull(value, "Null value"); + + return this.put(key, value, true); + } + + protected final V remove(final long key, final int hash) { + final TableEntry[] table = this.getTablePlain(); + final int index = (table.length - 1) & hash; + + final TableEntry head = table[index]; + if (head == null) { + return null; + } + + if (head.key == key) { + ArrayUtil.setRelease(table, index, head.getNextPlain()); + this.removeFromSize(1); + + return head.getValuePlain(); + } + + for (TableEntry curr = head.getNextPlain(), prev = head; curr != null; prev = curr, curr = curr.getNextPlain()) { + if (key == curr.key) { + prev.setNextRelease(curr.getNextPlain()); + this.removeFromSize(1); + + return curr.getValuePlain(); + } + } + + return null; + } + + /** + * {@inheritDoc} + */ + public V remove(final long key) { + return this.remove(key, SWMRLong2ObjectHashTable.getHash(key)); + } + + /** + * {@inheritDoc} + */ + public void putAll(final SWMRLong2ObjectHashTable 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. + *

+ */ + public void clear() { + Arrays.fill(this.getTablePlain(), null); + this.setSizeRelease(0); + } + + public static final class TableEntry { + + protected final long key; + protected V value; + + protected TableEntry next; + + protected static final VarHandle VALUE_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "value", Object.class); + protected static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "next", TableEntry.class); + + /* value */ + + protected final V getValuePlain() { + //noinspection unchecked + return (V)VALUE_HANDLE.get(this); + } + + protected final V getValueAcquire() { + //noinspection unchecked + return (V)VALUE_HANDLE.getAcquire(this); + } + + protected final void setValueRelease(final V to) { + VALUE_HANDLE.setRelease(this, to); + } + + /* next */ + + protected final TableEntry getNextPlain() { + //noinspection unchecked + return (TableEntry)NEXT_HANDLE.get(this); + } + + protected final TableEntry getNextOpaque() { + //noinspection unchecked + return (TableEntry)NEXT_HANDLE.getOpaque(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 TableEntry(final long key, final V value) { + this.key = key; + this.value = value; + } + + public long getKey() { + return this.key; + } + + public V getValue() { + return this.getValueAcquire(); + } + + /** + * {@inheritDoc} + */ + public V setValue(final V value) { + if (value == null) { + throw new NullPointerException(); + } + + final V curr = this.getValuePlain(); + + this.setValueRelease(value); + return curr; + } + + protected static int hash(final long key, final Object value) { + return SWMRLong2ObjectHashTable.getHash(key) ^ (value == null ? 0 : value.hashCode()); + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return hash(this.key, this.getValueAcquire()); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + + if (!(obj instanceof TableEntry)) { + return false; + } + final TableEntry other = (TableEntry)obj; + final long otherKey = other.getKey(); + final long thisKey = this.getKey(); + final Object otherValue = other.getValueAcquire(); + final V thisVal = this.getValueAcquire(); + return (thisKey == otherKey) && (thisVal == otherValue || thisVal.equals(otherValue)); + } + } +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/ArrayUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/ArrayUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..ebb1ab06165addb173fea4d295001fe37f4e79d3 --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/util/ArrayUtil.java @@ -0,0 +1,816 @@ +package ca.spottedleaf.concurrentutil.util; + +import java.lang.invoke.VarHandle; + +public final class ArrayUtil { + + public static final VarHandle BOOLEAN_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(boolean[].class); + + public static final VarHandle BYTE_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(byte[].class); + + public static final VarHandle SHORT_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(short[].class); + + public static final VarHandle INT_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(int[].class); + + public static final VarHandle LONG_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(long[].class); + + public static final VarHandle OBJECT_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(Object[].class); + + private ArrayUtil() { + throw new RuntimeException(); + } + + /* byte array */ + + public static byte getPlain(final byte[] array, final int index) { + return (byte)BYTE_ARRAY_HANDLE.get(array, index); + } + + public static byte getOpaque(final byte[] array, final int index) { + return (byte)BYTE_ARRAY_HANDLE.getOpaque(array, index); + } + + public static byte getAcquire(final byte[] array, final int index) { + return (byte)BYTE_ARRAY_HANDLE.getAcquire(array, index); + } + + public static byte getVolatile(final byte[] array, final int index) { + return (byte)BYTE_ARRAY_HANDLE.getVolatile(array, index); + } + + public static void setPlain(final byte[] array, final int index, final byte value) { + BYTE_ARRAY_HANDLE.set(array, index, value); + } + + public static void setOpaque(final byte[] array, final int index, final byte value) { + BYTE_ARRAY_HANDLE.setOpaque(array, index, value); + } + + public static void setRelease(final byte[] array, final int index, final byte value) { + BYTE_ARRAY_HANDLE.setRelease(array, index, value); + } + + public static void setVolatile(final byte[] array, final int index, final byte value) { + BYTE_ARRAY_HANDLE.setVolatile(array, index, value); + } + + public static void setVolatileContended(final byte[] array, final int index, final byte param) { + int failures = 0; + + for (byte curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { + return; + } + } + } + + public static byte compareAndExchangeVolatile(final byte[] array, final int index, final byte expect, final byte update) { + return (byte)BYTE_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); + } + + public static byte getAndAddVolatile(final byte[] array, final int index, final byte param) { + return (byte)BYTE_ARRAY_HANDLE.getAndAdd(array, index, param); + } + + public static byte getAndAndVolatile(final byte[] array, final int index, final byte param) { + return (byte)BYTE_ARRAY_HANDLE.getAndBitwiseAnd(array, index, param); + } + + public static byte getAndOrVolatile(final byte[] array, final int index, final byte param) { + return (byte)BYTE_ARRAY_HANDLE.getAndBitwiseOr(array, index, param); + } + + public static byte getAndXorVolatile(final byte[] array, final int index, final byte param) { + return (byte)BYTE_ARRAY_HANDLE.getAndBitwiseXor(array, index, param); + } + + public static byte getAndSetVolatile(final byte[] array, final int index, final byte param) { + return (byte)BYTE_ARRAY_HANDLE.getAndSet(array, index, param); + } + + public static byte compareAndExchangeVolatileContended(final byte[] array, final int index, final byte expect, final byte update) { + return (byte)BYTE_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); + } + + public static byte getAndAddVolatileContended(final byte[] array, final int index, final byte param) { + int failures = 0; + + for (byte curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (byte) (curr + param)))) { + return curr; + } + } + } + + public static byte getAndAndVolatileContended(final byte[] array, final int index, final byte param) { + int failures = 0; + + for (byte curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (byte) (curr & param)))) { + return curr; + } + } + } + + public static byte getAndOrVolatileContended(final byte[] array, final int index, final byte param) { + int failures = 0; + + for (byte curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (byte) (curr | param)))) { + return curr; + } + } + } + + public static byte getAndXorVolatileContended(final byte[] array, final int index, final byte param) { + int failures = 0; + + for (byte curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (byte) (curr ^ param)))) { + return curr; + } + } + } + + public static byte getAndSetVolatileContended(final byte[] array, final int index, final byte param) { + int failures = 0; + + for (byte curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { + return curr; + } + } + } + + /* short array */ + + public static short getPlain(final short[] array, final int index) { + return (short)SHORT_ARRAY_HANDLE.get(array, index); + } + + public static short getOpaque(final short[] array, final int index) { + return (short)SHORT_ARRAY_HANDLE.getOpaque(array, index); + } + + public static short getAcquire(final short[] array, final int index) { + return (short)SHORT_ARRAY_HANDLE.getAcquire(array, index); + } + + public static short getVolatile(final short[] array, final int index) { + return (short)SHORT_ARRAY_HANDLE.getVolatile(array, index); + } + + public static void setPlain(final short[] array, final int index, final short value) { + SHORT_ARRAY_HANDLE.set(array, index, value); + } + + public static void setOpaque(final short[] array, final int index, final short value) { + SHORT_ARRAY_HANDLE.setOpaque(array, index, value); + } + + public static void setRelease(final short[] array, final int index, final short value) { + SHORT_ARRAY_HANDLE.setRelease(array, index, value); + } + + public static void setVolatile(final short[] array, final int index, final short value) { + SHORT_ARRAY_HANDLE.setVolatile(array, index, value); + } + + public static void setVolatileContended(final short[] array, final int index, final short param) { + int failures = 0; + + for (short curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { + return; + } + } + } + + public static short compareAndExchangeVolatile(final short[] array, final int index, final short expect, final short update) { + return (short)SHORT_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); + } + + public static short getAndAddVolatile(final short[] array, final int index, final short param) { + return (short)SHORT_ARRAY_HANDLE.getAndAdd(array, index, param); + } + + public static short getAndAndVolatile(final short[] array, final int index, final short param) { + return (short)SHORT_ARRAY_HANDLE.getAndBitwiseAnd(array, index, param); + } + + public static short getAndOrVolatile(final short[] array, final int index, final short param) { + return (short)SHORT_ARRAY_HANDLE.getAndBitwiseOr(array, index, param); + } + + public static short getAndXorVolatile(final short[] array, final int index, final short param) { + return (short)SHORT_ARRAY_HANDLE.getAndBitwiseXor(array, index, param); + } + + public static short getAndSetVolatile(final short[] array, final int index, final short param) { + return (short)SHORT_ARRAY_HANDLE.getAndSet(array, index, param); + } + + public static short compareAndExchangeVolatileContended(final short[] array, final int index, final short expect, final short update) { + return (short)SHORT_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); + } + + public static short getAndAddVolatileContended(final short[] array, final int index, final short param) { + int failures = 0; + + for (short curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (short) (curr + param)))) { + return curr; + } + } + } + + public static short getAndAndVolatileContended(final short[] array, final int index, final short param) { + int failures = 0; + + for (short curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (short) (curr & param)))) { + return curr; + } + } + } + + public static short getAndOrVolatileContended(final short[] array, final int index, final short param) { + int failures = 0; + + for (short curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (short) (curr | param)))) { + return curr; + } + } + } + + public static short getAndXorVolatileContended(final short[] array, final int index, final short param) { + int failures = 0; + + for (short curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (short) (curr ^ param)))) { + return curr; + } + } + } + + public static short getAndSetVolatileContended(final short[] array, final int index, final short param) { + int failures = 0; + + for (short curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { + return curr; + } + } + } + + /* int array */ + + public static int getPlain(final int[] array, final int index) { + return (int)INT_ARRAY_HANDLE.get(array, index); + } + + public static int getOpaque(final int[] array, final int index) { + return (int)INT_ARRAY_HANDLE.getOpaque(array, index); + } + + public static int getAcquire(final int[] array, final int index) { + return (int)INT_ARRAY_HANDLE.getAcquire(array, index); + } + + public static int getVolatile(final int[] array, final int index) { + return (int)INT_ARRAY_HANDLE.getVolatile(array, index); + } + + public static void setPlain(final int[] array, final int index, final int value) { + INT_ARRAY_HANDLE.set(array, index, value); + } + + public static void setOpaque(final int[] array, final int index, final int value) { + INT_ARRAY_HANDLE.setOpaque(array, index, value); + } + + public static void setRelease(final int[] array, final int index, final int value) { + INT_ARRAY_HANDLE.setRelease(array, index, value); + } + + public static void setVolatile(final int[] array, final int index, final int value) { + INT_ARRAY_HANDLE.setVolatile(array, index, value); + } + + public static void setVolatileContended(final int[] array, final int index, final int param) { + int failures = 0; + + for (int curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { + return; + } + } + } + + public static int compareAndExchangeVolatile(final int[] array, final int index, final int expect, final int update) { + return (int)INT_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); + } + + public static int getAndAddVolatile(final int[] array, final int index, final int param) { + return (int)INT_ARRAY_HANDLE.getAndAdd(array, index, param); + } + + public static int getAndAndVolatile(final int[] array, final int index, final int param) { + return (int)INT_ARRAY_HANDLE.getAndBitwiseAnd(array, index, param); + } + + public static int getAndOrVolatile(final int[] array, final int index, final int param) { + return (int)INT_ARRAY_HANDLE.getAndBitwiseOr(array, index, param); + } + + public static int getAndXorVolatile(final int[] array, final int index, final int param) { + return (int)INT_ARRAY_HANDLE.getAndBitwiseXor(array, index, param); + } + + public static int getAndSetVolatile(final int[] array, final int index, final int param) { + return (int)INT_ARRAY_HANDLE.getAndSet(array, index, param); + } + + public static int compareAndExchangeVolatileContended(final int[] array, final int index, final int expect, final int update) { + return (int)INT_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); + } + + public static int getAndAddVolatileContended(final int[] array, final int index, final int param) { + int failures = 0; + + for (int curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (int) (curr + param)))) { + return curr; + } + } + } + + public static int getAndAndVolatileContended(final int[] array, final int index, final int param) { + int failures = 0; + + for (int curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (int) (curr & param)))) { + return curr; + } + } + } + + public static int getAndOrVolatileContended(final int[] array, final int index, final int param) { + int failures = 0; + + for (int curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (int) (curr | param)))) { + return curr; + } + } + } + + public static int getAndXorVolatileContended(final int[] array, final int index, final int param) { + int failures = 0; + + for (int curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (int) (curr ^ param)))) { + return curr; + } + } + } + + public static int getAndSetVolatileContended(final int[] array, final int index, final int param) { + int failures = 0; + + for (int curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { + return curr; + } + } + } + + /* long array */ + + public static long getPlain(final long[] array, final int index) { + return (long)LONG_ARRAY_HANDLE.get(array, index); + } + + public static long getOpaque(final long[] array, final int index) { + return (long)LONG_ARRAY_HANDLE.getOpaque(array, index); + } + + public static long getAcquire(final long[] array, final int index) { + return (long)LONG_ARRAY_HANDLE.getAcquire(array, index); + } + + public static long getVolatile(final long[] array, final int index) { + return (long)LONG_ARRAY_HANDLE.getVolatile(array, index); + } + + public static void setPlain(final long[] array, final int index, final long value) { + LONG_ARRAY_HANDLE.set(array, index, value); + } + + public static void setOpaque(final long[] array, final int index, final long value) { + LONG_ARRAY_HANDLE.setOpaque(array, index, value); + } + + public static void setRelease(final long[] array, final int index, final long value) { + LONG_ARRAY_HANDLE.setRelease(array, index, value); + } + + public static void setVolatile(final long[] array, final int index, final long value) { + LONG_ARRAY_HANDLE.setVolatile(array, index, value); + } + + public static void setVolatileContended(final long[] array, final int index, final long param) { + int failures = 0; + + for (long curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { + return; + } + } + } + + public static long compareAndExchangeVolatile(final long[] array, final int index, final long expect, final long update) { + return (long)LONG_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); + } + + public static long getAndAddVolatile(final long[] array, final int index, final long param) { + return (long)LONG_ARRAY_HANDLE.getAndAdd(array, index, param); + } + + public static long getAndAndVolatile(final long[] array, final int index, final long param) { + return (long)LONG_ARRAY_HANDLE.getAndBitwiseAnd(array, index, param); + } + + public static long getAndOrVolatile(final long[] array, final int index, final long param) { + return (long)LONG_ARRAY_HANDLE.getAndBitwiseOr(array, index, param); + } + + public static long getAndXorVolatile(final long[] array, final int index, final long param) { + return (long)LONG_ARRAY_HANDLE.getAndBitwiseXor(array, index, param); + } + + public static long getAndSetVolatile(final long[] array, final int index, final long param) { + return (long)LONG_ARRAY_HANDLE.getAndSet(array, index, param); + } + + public static long compareAndExchangeVolatileContended(final long[] array, final int index, final long expect, final long update) { + return (long)LONG_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); + } + + public static long getAndAddVolatileContended(final long[] array, final int index, final long param) { + int failures = 0; + + for (long curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (long) (curr + param)))) { + return curr; + } + } + } + + public static long getAndAndVolatileContended(final long[] array, final int index, final long param) { + int failures = 0; + + for (long curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (long) (curr & param)))) { + return curr; + } + } + } + + public static long getAndOrVolatileContended(final long[] array, final int index, final long param) { + int failures = 0; + + for (long curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (long) (curr | param)))) { + return curr; + } + } + } + + public static long getAndXorVolatileContended(final long[] array, final int index, final long param) { + int failures = 0; + + for (long curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (long) (curr ^ param)))) { + return curr; + } + } + } + + public static long getAndSetVolatileContended(final long[] array, final int index, final long param) { + int failures = 0; + + for (long curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { + return curr; + } + } + } + + /* boolean array */ + + public static boolean getPlain(final boolean[] array, final int index) { + return (boolean)BOOLEAN_ARRAY_HANDLE.get(array, index); + } + + public static boolean getOpaque(final boolean[] array, final int index) { + return (boolean)BOOLEAN_ARRAY_HANDLE.getOpaque(array, index); + } + + public static boolean getAcquire(final boolean[] array, final int index) { + return (boolean)BOOLEAN_ARRAY_HANDLE.getAcquire(array, index); + } + + public static boolean getVolatile(final boolean[] array, final int index) { + return (boolean)BOOLEAN_ARRAY_HANDLE.getVolatile(array, index); + } + + public static void setPlain(final boolean[] array, final int index, final boolean value) { + BOOLEAN_ARRAY_HANDLE.set(array, index, value); + } + + public static void setOpaque(final boolean[] array, final int index, final boolean value) { + BOOLEAN_ARRAY_HANDLE.setOpaque(array, index, value); + } + + public static void setRelease(final boolean[] array, final int index, final boolean value) { + BOOLEAN_ARRAY_HANDLE.setRelease(array, index, value); + } + + public static void setVolatile(final boolean[] array, final int index, final boolean value) { + BOOLEAN_ARRAY_HANDLE.setVolatile(array, index, value); + } + + public static void setVolatileContended(final boolean[] array, final int index, final boolean param) { + int failures = 0; + + for (boolean curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { + return; + } + } + } + + public static boolean compareAndExchangeVolatile(final boolean[] array, final int index, final boolean expect, final boolean update) { + return (boolean)BOOLEAN_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); + } + + public static boolean getAndOrVolatile(final boolean[] array, final int index, final boolean param) { + return (boolean)BOOLEAN_ARRAY_HANDLE.getAndBitwiseOr(array, index, param); + } + + public static boolean getAndXorVolatile(final boolean[] array, final int index, final boolean param) { + return (boolean)BOOLEAN_ARRAY_HANDLE.getAndBitwiseXor(array, index, param); + } + + public static boolean getAndSetVolatile(final boolean[] array, final int index, final boolean param) { + return (boolean)BOOLEAN_ARRAY_HANDLE.getAndSet(array, index, param); + } + + public static boolean compareAndExchangeVolatileContended(final boolean[] array, final int index, final boolean expect, final boolean update) { + return (boolean)BOOLEAN_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); + } + + public static boolean getAndAndVolatileContended(final boolean[] array, final int index, final boolean param) { + int failures = 0; + + for (boolean curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (boolean) (curr & param)))) { + return curr; + } + } + } + + public static boolean getAndOrVolatileContended(final boolean[] array, final int index, final boolean param) { + int failures = 0; + + for (boolean curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (boolean) (curr | param)))) { + return curr; + } + } + } + + public static boolean getAndXorVolatileContended(final boolean[] array, final int index, final boolean param) { + int failures = 0; + + for (boolean curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (boolean) (curr ^ param)))) { + return curr; + } + } + } + + public static boolean getAndSetVolatileContended(final boolean[] array, final int index, final boolean param) { + int failures = 0; + + for (boolean curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { + return curr; + } + } + } + + @SuppressWarnings("unchecked") + public static T getPlain(final T[] array, final int index) { + final Object ret = OBJECT_ARRAY_HANDLE.get((Object[])array, index); + return (T)ret; + } + + @SuppressWarnings("unchecked") + public static T getOpaque(final T[] array, final int index) { + final Object ret = OBJECT_ARRAY_HANDLE.getOpaque((Object[])array, index); + return (T)ret; + } + + @SuppressWarnings("unchecked") + public static T getAcquire(final T[] array, final int index) { + final Object ret = OBJECT_ARRAY_HANDLE.getAcquire((Object[])array, index); + return (T)ret; + } + + @SuppressWarnings("unchecked") + public static T getVolatile(final T[] array, final int index) { + final Object ret = OBJECT_ARRAY_HANDLE.getVolatile((Object[])array, index); + return (T)ret; + } + + public static void setPlain(final T[] array, final int index, final T value) { + OBJECT_ARRAY_HANDLE.set((Object[])array, index, (Object)value); + } + + public static void setOpaque(final T[] array, final int index, final T value) { + OBJECT_ARRAY_HANDLE.setOpaque((Object[])array, index, (Object)value); + } + + public static void setRelease(final T[] array, final int index, final T value) { + OBJECT_ARRAY_HANDLE.setRelease((Object[])array, index, (Object)value); + } + + public static void setVolatile(final T[] array, final int index, final T value) { + OBJECT_ARRAY_HANDLE.setVolatile((Object[])array, index, (Object)value); + } + + public static void setVolatileContended(final T[] array, final int index, final T param) { + int failures = 0; + + for (T curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { + return; + } + } + } + + @SuppressWarnings("unchecked") + public static T compareAndExchangeVolatile(final T[] array, final int index, final T expect, final T update) { + final Object ret = OBJECT_ARRAY_HANDLE.compareAndExchange((Object[])array, index, (Object)expect, (Object)update); + return (T)ret; + } + + @SuppressWarnings("unchecked") + public static T getAndSetVolatile(final T[] array, final int index, final T param) { + final Object ret = BYTE_ARRAY_HANDLE.getAndSet((Object[])array, index, (Object)param); + return (T)ret; + } + + @SuppressWarnings("unchecked") + public static T compareAndExchangeVolatileContended(final T[] array, final int index, final T expect, final T update) { + final Object ret = OBJECT_ARRAY_HANDLE.compareAndExchange((Object[])array, index, (Object)expect, (Object)update); + return (T)ret; + } + + public static T getAndSetVolatileContended(final T[] array, final int index, final T param) { + int failures = 0; + + for (T curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { + return curr; + } + } + } +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/CollectionUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/CollectionUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..9420b9822de99d3a31224642452835b0c986f7b4 --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/util/CollectionUtil.java @@ -0,0 +1,31 @@ +package ca.spottedleaf.concurrentutil.util; + +import java.util.Collection; + +public final class CollectionUtil { + + public static String toString(final Collection collection, final String name) { + return CollectionUtil.toString(collection, name, new StringBuilder(name.length() + 128)).toString(); + } + + public static StringBuilder toString(final Collection collection, final String name, final StringBuilder builder) { + builder.append(name).append("{elements={"); + + boolean first = true; + + for (final Object element : collection) { + if (!first) { + builder.append(", "); + } + first = false; + + builder.append('"').append(element).append('"'); + } + + return builder.append("}}"); + } + + private CollectionUtil() { + throw new RuntimeException(); + } +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/ConcurrentUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/ConcurrentUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..23ae82e55696a7e2ff0e0f9609c0df6a48bb8d1d --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/util/ConcurrentUtil.java @@ -0,0 +1,166 @@ +package ca.spottedleaf.concurrentutil.util; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.util.concurrent.locks.LockSupport; + +public final class ConcurrentUtil { + + public static String genericToString(final Object object) { + return object == null ? "null" : object.getClass().getName() + ":" + object.hashCode() + ":" + object.toString(); + } + + public static void rethrow(Throwable exception) { + rethrow0(exception); + } + + private static void rethrow0(Throwable thr) throws T { + throw (T)thr; + } + + public static VarHandle getVarHandle(final Class lookIn, final String fieldName, final Class fieldType) { + try { + return MethodHandles.privateLookupIn(lookIn, MethodHandles.lookup()).findVarHandle(lookIn, fieldName, fieldType); + } catch (final Exception ex) { + throw new RuntimeException(ex); // unreachable + } + } + + public static VarHandle getStaticVarHandle(final Class lookIn, final String fieldName, final Class fieldType) { + try { + return MethodHandles.privateLookupIn(lookIn, MethodHandles.lookup()).findStaticVarHandle(lookIn, fieldName, fieldType); + } catch (final Exception ex) { + throw new RuntimeException(ex); // unreachable + } + } + + /** + * Non-exponential backoff algorithm to use in lightly contended areas. + * @see ConcurrentUtil#exponentiallyBackoffSimple(long) + * @see ConcurrentUtil#exponentiallyBackoffComplex(long) + */ + public static void backoff() { + Thread.onSpinWait(); + } + + /** + * Backoff algorithm to use for a short held lock (i.e compareAndExchange operation). Generally this should not be + * used when a thread can block another thread. Instead, use {@link ConcurrentUtil#exponentiallyBackoffComplex(long)}. + * @param counter The current counter. + * @return The counter plus 1. + * @see ConcurrentUtil#backoff() + * @see ConcurrentUtil#exponentiallyBackoffComplex(long) + */ + public static long exponentiallyBackoffSimple(final long counter) { + for (long i = 0; i < counter; ++i) { + backoff(); + } + return counter + 1L; + } + + /** + * Backoff algorithm to use for a lock that can block other threads (i.e if another thread contending with this thread + * can be thrown off the scheduler). This lock should not be used for simple locks such as compareAndExchange. + * @param counter The current counter. + * @return The next (if any) step in the backoff logic. + * @see ConcurrentUtil#backoff() + * @see ConcurrentUtil#exponentiallyBackoffSimple(long) + */ + public static long exponentiallyBackoffComplex(final long counter) { + // TODO experimentally determine counters + if (counter < 100L) { + return exponentiallyBackoffSimple(counter); + } + if (counter < 1_200L) { + Thread.yield(); + LockSupport.parkNanos(1_000L); + return counter + 1L; + } + // scale 0.1ms (100us) per failure + Thread.yield(); + LockSupport.parkNanos(100_000L * counter); + return counter + 1; + } + + /** + * Simple exponential backoff that will linearly increase the time per failure, according to the scale. + * @param counter The current failure counter. + * @param scale Time per failure, in ns. + * @param max The maximum time to wait for, in ns. + * @return The next counter. + */ + public static long linearLongBackoff(long counter, final long scale, long max) { + counter = Math.min(Long.MAX_VALUE, counter + 1); // prevent overflow + max = Math.max(0, max); + + if (scale <= 0L) { + return counter; + } + + long time = scale * counter; + + if (time > max || time / scale != counter) { + time = max; + } + + boolean interrupted = Thread.interrupted(); + if (time > 1_000_000L) { // 1ms + Thread.yield(); + } + LockSupport.parkNanos(time); + if (interrupted) { + Thread.currentThread().interrupt(); + } + return counter; + } + + /** + * Simple exponential backoff that will linearly increase the time per failure, according to the scale. + * @param counter The current failure counter. + * @param scale Time per failure, in ns. + * @param max The maximum time to wait for, in ns. + * @param deadline The deadline in ns. Deadline time source: {@link System#nanoTime()}. + * @return The next counter. + */ + public static long linearLongBackoffDeadline(long counter, final long scale, long max, long deadline) { + counter = Math.min(Long.MAX_VALUE, counter + 1); // prevent overflow + max = Math.max(0, max); + + if (scale <= 0L) { + return counter; + } + + long time = scale * counter; + + // check overflow + if (time / scale != counter) { + // overflew + --counter; + time = max; + } else if (time > max) { + time = max; + } + + final long currTime = System.nanoTime(); + final long diff = deadline - currTime; + if (diff <= 0) { + return counter; + } + if (diff <= 1_500_000L) { // 1.5ms + time = 100_000L; // 100us + } else if (time > 1_000_000L) { // 1ms + Thread.yield(); + } + + boolean interrupted = Thread.interrupted(); + LockSupport.parkNanos(time); + if (interrupted) { + Thread.currentThread().interrupt(); + } + return counter; + } + + public static VarHandle getArrayHandle(final Class type) { + return MethodHandles.arrayElementVarHandle(type); + } +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/Validate.java b/src/main/java/ca/spottedleaf/concurrentutil/util/Validate.java new file mode 100644 index 0000000000000000000000000000000000000000..382177d0d162fa3139c9078a873ce2504a2b17b2 --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/util/Validate.java @@ -0,0 +1,28 @@ +package ca.spottedleaf.concurrentutil.util; + +public final class Validate { + + public static T notNull(final T obj) { + if (obj == null) { + throw new NullPointerException(); + } + return obj; + } + + public static T notNull(final T obj, final String msgIfNull) { + if (obj == null) { + throw new NullPointerException(msgIfNull); + } + return obj; + } + + public static void arrayBounds(final int off, final int len, final int arrayLength, final String msgPrefix) { + if (off < 0 || len < 0 || (arrayLength - off) < len) { + throw new ArrayIndexOutOfBoundsException(msgPrefix + ": off: " + off + ", len: " + len + ", array length: " + arrayLength); + } + } + + private Validate() { + throw new RuntimeException(); + } +}