From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Spottedleaf Date: Sun, 23 Jan 2022 22:58:11 -0800 Subject: [PATCH] ConcurrentUtil diff --git a/src/main/java/ca/spottedleaf/concurrentutil/collection/MultiThreadedQueue.java b/src/main/java/ca/spottedleaf/concurrentutil/collection/MultiThreadedQueue.java new file mode 100644 index 0000000000000000000000000000000000000000..f84a622dc29750139ac280f480b7cd132b036287 --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/collection/MultiThreadedQueue.java @@ -0,0 +1,1421 @@ +package ca.spottedleaf.concurrentutil.collection; + +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.concurrentutil.util.Validate; + +import java.lang.invoke.VarHandle; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Queue; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.function.Consumer; +import java.util.function.IntFunction; +import java.util.function.Predicate; + +/** + * MT-Safe linked first in first out ordered queue. + * + * This queue should out-perform {@link java.util.concurrent.ConcurrentLinkedQueue} in high-contention reads/writes, and is + * not any slower in lower contention reads/writes. + *

+ * Note that this queue breaks the specification laid out by {@link Collection}, see {@link #preventAdds()} and {@link Collection#add(Object)}. + *

+ *

+ * This queue will only unlink linked nodes through the {@link #peek()} and {@link #poll()} methods, and this is only if + * they are at the head of the queue. + *

+ * @param Type of element in this queue. + */ +public class MultiThreadedQueue implements Queue { + + protected volatile LinkedNode head; /* Always non-null, high chance of being the actual head */ + + protected volatile LinkedNode tail; /* Always non-null, high chance of being the actual tail */ + + /* Note that it is possible to reach head from tail. */ + + /* IMPL NOTE: Leave hashCode and equals to their defaults */ + + protected static final VarHandle HEAD_HANDLE = ConcurrentUtil.getVarHandle(MultiThreadedQueue.class, "head", LinkedNode.class); + protected static final VarHandle TAIL_HANDLE = ConcurrentUtil.getVarHandle(MultiThreadedQueue.class, "tail", LinkedNode.class); + + /* head */ + + protected final void setHeadPlain(final LinkedNode newHead) { + HEAD_HANDLE.set(this, newHead); + } + + protected final void setHeadOpaque(final LinkedNode newHead) { + HEAD_HANDLE.setOpaque(this, newHead); + } + + @SuppressWarnings("unchecked") + protected final LinkedNode getHeadPlain() { + return (LinkedNode)HEAD_HANDLE.get(this); + } + + @SuppressWarnings("unchecked") + protected final LinkedNode getHeadOpaque() { + return (LinkedNode)HEAD_HANDLE.getOpaque(this); + } + + @SuppressWarnings("unchecked") + protected final LinkedNode getHeadAcquire() { + return (LinkedNode)HEAD_HANDLE.getAcquire(this); + } + + /* tail */ + + protected final void setTailPlain(final LinkedNode newTail) { + TAIL_HANDLE.set(this, newTail); + } + + protected final void setTailOpaque(final LinkedNode newTail) { + TAIL_HANDLE.setOpaque(this, newTail); + } + + @SuppressWarnings("unchecked") + protected final LinkedNode getTailPlain() { + return (LinkedNode)TAIL_HANDLE.get(this); + } + + @SuppressWarnings("unchecked") + protected final LinkedNode getTailOpaque() { + return (LinkedNode)TAIL_HANDLE.getOpaque(this); + } + + /** + * Constructs a {@code MultiThreadedQueue}, initially empty. + *

+ * The returned object may not be published without synchronization. + *

+ */ + public MultiThreadedQueue() { + final LinkedNode value = new LinkedNode<>(null, null); + this.setHeadPlain(value); + this.setTailPlain(value); + } + + /** + * Constructs a {@code MultiThreadedQueue}, initially containing all elements in the specified {@code collection}. + *

+ * The returned object may not be published without synchronization. + *

+ * @param collection The specified collection. + * @throws NullPointerException If {@code collection} is {@code null} or contains {@code null} elements. + */ + public MultiThreadedQueue(final Iterable 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; + } + } + } + } + + /** + * Returns whether this queue is currently add-blocked. That is, whether {@link #add(Object)} and friends will return {@code false}. + */ + public boolean isAddBlocked() { + for (LinkedNode tail = this.getTailOpaque();;) { + LinkedNode next = tail.getNextVolatile(); + if (next == null) { + return false; + } + + if (next == tail) { + return true; + } + + tail = next; + } + } + + /** + * Atomically removes the head from this queue if it exists, otherwise prevents additions to this queue if no + * head is removed. + *

+ * This function is MT-Safe. + *

+ * If the queue is already add-blocked and empty then no operation is performed. + * @return {@code null} if the queue is now add-blocked or was previously add-blocked, else returns + * an non-null value which was the previous head of queue. + */ + public E pollOrBlockAdds() { + int failures = 0; + for (LinkedNode head = this.getHeadOpaque(), curr = head;;) { + final E currentVal = curr.getElementVolatile(); + final LinkedNode next = curr.getNextOpaque(); + + if (next == curr) { + return null; /* Additions are already blocked */ + } + + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (currentVal != null) { + if (curr.getAndSetElementVolatile(null) == null) { + ++failures; + continue; + } + + /* "CAS" to avoid setting an out-of-date head */ + if (this.getHeadOpaque() == head) { + this.setHeadOpaque(next != null ? next : curr); + } + + return currentVal; + } + + if (next == null) { + /* Try to update stale head */ + if (curr != head && this.getHeadOpaque() == head) { + this.setHeadOpaque(curr); + } + + final LinkedNode compared = curr.compareExchangeNextVolatile(null, curr); + + if (compared != null) { + // failed to block additions + curr = compared; + ++failures; + continue; + } + + return null; /* We blocked additions */ + } + + if (head == curr) { + /* head is likely not up-to-date */ + curr = next; + } else { + /* Try to update to head */ + if (head == (head = this.getHeadOpaque())) { + curr = next; + } else { + curr = head; + } + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public boolean remove(final Object object) { + Validate.notNull(object, "Null object to remove"); + + for (LinkedNode curr = this.getHeadOpaque();;) { + final LinkedNode next = curr.getNextVolatile(); + final E element = curr.getElementPlain(); /* Likely in sync */ + + if (element != null) { + if ((element == object || element.equals(object)) && curr.getAndSetElementVolatile(null) == element) { + return true; + } + } + + if (next == curr || next == null) { + break; + } + curr = next; + } + + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean removeIf(final Predicate 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..094eff418b4e3bffce020d650931b4d9e58fa9ed --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/collection/SRSWLinkedQueue.java @@ -0,0 +1,149 @@ +package ca.spottedleaf.concurrentutil.collection; + +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.concurrentutil.util.Validate; + +import java.lang.invoke.VarHandle; +import java.util.ConcurrentModificationException; + +/** + * Single reader thread single writer thread queue. The reader side of the queue is ordered by acquire semantics, + * and the writer side of the queue is ordered by release semantics. + */ +// TODO test +public class SRSWLinkedQueue { + + // always non-null + protected LinkedNode head; + + // always non-null + protected LinkedNode tail; + + /* IMPL NOTE: Leave hashCode and equals to their defaults */ + + public SRSWLinkedQueue() { + final LinkedNode dummy = new LinkedNode<>(null, null); + this.head = this.tail = dummy; + } + + /** + * Must be the reader thread. + * + *

+ * Returns, without removing, the first element of this queue. + *

+ * @return Returns, without removing, the first element of this queue. + */ + public E peekFirst() { + LinkedNode head = this.head; + E ret = head.getElementPlain(); + if (ret == null) { + head = head.getNextAcquire(); + if (head == null) { + // empty + return null; + } + // update head reference for next poll() call + this.head = head; + // guaranteed to be non-null + ret = head.getElementPlain(); + if (ret == null) { + throw new ConcurrentModificationException("Multiple reader threads"); + } + } + + return ret; + } + + /** + * Must be the reader thread. + * + *

+ * Returns and removes the first element of this queue. + *

+ * @return Returns and removes the first element of this queue. + */ + public E poll() { + LinkedNode head = this.head; + E ret = head.getElementPlain(); + if (ret == null) { + head = head.getNextAcquire(); + if (head == null) { + // empty + return null; + } + // guaranteed to be non-null + ret = head.getElementPlain(); + if (ret == null) { + throw new ConcurrentModificationException("Multiple reader threads"); + } + } + + head.setElementPlain(null); + LinkedNode next = head.getNextAcquire(); + this.head = next == null ? head : next; + + return ret; + } + + /** + * Must be the writer thread. + * + *

+ * Adds the element to the end of the queue. + *

+ * + * @throws NullPointerException If the provided element is null + */ + public void addLast(final E element) { + Validate.notNull(element, "Provided element cannot be null"); + final LinkedNode append = new LinkedNode<>(element, null); + + this.tail.setNextRelease(append); + this.tail = append; + } + + protected static final class LinkedNode { + + protected volatile Object element; + protected volatile LinkedNode next; + + protected static final VarHandle ELEMENT_HANDLE = ConcurrentUtil.getVarHandle(LinkedNode.class, "element", Object.class); + protected static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(LinkedNode.class, "next", LinkedNode.class); + + protected LinkedNode(final Object element, final LinkedNode next) { + ELEMENT_HANDLE.set(this, element); + NEXT_HANDLE.set(this, next); + } + + /* element */ + + @SuppressWarnings("unchecked") + protected final E getElementPlain() { + return (E)ELEMENT_HANDLE.get(this); + } + + protected final void setElementPlain(final E update) { + ELEMENT_HANDLE.set(this, (Object)update); + } + /* next */ + + @SuppressWarnings("unchecked") + protected final LinkedNode getNextPlain() { + return (LinkedNode)NEXT_HANDLE.get(this); + } + + @SuppressWarnings("unchecked") + protected final LinkedNode getNextAcquire() { + return (LinkedNode)NEXT_HANDLE.getAcquire(this); + } + + protected final void setNextPlain(final LinkedNode next) { + NEXT_HANDLE.set(this, next); + } + + protected final void setNextRelease(final LinkedNode next) { + NEXT_HANDLE.setRelease(this, next); + } + } +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/completable/Completable.java b/src/main/java/ca/spottedleaf/concurrentutil/completable/Completable.java new file mode 100644 index 0000000000000000000000000000000000000000..46d1bd01542ebeeffc0006a5c585a50dbbbff907 --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/completable/Completable.java @@ -0,0 +1,112 @@ +package ca.spottedleaf.concurrentutil.completable; + +import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; +import ca.spottedleaf.concurrentutil.executor.Cancellable; +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.function.BiConsumer; + +public final class Completable { + + private static final Logger LOGGER = LoggerFactory.getLogger(Completable.class); + + private final MultiThreadedQueue> waiters = new MultiThreadedQueue<>(); + private T result; + private Throwable throwable; + private volatile boolean completed; + + public boolean isCompleted() { + return this.completed; + } + + /** + * Note: Can only use after calling {@link #addAsynchronousWaiter(BiConsumer)}, as this function performs zero + * synchronisation + */ + public T getResult() { + return this.result; + } + + /** + * Note: Can only use after calling {@link #addAsynchronousWaiter(BiConsumer)}, as this function performs zero + * synchronisation + */ + public Throwable getThrowable() { + return this.throwable; + } + + /** + * Adds a waiter that should only be completed asynchronously by the complete() calls. If complete() + * has already been called, returns {@code null} and does not invoke the specified consumer. + * @param consumer Consumer to be executed on completion + * @throws NullPointerException If consumer is null + * @return A cancellable which will control the execution of the specified consumer + */ + public Cancellable addAsynchronousWaiter(final BiConsumer consumer) { + if (this.waiters.add(consumer)) { + return new CancellableImpl(consumer); + } + return null; + } + + private void completeAllWaiters(final T result, final Throwable throwable) { + this.completed = true; + BiConsumer waiter; + while ((waiter = this.waiters.pollOrBlockAdds()) != null) { + this.completeWaiter(waiter, result, throwable); + } + } + + private void completeWaiter(final BiConsumer consumer, final T result, final Throwable throwable) { + try { + consumer.accept(result, throwable); + } catch (final ThreadDeath death) { + throw death; + } catch (final Throwable throwable2) { + LOGGER.error("Failed to complete callback " + ConcurrentUtil.genericToString(consumer), throwable2); + } + } + + /** + * Adds a waiter that will be completed asynchronously by the complete() calls. If complete() + * has already been called, then invokes the consumer synchronously with the completed result. + * @param consumer Consumer to be executed on completion + * @throws NullPointerException If consumer is null + * @return A cancellable which will control the execution of the specified consumer + */ + public Cancellable addWaiter(final BiConsumer consumer) { + if (this.waiters.add(consumer)) { + return new CancellableImpl(consumer); + } + this.completeWaiter(consumer, this.result, this.throwable); + return new CancellableImpl(consumer); + } + + public void complete(final T result) { + this.result = result; + this.completeAllWaiters(result, null); + } + + public void completeWithThrowable(final Throwable throwable) { + if (throwable == null) { + throw new NullPointerException("Throwable cannot be null"); + } + this.throwable = throwable; + this.completeAllWaiters(null, throwable); + } + + private final class CancellableImpl implements Cancellable { + + private final BiConsumer waiter; + + private CancellableImpl(final BiConsumer waiter) { + this.waiter = waiter; + } + + @Override + public boolean cancel() { + return Completable.this.waiters.remove(this.waiter); + } + } +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/BaseExecutor.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/BaseExecutor.java new file mode 100644 index 0000000000000000000000000000000000000000..18d646676fd022afd64afaac30ec1bd283a73b0e --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/BaseExecutor.java @@ -0,0 +1,208 @@ +package ca.spottedleaf.concurrentutil.executor; + +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import java.util.function.BooleanSupplier; + +/** + * Base implementation for an abstract queue of tasks which are executed either synchronously or asynchronously. + * + *

+ * The implementation supports tracking task executions using {@link #getTotalTasksScheduled()} and + * {@link #getTotalTasksExecuted()}, and optionally shutting down the executor using {@link #shutdown()} + *

+ * + *

+ * The base implementation does not provide a method to queue a task for execution, rather that is specified in + * the specific implementation. However, it is required that a specific implementation provides a method to + * queue a task or create a task. A queued task is one which will eventually be executed, + * and a created task must be queued to execute via {@link BaseTask#queue()} or be executed manually via + * {@link BaseTask#execute()}. This choice of delaying the queueing of a task may be useful to provide a task handle + * which may be cancelled or adjusted before the actual real task logic is ready to be executed. + *

+ */ +public interface BaseExecutor { + + /** + * Returns whether every task scheduled to this queue has been removed and executed or cancelled. If no tasks have been queued, + * returns {@code true}. + * + * @return {@code true} if all tasks that have been queued have finished executing or no tasks have been queued, {@code false} otherwise. + */ + public default boolean haveAllTasksExecuted() { + // order is important + // if new tasks are scheduled between the reading of these variables, scheduled is guaranteed to be higher - + // so our check fails, and we try again + final long completed = this.getTotalTasksExecuted(); + final long scheduled = this.getTotalTasksScheduled(); + + return completed == scheduled; + } + + /** + * Returns the number of tasks that have been scheduled or execute or are pending to be scheduled. + */ + public long getTotalTasksScheduled(); + + /** + * Returns the number of tasks that have fully been executed. + */ + public long getTotalTasksExecuted(); + + /** + * Waits until this queue has had all of its tasks executed (NOT removed). See {@link #haveAllTasksExecuted()} + *

+ * This call is most effective after a {@link #shutdown()} call, as the shutdown call guarantees no tasks can + * be executed and the waitUntilAllExecuted call makes sure the queue is empty. Effectively, using shutdown then using + * waitUntilAllExecuted ensures this queue is empty - and most importantly, will remain empty. + *

+ *

+ * This method is not guaranteed to be immediately responsive to queue state, so calls may take significantly more + * time than expected. Effectively, do not rely on this call being fast - even if there are few tasks scheduled. + *

+ *

+ * Note: Interruptions to the the current thread have no effect. Interrupt status is also not affected by this call. + *

+ * + * @throws IllegalStateException If the current thread is not allowed to wait + */ + public default void waitUntilAllExecuted() throws IllegalStateException { + long failures = 1L; // start at 0.25ms + + while (!this.haveAllTasksExecuted()) { + Thread.yield(); + failures = ConcurrentUtil.linearLongBackoff(failures, 250_000L, 5_000_000L); // 500us, 5ms + } + } + + /** + * Executes the next available task. + * + * @return {@code true} if a task was executed, {@code false} otherwise + * @throws IllegalStateException If the current thread is not allowed to execute a task + */ + public boolean executeTask() throws IllegalStateException; + + /** + * Executes all queued tasks. + * + * @return {@code true} if a task was executed, {@code false} otherwise + * @throws IllegalStateException If the current thread is not allowed to execute a task + */ + public default boolean executeAll() { + if (!this.executeTask()) { + return false; + } + + while (this.executeTask()); + + return true; + } + + /** + * Waits and executes tasks until the condition returns {@code true}. + *

+ * WARNING: This function is not suitable for waiting until a deadline! + * Use {@link #executeUntil(long)} or {@link #executeConditionally(BooleanSupplier, long)} instead. + *

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

+ * This operation is atomic with respect to other shutdown calls + *

+ *

+ * After this call has completed, regardless of return value, this queue will be shutdown. + *

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

+ * Exceptions thrown from the runnable will be rethrown. + *

+ * + * @return {@code true} if this task was executed, {@code false} if it was already marked as completed. + */ + public boolean execute(); + } +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/Cancellable.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/Cancellable.java new file mode 100644 index 0000000000000000000000000000000000000000..11449056361bb6c5a055f543cdd135c4113757c6 --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/Cancellable.java @@ -0,0 +1,14 @@ +package ca.spottedleaf.concurrentutil.executor; + +/** + * Interface specifying that something can be cancelled. + */ +public interface Cancellable { + + /** + * Tries to cancel this task. If the task is in a stage that is too late to be cancelled, then this function + * will return {@code false}. If the task is already cancelled, then this function returns {@code false}. Only + * when this function successfully stops this task from being completed will it return {@code true}. + */ + public boolean cancel(); +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/DelayedPrioritisedTask.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/DelayedPrioritisedTask.java new file mode 100644 index 0000000000000000000000000000000000000000..3ce10053d4ec51855ad7012abb5d97df1c0e557a --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/DelayedPrioritisedTask.java @@ -0,0 +1,170 @@ +package ca.spottedleaf.concurrentutil.executor.standard; + +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import java.lang.invoke.VarHandle; + +public class DelayedPrioritisedTask { + + protected volatile int priority; + protected static final VarHandle PRIORITY_HANDLE = ConcurrentUtil.getVarHandle(DelayedPrioritisedTask.class, "priority", int.class); + + protected static final int PRIORITY_SET = Integer.MIN_VALUE >>> 0; + + protected final int getPriorityVolatile() { + return (int)PRIORITY_HANDLE.getVolatile((DelayedPrioritisedTask)this); + } + + protected final int compareAndExchangePriorityVolatile(final int expect, final int update) { + return (int)PRIORITY_HANDLE.compareAndExchange((DelayedPrioritisedTask)this, (int)expect, (int)update); + } + + protected final int getAndOrPriorityVolatile(final int val) { + return (int)PRIORITY_HANDLE.getAndBitwiseOr((DelayedPrioritisedTask)this, (int)val); + } + + protected final void setPriorityPlain(final int val) { + PRIORITY_HANDLE.set((DelayedPrioritisedTask)this, (int)val); + } + + protected volatile PrioritisedExecutor.PrioritisedTask task; + protected static final VarHandle TASK_HANDLE = ConcurrentUtil.getVarHandle(DelayedPrioritisedTask.class, "task", PrioritisedExecutor.PrioritisedTask.class); + + protected PrioritisedExecutor.PrioritisedTask getTaskPlain() { + return (PrioritisedExecutor.PrioritisedTask)TASK_HANDLE.get((DelayedPrioritisedTask)this); + } + + protected PrioritisedExecutor.PrioritisedTask getTaskVolatile() { + return (PrioritisedExecutor.PrioritisedTask)TASK_HANDLE.getVolatile((DelayedPrioritisedTask)this); + } + + protected final PrioritisedExecutor.PrioritisedTask compareAndExchangeTaskVolatile(final PrioritisedExecutor.PrioritisedTask expect, final PrioritisedExecutor.PrioritisedTask update) { + return (PrioritisedExecutor.PrioritisedTask)TASK_HANDLE.compareAndExchange((DelayedPrioritisedTask)this, (PrioritisedExecutor.PrioritisedTask)expect, (PrioritisedExecutor.PrioritisedTask)update); + } + + public DelayedPrioritisedTask(final PrioritisedExecutor.Priority priority) { + this.setPriorityPlain(priority.priority); + } + + // only public for debugging + public int getPriorityInternal() { + return this.getPriorityVolatile(); + } + + public PrioritisedExecutor.PrioritisedTask getTask() { + return this.getTaskVolatile(); + } + + public void setTask(final PrioritisedExecutor.PrioritisedTask task) { + int priority = this.getPriorityVolatile(); + + if (this.compareAndExchangeTaskVolatile(null, task) != null) { + throw new IllegalStateException("setTask() called twice"); + } + + int failures = 0; + for (;;) { + task.setPriority(PrioritisedExecutor.Priority.getPriority(priority)); + + if (priority == (priority = this.compareAndExchangePriorityVolatile(priority, priority | PRIORITY_SET))) { + return; + } + + ++failures; + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + } + } + + public PrioritisedExecutor.Priority getPriority() { + final int priority = this.getPriorityVolatile(); + if ((priority & PRIORITY_SET) != 0) { + return this.task.getPriority(); + } + + return PrioritisedExecutor.Priority.getPriority(priority); + } + + public void raisePriority(final PrioritisedExecutor.Priority priority) { + if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + + int failures = 0; + for (int curr = this.getPriorityVolatile();;) { + if ((curr & PRIORITY_SET) != 0) { + this.getTaskPlain().raisePriority(priority); + return; + } + + if (!priority.isLowerPriority(curr)) { + return; + } + + if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) { + return; + } + + // failed, retry + + ++failures; + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + } + } + + public void setPriority(final PrioritisedExecutor.Priority priority) { + if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + + int failures = 0; + for (int curr = this.getPriorityVolatile();;) { + if ((curr & PRIORITY_SET) != 0) { + this.getTaskPlain().setPriority(priority); + return; + } + + if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) { + return; + } + + // failed, retry + + ++failures; + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + } + } + + public void lowerPriority(final PrioritisedExecutor.Priority priority) { + if (!PrioritisedExecutor.Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + + int failures = 0; + for (int curr = this.getPriorityVolatile();;) { + if ((curr & PRIORITY_SET) != 0) { + this.getTaskPlain().lowerPriority(priority); + return; + } + + if (!priority.isHigherPriority(curr)) { + return; + } + + if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) { + return; + } + + // failed, retry + + ++failures; + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + } + } +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedExecutor.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedExecutor.java new file mode 100644 index 0000000000000000000000000000000000000000..91beb6f23f257cf265fe3150f760892e605f217a --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedExecutor.java @@ -0,0 +1,276 @@ +package ca.spottedleaf.concurrentutil.executor.standard; + +import ca.spottedleaf.concurrentutil.executor.BaseExecutor; + +/** + * Implementation of {@link BaseExecutor} which schedules tasks to be executed by a given priority. + * @see BaseExecutor + */ +public interface PrioritisedExecutor extends BaseExecutor { + + public static enum Priority { + + /** + * Priority value indicating the task has completed or is being completed. + * This priority cannot be used to schedule tasks. + */ + COMPLETING(-1), + + /** + * Absolute highest priority, should only be used for when a task is blocking a time-critical thread. + */ + BLOCKING(), + + /** + * Should only be used for urgent but not time-critical tasks. + */ + HIGHEST(), + + /** + * Two priorities above normal. + */ + HIGHER(), + + /** + * One priority above normal. + */ + HIGH(), + + /** + * Default priority. + */ + NORMAL(), + + /** + * One priority below normal. + */ + LOW(), + + /** + * Two priorities below normal. + */ + LOWER(), + + /** + * Use for tasks that should eventually execute, but are not needed to. + */ + LOWEST(), + + /** + * Use for tasks that can be delayed indefinitely. + */ + IDLE(); + + // returns whether the priority can be scheduled + public static boolean isValidPriority(final Priority priority) { + return priority != null && priority != Priority.COMPLETING; + } + + // returns the higher priority of the two + public static Priority max(final Priority p1, final Priority p2) { + return p1.isHigherOrEqualPriority(p2) ? p1 : p2; + } + + // returns the lower priroity of the two + public static Priority min(final Priority p1, final Priority p2) { + return p1.isLowerOrEqualPriority(p2) ? p1 : p2; + } + + public boolean isHigherOrEqualPriority(final Priority than) { + return this.priority <= than.priority; + } + + public boolean isHigherPriority(final Priority than) { + return this.priority < than.priority; + } + + public boolean isLowerOrEqualPriority(final Priority than) { + return this.priority >= than.priority; + } + + public boolean isLowerPriority(final Priority than) { + return this.priority > than.priority; + } + + public boolean isHigherOrEqualPriority(final int than) { + return this.priority <= than; + } + + public boolean isHigherPriority(final int than) { + return this.priority < than; + } + + public boolean isLowerOrEqualPriority(final int than) { + return this.priority >= than; + } + + public boolean isLowerPriority(final int than) { + return this.priority > than; + } + + public static boolean isHigherOrEqualPriority(final int priority, final int than) { + return priority <= than; + } + + public static boolean isHigherPriority(final int priority, final int than) { + return priority < than; + } + + public static boolean isLowerOrEqualPriority(final int priority, final int than) { + return priority >= than; + } + + public static boolean isLowerPriority(final int priority, final int than) { + return priority > than; + } + + static final Priority[] PRIORITIES = Priority.values(); + + /** includes special priorities */ + public static final int TOTAL_PRIORITIES = PRIORITIES.length; + + public static final int TOTAL_SCHEDULABLE_PRIORITIES = TOTAL_PRIORITIES - 1; + + public static Priority getPriority(final int priority) { + return PRIORITIES[priority + 1]; + } + + private static int priorityCounter; + + private static int nextCounter() { + return priorityCounter++; + } + + public final int priority; + + Priority() { + this(nextCounter()); + } + + Priority(final int priority) { + this.priority = priority; + } + } + + /** + * Executes the next available task. + *

+ * If there is a task with priority {@link PrioritisedExecutor.Priority#BLOCKING} available, then that such task is executed. + *

+ *

+ * If there is a task with priority {@link PrioritisedExecutor.Priority#IDLE} available then that task is only executed + * when there are no other tasks available with a higher priority. + *

+ *

+ * If there are no tasks that have priority {@link PrioritisedExecutor.Priority#BLOCKING} or {@link PrioritisedExecutor.Priority#IDLE}, then + * this function will be biased to execute tasks that have higher priorities. + *

+ * + * @return {@code true} if a task was executed, {@code false} otherwise + * @throws IllegalStateException If the current thread is not allowed to execute a task + */ + @Override + public boolean executeTask() throws IllegalStateException; + + /** + * Queues or executes a task at {@link Priority#NORMAL} priority. + * @param task The task to run. + * + * @throws IllegalStateException If this queue has shutdown. + * @throws NullPointerException If the task is null + * @return {@code null} if the current thread immediately executed the task, else returns the prioritised task + * associated with the parameter + */ + public default PrioritisedTask queueRunnable(final Runnable task) { + return this.queueRunnable(task, Priority.NORMAL); + } + + /** + * Queues or executes a task. + * + * @param task The task to run. + * @param priority The priority for the task. + * + * @throws IllegalStateException If this queue has shutdown. + * @throws NullPointerException If the task is null + * @throws IllegalArgumentException If the priority is invalid. + * @return {@code null} if the current thread immediately executed the task, else returns the prioritised task + * associated with the parameter + */ + public PrioritisedTask queueRunnable(final Runnable task, final Priority priority); + + /** + * Creates, but does not execute or queue the task. The task must later be queued via {@link BaseTask#queue()}. + * + * @param task The task to run. + * + * @throws IllegalStateException If this queue has shutdown. + * @throws NullPointerException If the task is null + * @throws IllegalArgumentException If the priority is invalid. + * @throws UnsupportedOperationException If this executor does not support lazily queueing tasks + * @return The prioritised task associated with the parameters + */ + public default PrioritisedTask createTask(final Runnable task) { + return this.createTask(task, Priority.NORMAL); + } + + /** + * Creates, but does not execute or queue the task. The task must later be queued via {@link BaseTask#queue()}. + * + * @param task The task to run. + * @param priority The priority for the task. + * + * @throws IllegalStateException If this queue has shutdown. + * @throws NullPointerException If the task is null + * @throws IllegalArgumentException If the priority is invalid. + * @throws UnsupportedOperationException If this executor does not support lazily queueing tasks + * @return The prioritised task associated with the parameters + */ + public PrioritisedTask createTask(final Runnable task, final Priority priority); + + /** + * Extension of {@link ca.spottedleaf.concurrentutil.executor.BaseExecutor.BaseTask} which adds functions + * to retrieve and modify the task's associated priority. + * + * @see ca.spottedleaf.concurrentutil.executor.BaseExecutor.BaseTask + */ + public static interface PrioritisedTask extends BaseTask { + + /** + * Returns the current priority. Note that {@link Priority#COMPLETING} will be returned + * if this task is completing or has completed. + */ + public Priority getPriority(); + + /** + * Attempts to set this task's priority level to the level specified. + * + * @param priority Specified priority level. + * + * @throws IllegalArgumentException If the priority is invalid + * @return {@code true} if successful, {@code false} if this task is completing or has completed or the queue + * this task was scheduled on was shutdown, or if the priority was already at the specified level. + */ + public boolean setPriority(final Priority priority); + + /** + * Attempts to raise the priority to the priority level specified. + * + * @param priority Priority specified + * + * @throws IllegalArgumentException If the priority is invalid + * @return {@code false} if the current task is completing, {@code true} if the priority was raised to the specified level or was already at the specified level or higher. + */ + public boolean raisePriority(final Priority priority); + + /** + * Attempts to lower the priority to the priority level specified. + * + * @param priority Priority specified + * + * @throws IllegalArgumentException If the priority is invalid + * @return {@code false} if the current task is completing, {@code true} if the priority was lowered to the specified level or was already at the specified level or lower. + */ + public boolean lowerPriority(final Priority priority); + } +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedQueueExecutorThread.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedQueueExecutorThread.java new file mode 100644 index 0000000000000000000000000000000000000000..d1683ba6350e530373944f98192c0f2baf241e70 --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedQueueExecutorThread.java @@ -0,0 +1,301 @@ +package ca.spottedleaf.concurrentutil.executor.standard; + +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.lang.invoke.VarHandle; +import java.util.concurrent.locks.LockSupport; + +/** + * Thread which will continuously drain from a specified queue. + *

+ * Note: When using this thread, queue additions to the underlying {@link #queue} are not sufficient to get this thread + * to execute the task. The function {@link #notifyTasks()} must be used after scheduling a task. For expected behaviour + * of task scheduling (thread wakes up after tasks are scheduled), use the methods provided on {@link PrioritisedExecutor} + * methods. + *

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

+ * This function is MT-Safe. + *

+ * @param wait If this call is to wait until the queue is empty and there are no tasks executing in the queue. + * @param killQueue Whether to shutdown this thread's queue + * @return whether this thread shut down the queue + * @see #halt(boolean) + */ + public boolean close(final boolean wait, final boolean killQueue) { + final boolean ret = killQueue && this.queue.shutdown(); + this.threadShutdown = true; + + // force thread to respond to the shutdown + this.setThreadParkedVolatile(false); + LockSupport.unpark(this); + + if (wait) { + this.waitUntilAllExecuted(); + } + + return ret; + } + + + /** + * Causes this thread to exit without draining the queue. To ensure tasks are completed, use {@link #close(boolean, boolean)}. + *

+ * This is not safe to call with {@link #close(boolean, boolean)} if wait = true, in which case + * the waiting thread may block indefinitely. + *

+ *

+ * This function is MT-Safe. + *

+ * @param killQueue Whether to shutdown this thread's queue + * @see #close(boolean, boolean) + */ + public void halt(final boolean killQueue) { + if (killQueue) { + this.queue.shutdown(); + } + this.threadShutdown = true; + this.halted = true; + + // force thread to respond to the shutdown + this.setThreadParkedVolatile(false); + LockSupport.unpark(this); + } + + protected final boolean getThreadParkedVolatile() { + return (boolean)THREAD_PARKED_HANDLE.getVolatile(this); + } + + protected final boolean exchangeThreadParkedVolatile(final boolean value) { + return (boolean)THREAD_PARKED_HANDLE.getAndSet(this, value); + } + + protected final void setThreadParkedVolatile(final boolean value) { + THREAD_PARKED_HANDLE.setVolatile(this, value); + } +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadPool.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadPool.java new file mode 100644 index 0000000000000000000000000000000000000000..2ba36e29d0d8693f2f5e6c6d195ca27f2a5099aa --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadPool.java @@ -0,0 +1,632 @@ +package ca.spottedleaf.concurrentutil.executor.standard; + +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.TreeSet; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiConsumer; + +public final class PrioritisedThreadPool { + + private static final Logger LOGGER = LoggerFactory.getLogger(PrioritisedThreadPool.class); + + private final PrioritisedThread[] threads; + private final TreeSet queues = new TreeSet<>(PrioritisedPoolExecutorImpl.comparator()); + private final String name; + private final long queueMaxHoldTime; + + private final ReferenceOpenHashSet nonShutdownQueues = new ReferenceOpenHashSet<>(); + private final ReferenceOpenHashSet activeQueues = new ReferenceOpenHashSet<>(); + + private boolean shutdown; + + private long schedulingIdGenerator; + + private static final long DEFAULT_QUEUE_HOLD_TIME = (long)(5.0e6); + + /** + * @param name Specified debug name of this thread pool + * @param threads The number of threads to use + */ + public PrioritisedThreadPool(final String name, final int threads) { + this(name, threads, null); + } + + /** + * @param name Specified debug name of this thread pool + * @param threads The number of threads to use + * @param threadModifier Invoked for each created thread with its incremental id before starting them + */ + public PrioritisedThreadPool(final String name, final int threads, final BiConsumer threadModifier) { + this(name, threads, threadModifier, DEFAULT_QUEUE_HOLD_TIME); // 5ms + } + + /** + * @param name Specified debug name of this thread pool + * @param threads The number of threads to use + * @param threadModifier Invoked for each created thread with its incremental id before starting them + * @param queueHoldTime The maximum amount of time to spend executing tasks in a specific queue before attempting + * to switch to another queue, per thread + */ + public PrioritisedThreadPool(final String name, final int threads, final BiConsumer threadModifier, + final long queueHoldTime) { // in ns + if (threads <= 0) { + throw new IllegalArgumentException("Thread count must be > 0, not " + threads); + } + if (name == null) { + throw new IllegalArgumentException("Name cannot be null"); + } + this.name = name; + this.queueMaxHoldTime = queueHoldTime; + + this.threads = new PrioritisedThread[threads]; + for (int i = 0; i < threads; ++i) { + this.threads[i] = new PrioritisedThread(this); + + // set default attributes + this.threads[i].setName("Prioritised thread for pool '" + name + "' #" + i); + this.threads[i].setUncaughtExceptionHandler((final Thread thread, final Throwable throwable) -> { + LOGGER.error("Uncaught exception in thread " + thread.getName(), throwable); + }); + + // let thread modifier override defaults + if (threadModifier != null) { + threadModifier.accept(this.threads[i], Integer.valueOf(i)); + } + + // now the thread can start + this.threads[i].start(); + } + } + + /** + * Returns an array representing the threads backing this thread pool. + */ + public Thread[] getThreads() { + return Arrays.copyOf(this.threads, this.threads.length, Thread[].class); + } + + /** + * Creates and returns a {@link PrioritisedPoolExecutor} to schedule tasks onto. The returned executor will execute + * tasks on this thread pool only. + * @param name The debug name of the executor. + * @param minParallelism The minimum number of threads to be executing tasks from the returned executor + * before threads may be allocated to other queues in this thread pool. + * @param parallelism The maximum number of threads which may be executing tasks from the returned executor. + * @throws IllegalStateException If this thread pool is shut down + */ + public PrioritisedPoolExecutor createExecutor(final String name, final int minParallelism, final int parallelism) { + synchronized (this.nonShutdownQueues) { + if (this.shutdown) { + throw new IllegalStateException("Queue is shutdown: " + this.toString()); + } + final PrioritisedPoolExecutorImpl ret = new PrioritisedPoolExecutorImpl( + this, name, + Math.min(Math.max(1, parallelism), this.threads.length), + Math.min(Math.max(0, minParallelism), this.threads.length) + ); + + this.nonShutdownQueues.add(ret); + + synchronized (this.activeQueues) { + this.activeQueues.add(ret); + } + + return ret; + } + } + + /** + * Prevents creation of new queues, shutdowns all non-shutdown queues if specified + */ + public void halt(final boolean shutdownQueues) { + synchronized (this.nonShutdownQueues) { + this.shutdown = true; + } + if (shutdownQueues) { + final ArrayList queuesToShutdown; + synchronized (this.nonShutdownQueues) { + this.shutdown = true; + queuesToShutdown = new ArrayList<>(this.nonShutdownQueues); + } + + for (final PrioritisedPoolExecutorImpl queue : queuesToShutdown) { + queue.shutdown(); + } + } + + + for (final PrioritisedThread thread : this.threads) { + // can't kill queue, queue is null + thread.halt(false); + } + } + + /** + * Waits until all threads in this pool have shutdown, or until the specified time has passed. + * @param msToWait Maximum time to wait. + * @return {@code false} if the maximum time passed, {@code true} otherwise. + */ + public boolean join(final long msToWait) { + try { + return this.join(msToWait, false); + } catch (final InterruptedException ex) { + throw new IllegalStateException(ex); + } + } + + /** + * Waits until all threads in this pool have shutdown, or until the specified time has passed. + * @param msToWait Maximum time to wait. + * @return {@code false} if the maximum time passed, {@code true} otherwise. + * @throws InterruptedException If this thread is interrupted. + */ + public boolean joinInterruptable(final long msToWait) throws InterruptedException { + return this.join(msToWait, true); + } + + protected final boolean join(final long msToWait, final boolean interruptable) throws InterruptedException { + final long nsToWait = msToWait * (1000 * 1000); + final long start = System.nanoTime(); + final long deadline = start + nsToWait; + boolean interrupted = false; + try { + for (final PrioritisedThread thread : this.threads) { + for (;;) { + if (!thread.isAlive()) { + break; + } + final long current = System.nanoTime(); + if (current >= deadline) { + return false; + } + + try { + thread.join(Math.max(1L, (deadline - current) / (1000 * 1000))); + } catch (final InterruptedException ex) { + if (interruptable) { + throw ex; + } + interrupted = true; + } + } + } + + return true; + } finally { + if (interrupted) { + Thread.currentThread().interrupt(); + } + } + } + + /** + * Shuts down this thread pool, optionally waiting for all tasks to be executed. + * This function will invoke {@link PrioritisedPoolExecutor#shutdown()} on all created executors on this + * thread pool. + * @param wait Whether to wait for tasks to be executed + */ + public void shutdown(final boolean wait) { + final ArrayList queuesToShutdown; + synchronized (this.nonShutdownQueues) { + this.shutdown = true; + queuesToShutdown = new ArrayList<>(this.nonShutdownQueues); + } + + for (final PrioritisedPoolExecutorImpl queue : queuesToShutdown) { + queue.shutdown(); + } + + for (final PrioritisedThread thread : this.threads) { + // none of these can be true or else NPE + thread.close(false, false); + } + + if (wait) { + final ArrayList queues; + synchronized (this.activeQueues) { + queues = new ArrayList<>(this.activeQueues); + } + for (final PrioritisedPoolExecutorImpl queue : queues) { + queue.waitUntilAllExecuted(); + } + } + } + + protected static final class PrioritisedThread extends PrioritisedQueueExecutorThread { + + protected final PrioritisedThreadPool pool; + protected final AtomicBoolean alertedHighPriority = new AtomicBoolean(); + + public PrioritisedThread(final PrioritisedThreadPool pool) { + super(null); + this.pool = pool; + } + + public boolean alertHighPriorityExecutor() { + if (!this.notifyTasks()) { + if (!this.alertedHighPriority.get()) { + this.alertedHighPriority.set(true); + } + return false; + } + + return true; + } + + private boolean isAlertedHighPriority() { + return this.alertedHighPriority.get() && this.alertedHighPriority.getAndSet(false); + } + + @Override + protected boolean pollTasks() { + final PrioritisedThreadPool pool = this.pool; + final TreeSet queues = this.pool.queues; + + boolean ret = false; + for (;;) { + if (this.halted) { + break; + } + // try to find a queue + // note that if and ONLY IF the queues set is empty, this means there are no tasks for us to execute. + // so we can only break when it's empty + final PrioritisedPoolExecutorImpl queue; + // select queue + synchronized (queues) { + queue = queues.pollFirst(); + if (queue == null) { + // no tasks to execute + break; + } + + queue.schedulingId = ++pool.schedulingIdGenerator; + // we own this queue now, so increment the executor count + // do we also need to push this queue up for grabs for another executor? + if (++queue.concurrentExecutors < queue.maximumExecutors) { + // re-add to queues + // it's very important this is done in the same synchronised block for polling, as this prevents + // us from possibly later adding a queue that should not exist in the set + queues.add(queue); + queue.isQueued = true; + } else { + queue.isQueued = false; + } + // note: we cannot drain entries from the queue while holding this lock, as it will cause deadlock + // the queue addition holds the per-queue lock first then acquires the lock we have now, but if we + // try to poll now we don't hold the per queue lock but we do hold the global lock... + } + + // parse tasks as long as we are allowed + final long start = System.nanoTime(); + final long deadline = start + pool.queueMaxHoldTime; + do { + try { + if (this.halted) { + break; + } + if (!queue.executeTask()) { + // no more tasks, try next queue + break; + } + ret = true; + } catch (final ThreadDeath death) { + throw death; // goodbye world... + } catch (final Throwable throwable) { + LOGGER.error("Exception thrown from thread '" + this.getName() + "' in queue '" + queue.toString() + "'", throwable); + } + } while (!this.isAlertedHighPriority() && System.nanoTime() <= deadline); + + synchronized (queues) { + // decrement executors, we are no longer executing + if (queue.isQueued) { + queues.remove(queue); + queue.isQueued = false; + } + if (--queue.concurrentExecutors == 0 && queue.scheduledPriority == null) { + // reset scheduling id once the queue is empty again + // this will ensure empty queues are not prioritised suddenly over active queues once tasks are + // queued + queue.schedulingId = 0L; + } + + // ensure the executor is queued for execution again + if (!queue.isHalted && queue.scheduledPriority != null) { // make sure it actually has tasks + queues.add(queue); + queue.isQueued = true; + } + } + } + + return ret; + } + } + + public interface PrioritisedPoolExecutor extends PrioritisedExecutor { + + /** + * Removes this queue from the thread pool without shutting the queue down or waiting for queued tasks to be executed + */ + public void halt(); + + /** + * Returns whether this executor is scheduled to run tasks or is running tasks, otherwise it returns whether + * this queue is not halted and not shutdown. + */ + public boolean isActive(); + } + + protected static final class PrioritisedPoolExecutorImpl extends PrioritisedThreadedTaskQueue implements PrioritisedPoolExecutor { + + protected final PrioritisedThreadPool pool; + protected final long[] priorityCounts = new long[Priority.TOTAL_SCHEDULABLE_PRIORITIES]; + protected long schedulingId; + protected int concurrentExecutors; + protected Priority scheduledPriority; + + protected final String name; + protected final int maximumExecutors; + protected final int minimumExecutors; + protected boolean isQueued; + + public PrioritisedPoolExecutorImpl(final PrioritisedThreadPool pool, final String name, final int maximumExecutors, final int minimumExecutors) { + this.pool = pool; + this.name = name; + this.maximumExecutors = maximumExecutors; + this.minimumExecutors = minimumExecutors; + } + + public static Comparator comparator() { + return (final PrioritisedPoolExecutorImpl p1, final PrioritisedPoolExecutorImpl p2) -> { + if (p1 == p2) { + return 0; + } + + final int belowMin1 = p1.minimumExecutors - p1.concurrentExecutors; + final int belowMin2 = p2.minimumExecutors - p2.concurrentExecutors; + + // test minimum executors + if (belowMin1 > 0 || belowMin2 > 0) { + // want the largest belowMin to be first + final int minCompare = Integer.compare(belowMin2, belowMin1); + + if (minCompare != 0) { + return minCompare; + } + } + + // prefer higher priority + final int priorityCompare = p1.scheduledPriority.ordinal() - p2.scheduledPriority.ordinal(); + if (priorityCompare != 0) { + return priorityCompare; + } + + // try to spread out the executors so that each can have threads executing + final int executorCompare = p1.concurrentExecutors - p2.concurrentExecutors; + if (executorCompare != 0) { + return executorCompare; + } + + // if all else fails here we just choose whichever executor was queued first + return Long.compare(p1.schedulingId, p2.schedulingId); + }; + } + + private boolean isHalted; + + @Override + public void halt() { + final PrioritisedThreadPool pool = this.pool; + final TreeSet queues = pool.queues; + synchronized (queues) { + if (this.isHalted) { + return; + } + this.isHalted = true; + if (this.isQueued) { + queues.remove(this); + this.isQueued = false; + } + } + synchronized (pool.nonShutdownQueues) { + pool.nonShutdownQueues.remove(this); + } + synchronized (pool.activeQueues) { + pool.activeQueues.remove(this); + } + } + + @Override + public boolean isActive() { + final PrioritisedThreadPool pool = this.pool; + final TreeSet queues = pool.queues; + + synchronized (queues) { + if (this.concurrentExecutors != 0) { + return true; + } + synchronized (pool.activeQueues) { + if (pool.activeQueues.contains(this)) { + return true; + } + } + } + + return false; + } + + private long totalQueuedTasks = 0L; + + @Override + protected void priorityChange(final PrioritisedThreadedTaskQueue.PrioritisedTask task, final Priority from, final Priority to) { + // Note: The superclass' queue lock is ALWAYS held when inside this method. So we do NOT need to do any additional synchronisation + // for accessing this queue's state. + final long[] priorityCounts = this.priorityCounts; + final boolean shutdown = this.isShutdown(); + + if (from == null && to == Priority.COMPLETING) { + throw new IllegalStateException("Cannot complete task without queueing it first"); + } + + // we should only notify for queueing of tasks, not changing priorities + final boolean shouldNotifyTasks = from == null; + + final Priority scheduledPriority = this.scheduledPriority; + if (from != null) { + --priorityCounts[from.priority]; + } + if (to != Priority.COMPLETING) { + ++priorityCounts[to.priority]; + } + final long totalQueuedTasks; + if (to == Priority.COMPLETING) { + totalQueuedTasks = --this.totalQueuedTasks; + } else if (from == null) { + totalQueuedTasks = ++this.totalQueuedTasks; + } else { + totalQueuedTasks = this.totalQueuedTasks; + } + + // find new highest priority + int highest = Math.min(to == Priority.COMPLETING ? Priority.IDLE.priority : to.priority, scheduledPriority == null ? Priority.IDLE.priority : scheduledPriority.priority); + int lowestPriority = priorityCounts.length; // exclusive + for (;highest < lowestPriority; ++highest) { + final long count = priorityCounts[highest]; + if (count < 0) { + throw new IllegalStateException("Priority " + highest + " has " + count + " scheduled tasks"); + } + + if (count != 0) { + break; + } + } + + final Priority newPriority; + if (highest == lowestPriority) { + // no tasks left + newPriority = null; + } else if (shutdown) { + // whichever is lower, the actual greatest priority or simply HIGHEST + // this is so shutdown automatically gets priority + newPriority = Priority.getPriority(Math.min(highest, Priority.HIGHEST.priority)); + } else { + newPriority = Priority.getPriority(highest); + } + + final int executorsWanted; + boolean shouldNotifyHighPriority = false; + + final PrioritisedThreadPool pool = this.pool; + final TreeSet queues = pool.queues; + + synchronized (queues) { + if (!this.isQueued) { + // see if we need to be queued + if (newPriority != null) { + if (this.schedulingId == 0L) { + this.schedulingId = ++pool.schedulingIdGenerator; + } + this.scheduledPriority = newPriority; // must be updated before queue add + if (!this.isHalted && this.concurrentExecutors < this.maximumExecutors) { + shouldNotifyHighPriority = newPriority.isHigherOrEqualPriority(Priority.HIGH); + queues.add(this); + this.isQueued = true; + } + } else { + // do not queue + this.scheduledPriority = null; + } + } else { + // see if we need to NOT be queued + if (newPriority == null) { + queues.remove(this); + this.scheduledPriority = null; + this.isQueued = false; + } else if (scheduledPriority != newPriority) { + // if our priority changed, we need to update it - which means removing and re-adding into the queue + queues.remove(this); + // only now can we update scheduledPriority, since we are no longer in queue + this.scheduledPriority = newPriority; + queues.add(this); + shouldNotifyHighPriority = (scheduledPriority == null || scheduledPriority.isLowerPriority(Priority.HIGH)) && newPriority.isHigherOrEqualPriority(Priority.HIGH); + } + } + + if (this.isQueued) { + executorsWanted = Math.min(this.maximumExecutors - this.concurrentExecutors, (int)totalQueuedTasks); + } else { + executorsWanted = 0; + } + } + + if (newPriority == null && shutdown) { + synchronized (pool.activeQueues) { + pool.activeQueues.remove(this); + } + } + + // Wake up the number of executors we want + if (executorsWanted > 0 || (shouldNotifyTasks | shouldNotifyHighPriority)) { + int notified = 0; + for (final PrioritisedThread thread : pool.threads) { + if ((shouldNotifyHighPriority ? thread.alertHighPriorityExecutor() : thread.notifyTasks()) + && (++notified >= executorsWanted)) { + break; + } + } + } + } + + @Override + public boolean shutdown() { + final boolean ret = super.shutdown(); + if (!ret) { + return ret; + } + + final PrioritisedThreadPool pool = this.pool; + + // remove from active queues + synchronized (pool.nonShutdownQueues) { + pool.nonShutdownQueues.remove(this); + } + + final TreeSet queues = pool.queues; + + // try and shift around our priority + synchronized (queues) { + if (this.scheduledPriority == null) { + // no tasks are queued, ensure we aren't in activeQueues + synchronized (pool.activeQueues) { + pool.activeQueues.remove(this); + } + + return ret; + } + + // try to set scheduled priority to HIGHEST so it drains faster + + if (this.scheduledPriority.isHigherOrEqualPriority(Priority.HIGHEST)) { + // already at target priority (highest or above) + return ret; + } + + // shift priority to HIGHEST + + if (this.isQueued) { + queues.remove(this); + this.scheduledPriority = Priority.HIGHEST; + queues.add(this); + } else { + this.scheduledPriority = Priority.HIGHEST; + } + } + + return ret; + } + } +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadedTaskQueue.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadedTaskQueue.java new file mode 100644 index 0000000000000000000000000000000000000000..3e8401b1b1f833c4f01bc87059a2f48d761d989f --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadedTaskQueue.java @@ -0,0 +1,378 @@ +package ca.spottedleaf.concurrentutil.executor.standard; + +import java.util.ArrayDeque; +import java.util.concurrent.atomic.AtomicLong; + +public class PrioritisedThreadedTaskQueue implements PrioritisedExecutor { + + protected final ArrayDeque[] queues = new ArrayDeque[Priority.TOTAL_SCHEDULABLE_PRIORITIES]; { + for (int i = 0; i < Priority.TOTAL_SCHEDULABLE_PRIORITIES; ++i) { + this.queues[i] = new ArrayDeque<>(); + } + } + + // Use AtomicLong to separate from the queue field, we don't want false sharing here. + protected final AtomicLong totalScheduledTasks = new AtomicLong(); + protected final AtomicLong totalCompletedTasks = new AtomicLong(); + + // this is here to prevent failures to queue stalling flush() calls (as the schedule calls would increment totalScheduledTasks without this check) + protected volatile boolean hasShutdown; + + protected long taskIdGenerator = 0; + + @Override + public PrioritisedExecutor.PrioritisedTask queueRunnable(final Runnable task, final Priority priority) throws IllegalStateException, IllegalArgumentException { + if (!Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Priority " + priority + " is invalid"); + } + if (task == null) { + throw new NullPointerException("Task cannot be null"); + } + + if (this.hasShutdown) { + // prevent us from stalling flush() calls by incrementing scheduled tasks when we really didn't schedule something + throw new IllegalStateException("Queue has shutdown"); + } + + final PrioritisedTask ret; + + synchronized (this.queues) { + if (this.hasShutdown) { + throw new IllegalStateException("Queue has shutdown"); + } + this.getAndAddTotalScheduledTasksVolatile(1L); + + ret = new PrioritisedTask(this.taskIdGenerator++, task, priority, this); + + this.queues[ret.priority.priority].add(ret); + + // call priority change callback (note: only after we successfully queue!) + this.priorityChange(ret, null, priority); + } + + return ret; + } + + @Override + public PrioritisedExecutor.PrioritisedTask createTask(final Runnable task, final Priority priority) { + if (!Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Priority " + priority + " is invalid"); + } + if (task == null) { + throw new NullPointerException("Task cannot be null"); + } + + return new PrioritisedTask(task, priority, this); + } + + @Override + public long getTotalTasksScheduled() { + return this.totalScheduledTasks.get(); + } + + @Override + public long getTotalTasksExecuted() { + return this.totalCompletedTasks.get(); + } + + // callback method for subclasses to override + // from is null when a task is immediately created + protected void priorityChange(final PrioritisedTask task, final Priority from, final Priority to) {} + + /** + * Polls the highest priority task currently available. {@code null} if none. This will mark the + * returned task as completed. + */ + protected PrioritisedTask poll() { + return this.poll(Priority.IDLE); + } + + protected PrioritisedTask poll(final Priority minPriority) { + final ArrayDeque[] queues = this.queues; + synchronized (queues) { + final int max = minPriority.priority; + for (int i = 0; i <= max; ++i) { + final ArrayDeque queue = queues[i]; + PrioritisedTask task; + while ((task = queue.pollFirst()) != null) { + if (task.trySetCompleting(i)) { + return task; + } + } + } + } + + return null; + } + + /** + * Polls and executes the highest priority task currently available. Exceptions thrown during task execution will + * be rethrown. + * @return {@code true} if a task was executed, {@code false} otherwise. + */ + @Override + public boolean executeTask() { + final PrioritisedTask task = this.poll(); + + if (task != null) { + task.executeInternal(); + return true; + } + + return false; + } + + @Override + public boolean shutdown() { + synchronized (this.queues) { + if (this.hasShutdown) { + return false; + } + this.hasShutdown = true; + } + return true; + } + + @Override + public boolean isShutdown() { + return this.hasShutdown; + } + + /* totalScheduledTasks */ + + protected final long getTotalScheduledTasksVolatile() { + return this.totalScheduledTasks.get(); + } + + protected final long getAndAddTotalScheduledTasksVolatile(final long value) { + return this.totalScheduledTasks.getAndAdd(value); + } + + /* totalCompletedTasks */ + + protected final long getTotalCompletedTasksVolatile() { + return this.totalCompletedTasks.get(); + } + + protected final long getAndAddTotalCompletedTasksVolatile(final long value) { + return this.totalCompletedTasks.getAndAdd(value); + } + + protected static final class PrioritisedTask implements PrioritisedExecutor.PrioritisedTask { + protected final PrioritisedThreadedTaskQueue queue; + protected long id; + protected static final long NOT_SCHEDULED_ID = -1L; + + protected Runnable runnable; + protected volatile Priority priority; + + protected PrioritisedTask(final long id, final Runnable runnable, final Priority priority, final PrioritisedThreadedTaskQueue queue) { + if (!Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + + this.priority = priority; + this.runnable = runnable; + this.queue = queue; + this.id = id; + } + + protected PrioritisedTask(final Runnable runnable, final Priority priority, final PrioritisedThreadedTaskQueue queue) { + if (!Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + + this.priority = priority; + this.runnable = runnable; + this.queue = queue; + this.id = NOT_SCHEDULED_ID; + } + + @Override + public boolean queue() { + if (this.queue.hasShutdown) { + throw new IllegalStateException("Queue has shutdown"); + } + + synchronized (this.queue.queues) { + if (this.queue.hasShutdown) { + throw new IllegalStateException("Queue has shutdown"); + } + + final Priority priority = this.priority; + if (priority == Priority.COMPLETING) { + return false; + } + + if (this.id != NOT_SCHEDULED_ID) { + return false; + } + + this.queue.getAndAddTotalScheduledTasksVolatile(1L); + this.id = this.queue.taskIdGenerator++; + this.queue.queues[priority.priority].add(this); + + this.queue.priorityChange(this, null, priority); + + return true; + } + } + + protected boolean trySetCompleting(final int minPriority) { + final Priority oldPriority = this.priority; + if (oldPriority != Priority.COMPLETING && oldPriority.isHigherOrEqualPriority(minPriority)) { + this.priority = Priority.COMPLETING; + if (this.id != NOT_SCHEDULED_ID) { + this.queue.priorityChange(this, oldPriority, Priority.COMPLETING); + } + return true; + } + + return false; + } + + @Override + public Priority getPriority() { + return this.priority; + } + + @Override + public boolean setPriority(final Priority priority) { + if (!Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + synchronized (this.queue.queues) { + final Priority curr = this.priority; + + if (curr == Priority.COMPLETING) { + return false; + } + + if (curr == priority) { + return true; + } + + this.priority = priority; + if (this.id != NOT_SCHEDULED_ID) { + this.queue.queues[priority.priority].add(this); + + // call priority change callback + this.queue.priorityChange(this, curr, priority); + } + } + + return true; + } + + @Override + public boolean raisePriority(final Priority priority) { + if (!Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + + synchronized (this.queue.queues) { + final Priority curr = this.priority; + + if (curr == Priority.COMPLETING) { + return false; + } + + if (curr.isHigherOrEqualPriority(priority)) { + return true; + } + + this.priority = priority; + if (this.id != NOT_SCHEDULED_ID) { + this.queue.queues[priority.priority].add(this); + + // call priority change callback + this.queue.priorityChange(this, curr, priority); + } + } + + return true; + } + + @Override + public boolean lowerPriority(final Priority priority) { + if (!Priority.isValidPriority(priority)) { + throw new IllegalArgumentException("Invalid priority " + priority); + } + + synchronized (this.queue.queues) { + final Priority curr = this.priority; + + if (curr == Priority.COMPLETING) { + return false; + } + + if (curr.isLowerOrEqualPriority(priority)) { + return true; + } + + this.priority = priority; + if (this.id != NOT_SCHEDULED_ID) { + this.queue.queues[priority.priority].add(this); + + // call priority change callback + this.queue.priorityChange(this, curr, priority); + } + } + + return true; + } + + @Override + public boolean cancel() { + final long id; + synchronized (this.queue.queues) { + final Priority oldPriority = this.priority; + if (oldPriority == Priority.COMPLETING) { + return false; + } + + this.priority = Priority.COMPLETING; + // call priority change callback + if ((id = this.id) != NOT_SCHEDULED_ID) { + this.queue.priorityChange(this, oldPriority, Priority.COMPLETING); + } + } + this.runnable = null; + if (id != NOT_SCHEDULED_ID) { + this.queue.getAndAddTotalCompletedTasksVolatile(1L); + } + return true; + } + + protected void executeInternal() { + try { + final Runnable execute = this.runnable; + this.runnable = null; + execute.run(); + } finally { + if (this.id != NOT_SCHEDULED_ID) { + this.queue.getAndAddTotalCompletedTasksVolatile(1L); + } + } + } + + @Override + public boolean execute() { + synchronized (this.queue.queues) { + final Priority oldPriority = this.priority; + if (oldPriority == Priority.COMPLETING) { + return false; + } + + this.priority = Priority.COMPLETING; + // call priority change callback + if (this.id != NOT_SCHEDULED_ID) { + this.queue.priorityChange(this, oldPriority, Priority.COMPLETING); + } + } + + this.executeInternal(); + return true; + } + } +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/function/BiLong1Function.java b/src/main/java/ca/spottedleaf/concurrentutil/function/BiLong1Function.java new file mode 100644 index 0000000000000000000000000000000000000000..94bfd7c56ffcea7d6491e94a7804bc3bd60fe9c3 --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/function/BiLong1Function.java @@ -0,0 +1,8 @@ +package ca.spottedleaf.concurrentutil.function; + +@FunctionalInterface +public interface BiLong1Function { + + public R apply(final long t1, final T t2); + +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/function/BiLongObjectConsumer.java b/src/main/java/ca/spottedleaf/concurrentutil/function/BiLongObjectConsumer.java new file mode 100644 index 0000000000000000000000000000000000000000..8e7eef07960a18d0593688eba55adfa1c85efadf --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/function/BiLongObjectConsumer.java @@ -0,0 +1,8 @@ +package ca.spottedleaf.concurrentutil.function; + +@FunctionalInterface +public interface BiLongObjectConsumer { + + public void accept(final long key, final V value); + +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/lock/ReentrantAreaLock.java b/src/main/java/ca/spottedleaf/concurrentutil/lock/ReentrantAreaLock.java new file mode 100644 index 0000000000000000000000000000000000000000..7ffe4379b06c03c56abbcbdee3bb720894a10702 --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/lock/ReentrantAreaLock.java @@ -0,0 +1,350 @@ +package ca.spottedleaf.concurrentutil.lock; + +import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; +import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable; +import ca.spottedleaf.concurrentutil.util.IntPairUtil; +import java.util.Objects; +import java.util.concurrent.locks.LockSupport; + +public final class ReentrantAreaLock { + + public final int coordinateShift; + + // aggressive load factor to reduce contention + private final ConcurrentLong2ReferenceChainedHashTable nodes = ConcurrentLong2ReferenceChainedHashTable.createWithCapacity(128, 0.2f); + + public ReentrantAreaLock(final int coordinateShift) { + this.coordinateShift = coordinateShift; + } + + public boolean isHeldByCurrentThread(final int x, final int z) { + final Thread currThread = Thread.currentThread(); + final int shift = this.coordinateShift; + final int sectionX = x >> shift; + final int sectionZ = z >> shift; + + final long coordinate = IntPairUtil.key(sectionX, sectionZ); + final Node node = this.nodes.get(coordinate); + + return node != null && node.thread == currThread; + } + + public boolean isHeldByCurrentThread(final int centerX, final int centerZ, final int radius) { + return this.isHeldByCurrentThread(centerX - radius, centerZ - radius, centerX + radius, centerZ + radius); + } + + public boolean isHeldByCurrentThread(final int fromX, final int fromZ, final int toX, final int toZ) { + if (fromX > toX || fromZ > toZ) { + throw new IllegalArgumentException(); + } + + final Thread currThread = Thread.currentThread(); + final int shift = this.coordinateShift; + final int fromSectionX = fromX >> shift; + final int fromSectionZ = fromZ >> shift; + final int toSectionX = toX >> shift; + final int toSectionZ = toZ >> shift; + + for (int currZ = fromSectionZ; currZ <= toSectionZ; ++currZ) { + for (int currX = fromSectionX; currX <= toSectionX; ++currX) { + final long coordinate = IntPairUtil.key(currX, currZ); + + final Node node = this.nodes.get(coordinate); + + if (node == null || node.thread != currThread) { + return false; + } + } + } + + return true; + } + + public Node tryLock(final int x, final int z) { + return this.tryLock(x, z, x, z); + } + + public Node tryLock(final int centerX, final int centerZ, final int radius) { + return this.tryLock(centerX - radius, centerZ - radius, centerX + radius, centerZ + radius); + } + + public Node tryLock(final int fromX, final int fromZ, final int toX, final int toZ) { + if (fromX > toX || fromZ > toZ) { + throw new IllegalArgumentException(); + } + + final Thread currThread = Thread.currentThread(); + final int shift = this.coordinateShift; + final int fromSectionX = fromX >> shift; + final int fromSectionZ = fromZ >> shift; + final int toSectionX = toX >> shift; + final int toSectionZ = toZ >> shift; + + final long[] areaAffected = new long[(toSectionX - fromSectionX + 1) * (toSectionZ - fromSectionZ + 1)]; + int areaAffectedLen = 0; + + final Node ret = new Node(this, areaAffected, currThread); + + boolean failed = false; + + // try to fast acquire area + for (int currZ = fromSectionZ; currZ <= toSectionZ; ++currZ) { + for (int currX = fromSectionX; currX <= toSectionX; ++currX) { + final long coordinate = IntPairUtil.key(currX, currZ); + + final Node prev = this.nodes.putIfAbsent(coordinate, ret); + + if (prev == null) { + areaAffected[areaAffectedLen++] = coordinate; + continue; + } + + if (prev.thread != currThread) { + failed = true; + break; + } + } + } + + if (!failed) { + return ret; + } + + // failed, undo logic + if (areaAffectedLen != 0) { + for (int i = 0; i < areaAffectedLen; ++i) { + final long key = areaAffected[i]; + + if (this.nodes.remove(key) != ret) { + throw new IllegalStateException(); + } + } + + areaAffectedLen = 0; + + // since we inserted, we need to drain waiters + Thread unpark; + while ((unpark = ret.pollOrBlockAdds()) != null) { + LockSupport.unpark(unpark); + } + } + + return null; + } + + public Node lock(final int x, final int z) { + final Thread currThread = Thread.currentThread(); + final int shift = this.coordinateShift; + final int sectionX = x >> shift; + final int sectionZ = z >> shift; + + final long coordinate = IntPairUtil.key(sectionX, sectionZ); + final long[] areaAffected = new long[1]; + areaAffected[0] = coordinate; + + final Node ret = new Node(this, areaAffected, currThread); + + for (long failures = 0L;;) { + final Node park; + + // try to fast acquire area + { + final Node prev = this.nodes.putIfAbsent(coordinate, ret); + + if (prev == null) { + ret.areaAffectedLen = 1; + return ret; + } else if (prev.thread != currThread) { + park = prev; + } else { + // only one node we would want to acquire, and it's owned by this thread already + // areaAffectedLen = 0 already + return ret; + } + } + + ++failures; + + if (failures > 128L && park.add(currThread)) { + LockSupport.park(); + } else { + // high contention, spin wait + if (failures < 128L) { + for (long i = 0; i < failures; ++i) { + Thread.onSpinWait(); + } + failures = failures << 1; + } else if (failures < 1_200L) { + LockSupport.parkNanos(1_000L); + failures = failures + 1L; + } else { // scale 0.1ms (100us) per failure + Thread.yield(); + LockSupport.parkNanos(100_000L * failures); + failures = failures + 1L; + } + } + } + } + + public Node lock(final int centerX, final int centerZ, final int radius) { + return this.lock(centerX - radius, centerZ - radius, centerX + radius, centerZ + radius); + } + + public Node lock(final int fromX, final int fromZ, final int toX, final int toZ) { + if (fromX > toX || fromZ > toZ) { + throw new IllegalArgumentException(); + } + + final Thread currThread = Thread.currentThread(); + final int shift = this.coordinateShift; + final int fromSectionX = fromX >> shift; + final int fromSectionZ = fromZ >> shift; + final int toSectionX = toX >> shift; + final int toSectionZ = toZ >> shift; + + if (((fromSectionX ^ toSectionX) | (fromSectionZ ^ toSectionZ)) == 0) { + return this.lock(fromX, fromZ); + } + + final long[] areaAffected = new long[(toSectionX - fromSectionX + 1) * (toSectionZ - fromSectionZ + 1)]; + int areaAffectedLen = 0; + + final Node ret = new Node(this, areaAffected, currThread); + + for (long failures = 0L;;) { + Node park = null; + boolean addedToArea = false; + boolean alreadyOwned = false; + boolean allOwned = true; + + // try to fast acquire area + for (int currZ = fromSectionZ; currZ <= toSectionZ; ++currZ) { + for (int currX = fromSectionX; currX <= toSectionX; ++currX) { + final long coordinate = IntPairUtil.key(currX, currZ); + + final Node prev = this.nodes.putIfAbsent(coordinate, ret); + + if (prev == null) { + addedToArea = true; + allOwned = false; + areaAffected[areaAffectedLen++] = coordinate; + continue; + } + + if (prev.thread != currThread) { + park = prev; + alreadyOwned = true; + break; + } + } + } + + // check for failure + if ((park != null && addedToArea) || (park == null && alreadyOwned && !allOwned)) { + // failure to acquire: added and we need to block, or improper lock usage + for (int i = 0; i < areaAffectedLen; ++i) { + final long key = areaAffected[i]; + + if (this.nodes.remove(key) != ret) { + throw new IllegalStateException(); + } + } + + areaAffectedLen = 0; + + // since we inserted, we need to drain waiters + Thread unpark; + while ((unpark = ret.pollOrBlockAdds()) != null) { + LockSupport.unpark(unpark); + } + } + + if (park == null) { + if (alreadyOwned && !allOwned) { + throw new IllegalStateException("Improper lock usage: Should never acquire intersecting areas"); + } + ret.areaAffectedLen = areaAffectedLen; + return ret; + } + + // failed + + ++failures; + + if (failures > 128L && park.add(currThread)) { + LockSupport.park(park); + } else { + // high contention, spin wait + if (failures < 128L) { + for (long i = 0; i < failures; ++i) { + Thread.onSpinWait(); + } + failures = failures << 1; + } else if (failures < 1_200L) { + LockSupport.parkNanos(1_000L); + failures = failures + 1L; + } else { // scale 0.1ms (100us) per failure + Thread.yield(); + LockSupport.parkNanos(100_000L * failures); + failures = failures + 1L; + } + } + + if (addedToArea) { + // try again, so we need to allow adds so that other threads can properly block on us + ret.allowAdds(); + } + } + } + + public void unlock(final Node node) { + if (node.lock != this) { + throw new IllegalStateException("Unlock target lock mismatch"); + } + + final long[] areaAffected = node.areaAffected; + final int areaAffectedLen = node.areaAffectedLen; + + if (areaAffectedLen == 0) { + // here we are not in the node map, and so do not need to remove from the node map or unblock any waiters + return; + } + + Objects.checkFromToIndex(0, areaAffectedLen, areaAffected.length); + + // remove from node map; allowing other threads to lock + for (int i = 0; i < areaAffectedLen; ++i) { + final long coordinate = areaAffected[i]; + if (this.nodes.remove(coordinate, node) != node) { + throw new IllegalStateException(); + } + } + + Thread unpark; + while ((unpark = node.pollOrBlockAdds()) != null) { + LockSupport.unpark(unpark); + } + } + + public static final class Node extends MultiThreadedQueue { + + private final ReentrantAreaLock lock; + private final long[] areaAffected; + private int areaAffectedLen; + private final Thread thread; + + private Node(final ReentrantAreaLock lock, final long[] areaAffected, final Thread thread) { + this.lock = lock; + this.areaAffected = areaAffected; + this.thread = thread; + } + + @Override + public String toString() { + return "Node{" + + "areaAffected=" + IntPairUtil.toString(this.areaAffected, 0, this.areaAffectedLen) + + ", thread=" + this.thread + + '}'; + } + } +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/map/ConcurrentLong2ReferenceChainedHashTable.java b/src/main/java/ca/spottedleaf/concurrentutil/map/ConcurrentLong2ReferenceChainedHashTable.java new file mode 100644 index 0000000000000000000000000000000000000000..d701998b376579ec652fb94823befa3cc0bc4090 --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/map/ConcurrentLong2ReferenceChainedHashTable.java @@ -0,0 +1,1684 @@ +package ca.spottedleaf.concurrentutil.map; + +import ca.spottedleaf.concurrentutil.function.BiLong1Function; +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.concurrentutil.util.HashUtil; +import ca.spottedleaf.concurrentutil.util.IntegerUtil; +import ca.spottedleaf.concurrentutil.util.ThrowUtil; +import ca.spottedleaf.concurrentutil.util.Validate; +import java.lang.invoke.VarHandle; +import java.util.Arrays; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.PrimitiveIterator; +import java.util.concurrent.atomic.LongAdder; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.LongConsumer; +import java.util.function.LongFunction; +import java.util.function.Predicate; + +/** + * Concurrent hashtable implementation supporting mapping arbitrary {@code long} values onto non-null {@code Object} + * values with support for multiple writer and multiple reader threads. + * + *

Happens-before relationship

+ *

+ * As with {@link java.util.concurrent.ConcurrentMap}, there is a happens-before relationship between actions in one thread + * prior to writing to the map and access to the results of those actions in another thread. + *

+ * + *

Atomicity of functional methods

+ *

+ * Functional methods are functions declared in this class which possibly perform a write (remove, replace, or modify) + * to an entry in this map as a result of invoking a function on an input parameter. For example, {@link #compute(long, BiLong1Function)}, + * {@link #merge(long, Object, BiFunction)} and {@link #removeIf(long, Predicate)} are examples of functional methods. + * Functional methods will be performed atomically, that is, the input parameter is guaranteed to only be invoked at most + * once per function call. The consequence of this behavior however is that a critical lock for a bin entry is held, which + * means that if the input parameter invocation makes additional calls to write into this hash table that the result + * is undefined and deadlock-prone. + *

+ * + * @param + * @see java.util.concurrent.ConcurrentMap + */ +public class ConcurrentLong2ReferenceChainedHashTable { + + protected static final int DEFAULT_CAPACITY = 16; + protected static final float DEFAULT_LOAD_FACTOR = 0.75f; + protected static final int MAXIMUM_CAPACITY = Integer.MIN_VALUE >>> 1; + + protected final LongAdder size = new LongAdder(); + protected final float loadFactor; + + protected volatile TableEntry[] table; + + protected static final int THRESHOLD_NO_RESIZE = -1; + protected static final int THRESHOLD_RESIZING = -2; + protected volatile int threshold; + protected static final VarHandle THRESHOLD_HANDLE = ConcurrentUtil.getVarHandle(ConcurrentLong2ReferenceChainedHashTable.class, "threshold", int.class); + + protected final int getThresholdAcquire() { + return (int)THRESHOLD_HANDLE.getAcquire(this); + } + + protected final int getThresholdVolatile() { + return (int)THRESHOLD_HANDLE.getVolatile(this); + } + + protected final void setThresholdPlain(final int threshold) { + THRESHOLD_HANDLE.set(this, threshold); + } + + protected final void setThresholdRelease(final int threshold) { + THRESHOLD_HANDLE.setRelease(this, threshold); + } + + protected final void setThresholdVolatile(final int threshold) { + THRESHOLD_HANDLE.setVolatile(this, threshold); + } + + protected final int compareExchangeThresholdVolatile(final int expect, final int update) { + return (int)THRESHOLD_HANDLE.compareAndExchange(this, expect, update); + } + + public ConcurrentLong2ReferenceChainedHashTable() { + this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR); + } + + protected static int getTargetThreshold(final int capacity, final float loadFactor) { + final double ret = (double)capacity * (double)loadFactor; + if (Double.isInfinite(ret) || ret >= ((double)Integer.MAX_VALUE)) { + return THRESHOLD_NO_RESIZE; + } + + return (int)Math.ceil(ret); + } + + protected static int getCapacityFor(final int capacity) { + if (capacity <= 0) { + throw new IllegalArgumentException("Invalid capacity: " + capacity); + } + if (capacity >= MAXIMUM_CAPACITY) { + return MAXIMUM_CAPACITY; + } + return IntegerUtil.roundCeilLog2(capacity); + } + + protected ConcurrentLong2ReferenceChainedHashTable(final int capacity, final float loadFactor) { + final int tableSize = getCapacityFor(capacity); + + if (loadFactor <= 0.0 || !Float.isFinite(loadFactor)) { + throw new IllegalArgumentException("Invalid load factor: " + loadFactor); + } + + if (tableSize == MAXIMUM_CAPACITY) { + this.setThresholdPlain(THRESHOLD_NO_RESIZE); + } else { + this.setThresholdPlain(getTargetThreshold(tableSize, loadFactor)); + } + + this.loadFactor = loadFactor; + // noinspection unchecked + this.table = (TableEntry[])new TableEntry[tableSize]; + } + + public static ConcurrentLong2ReferenceChainedHashTable createWithCapacity(final int capacity) { + return createWithCapacity(capacity, DEFAULT_LOAD_FACTOR); + } + + public static ConcurrentLong2ReferenceChainedHashTable createWithCapacity(final int capacity, final float loadFactor) { + return new ConcurrentLong2ReferenceChainedHashTable<>(capacity, loadFactor); + } + + public static ConcurrentLong2ReferenceChainedHashTable createWithExpected(final int expected) { + return createWithExpected(expected, DEFAULT_LOAD_FACTOR); + } + + public static ConcurrentLong2ReferenceChainedHashTable createWithExpected(final int expected, final float loadFactor) { + final int capacity = (int)Math.ceil((double)expected / (double)loadFactor); + + return createWithCapacity(capacity, loadFactor); + } + + /** must be deterministic given a key */ + protected static int getHash(final long key) { + return (int)HashUtil.mix(key); + } + + /** + * Returns the load factor associated with this map. + */ + public final float getLoadFactor() { + return this.loadFactor; + } + + protected static TableEntry getAtIndexVolatile(final TableEntry[] table, final int index) { + //noinspection unchecked + return (TableEntry)TableEntry.TABLE_ENTRY_ARRAY_HANDLE.getVolatile(table, index); + } + + protected static void setAtIndexRelease(final TableEntry[] table, final int index, final TableEntry value) { + TableEntry.TABLE_ENTRY_ARRAY_HANDLE.setRelease(table, index, value); + } + + protected static void setAtIndexVolatile(final TableEntry[] table, final int index, final TableEntry value) { + TableEntry.TABLE_ENTRY_ARRAY_HANDLE.setVolatile(table, index, value); + } + + protected static TableEntry compareAndExchangeAtIndexVolatile(final TableEntry[] table, final int index, + final TableEntry expect, final TableEntry update) { + //noinspection unchecked + return (TableEntry)TableEntry.TABLE_ENTRY_ARRAY_HANDLE.compareAndExchange(table, index, expect, update); + } + + /** + * Returns the possible node associated with the key, or {@code null} if there is no such node. The node + * returned may have a {@code null} {@link TableEntry#value}, in which case the node is a placeholder for + * a compute/computeIfAbsent call. The placeholder node should not be considered mapped in order to preserve + * happens-before relationships between writes and reads in the map. + */ + protected final TableEntry getNode(final long key) { + final int hash = getHash(key); + + TableEntry[] table = this.table; + for (;;) { + TableEntry node = getAtIndexVolatile(table, hash & (table.length - 1)); + + if (node == null) { + // node == null + return node; + } + + if (node.resize) { + table = (TableEntry[])node.getValuePlain(); + continue; + } + + for (; node != null; node = node.getNextVolatile()) { + if (node.key == key) { + return node; + } + } + + // node == null + return node; + } + } + + /** + * Returns the currently mapped value associated with the specified key, or {@code null} if there is none. + * + * @param key Specified key + */ + public V get(final long key) { + final TableEntry node = this.getNode(key); + return node == null ? null : node.getValueVolatile(); + } + + /** + * Returns the currently mapped value associated with the specified key, or the specified default value if there is none. + * + * @param key Specified key + * @param defaultValue Specified default value + */ + public V getOrDefault(final long key, final V defaultValue) { + final TableEntry node = this.getNode(key); + if (node == null) { + return defaultValue; + } + + final V ret = node.getValueVolatile(); + if (ret == null) { + // ret == null for nodes pre-allocated to compute() and friends + return defaultValue; + } + + return ret; + } + + /** + * Returns whether the specified key is mapped to some value. + * @param key Specified key + */ + public boolean containsKey(final long key) { + // cannot use getNode, as the node may be a placeholder for compute() + return this.get(key) != null; + } + + /** + * Returns whether the specified value has a key mapped to it. + * @param value Specified value + * @throws NullPointerException If value is null + */ + public boolean containsValue(final V value) { + Validate.notNull(value, "Value cannot be null"); + + final NodeIterator iterator = new NodeIterator<>(this.table); + + TableEntry node; + while ((node = iterator.findNext()) != null) { + // need to use acquire here to ensure the happens-before relationship + if (node.getValueAcquire() == value) { + return true; + } + } + + return false; + } + + /** + * Returns the number of mappings in this map. + */ + public int size() { + final long ret = this.size.sum(); + + if (ret <= 0L) { + return 0; + } + if (ret >= (long)Integer.MAX_VALUE) { + return Integer.MAX_VALUE; + } + + return (int)ret; + } + + /** + * Returns whether this map has no mappings. + */ + public boolean isEmpty() { + return this.size.sum() <= 0L; + } + + /** + * Adds count to size and checks threshold for resizing + */ + protected final void addSize(final long count) { + this.size.add(count); + + final int threshold = this.getThresholdAcquire(); + + if (threshold < 0L) { + // resizing or no resizing allowed, in either cases we do not need to do anything + return; + } + + final long sum = this.size.sum(); + + if (sum < (long)threshold) { + return; + } + + if (threshold != this.compareExchangeThresholdVolatile(threshold, THRESHOLD_RESIZING)) { + // some other thread resized + return; + } + + // create new table + this.resize(sum); + } + + /** + * Resizes table, only invoke for the thread which has successfully updated threshold to {@link #THRESHOLD_RESIZING} + * @param sum Estimate of current mapping count, must be >= old threshold + */ + private void resize(final long sum) { + int capacity; + + // add 1.0, as sum may equal threshold (in which case, sum / loadFactor = current capacity) + // adding 1.0 should at least raise the size by a factor of two due to usage of roundCeilLog2 + final double targetD = ((double)sum / (double)this.loadFactor) + 1.0; + if (targetD >= (double)MAXIMUM_CAPACITY) { + capacity = MAXIMUM_CAPACITY; + } else { + capacity = (int)Math.ceil(targetD); + capacity = IntegerUtil.roundCeilLog2(capacity); + if (capacity > MAXIMUM_CAPACITY) { + capacity = MAXIMUM_CAPACITY; + } + } + + // create new table data + + final TableEntry[] newTable = new TableEntry[capacity]; + // noinspection unchecked + final TableEntry resizeNode = new TableEntry<>(0L, (V)newTable, true); + + // transfer nodes from old table + + // does not need to be volatile read, just plain + final TableEntry[] oldTable = this.table; + + // when resizing, the old entries at bin i (where i = hash % oldTable.length) are assigned to + // bin k in the new table (where k = hash % newTable.length) + // since both table lengths are powers of two (specifically, newTable is a multiple of oldTable), + // the possible number of locations in the new table to assign any given i is newTable.length/oldTable.length + + // we can build the new linked nodes for the new table by using a work array sized to newTable.length/oldTable.length + // which holds the _last_ entry in the chain per bin + + final int capOldShift = IntegerUtil.floorLog2(oldTable.length); + final int capDiffShift = IntegerUtil.floorLog2(capacity) - capOldShift; + + if (capDiffShift == 0) { + throw new IllegalStateException("Resizing to same size"); + } + + final TableEntry[] work = new TableEntry[1 << capDiffShift]; // typically, capDiffShift = 1 + + for (int i = 0, len = oldTable.length; i < len; ++i) { + TableEntry binNode = getAtIndexVolatile(oldTable, i); + + for (;;) { + if (binNode == null) { + // just need to replace the bin node, do not need to move anything + if (null == (binNode = compareAndExchangeAtIndexVolatile(oldTable, i, null, resizeNode))) { + break; + } // else: binNode != null, fall through + } + + // need write lock to block other writers + synchronized (binNode) { + if (binNode != (binNode = getAtIndexVolatile(oldTable, i))) { + continue; + } + + // an important detail of resizing is that we do not need to be concerned with synchronisation on + // writes to the new table, as no access to any nodes on bin i on oldTable will occur until a thread + // sees the resizeNode + // specifically, as long as the resizeNode is release written there are no cases where another thread + // will see our writes to the new table + + TableEntry next = binNode.getNextPlain(); + + if (next == null) { + // simple case: do not use work array + + // do not need to create new node, readers only need to see the state of the map at the + // beginning of a call, so any additions onto _next_ don't really matter + // additionally, the old node is replaced so that writers automatically forward to the new table, + // which resolves any issues + newTable[getHash(binNode.key) & (capacity - 1)] = binNode; + } else { + // reset for next usage + Arrays.fill(work, null); + + for (TableEntry curr = binNode; curr != null; curr = curr.getNextPlain()) { + final int newTableIdx = getHash(curr.key) & (capacity - 1); + final int workIdx = newTableIdx >>> capOldShift; + + final TableEntry replace = new TableEntry<>(curr.key, curr.getValuePlain()); + + final TableEntry workNode = work[workIdx]; + work[workIdx] = replace; + + if (workNode == null) { + newTable[newTableIdx] = replace; + continue; + } else { + workNode.setNextPlain(replace); + continue; + } + } + } + + setAtIndexRelease(oldTable, i, resizeNode); + break; + } + } + } + + // calculate new threshold + final int newThreshold; + if (capacity == MAXIMUM_CAPACITY) { + newThreshold = THRESHOLD_NO_RESIZE; + } else { + newThreshold = getTargetThreshold(capacity, loadFactor); + } + + this.table = newTable; + // finish resize operation by releasing hold on threshold + this.setThresholdVolatile(newThreshold); + } + + /** + * Subtracts count from size + */ + protected final void subSize(final long count) { + this.size.add(-count); + } + + /** + * Atomically updates the value associated with {@code key} to {@code value}, or inserts a new mapping with {@code key} + * mapped to {@code value}. + * @param key Specified key + * @param value Specified value + * @throws NullPointerException If value is null + * @return Old value previously associated with key, or {@code null} if none. + */ + public V put(final long key, final V value) { + Validate.notNull(value, "Value may not be null"); + + final int hash = getHash(key); + + TableEntry[] table = this.table; + table_loop: + for (;;) { + final int index = hash & (table.length - 1); + + TableEntry node = getAtIndexVolatile(table, index); + node_loop: + for (;;) { + if (node == null) { + if (null == (node = compareAndExchangeAtIndexVolatile(table, index, null, new TableEntry<>(key, value)))) { + // successfully inserted + this.addSize(1L); + return null; + } // else: node != null, fall through + } + + if (node.resize) { + table = (TableEntry[])node.getValuePlain(); + continue table_loop; + } + + synchronized (node) { + if (node != (node = getAtIndexVolatile(table, index))) { + continue node_loop; + } + // plain reads are fine during synchronised access, as we are the only writer + TableEntry prev = null; + for (; node != null; prev = node, node = node.getNextPlain()) { + if (node.key == key) { + final V ret = node.getValuePlain(); + node.setValueVolatile(value); + return ret; + } + } + + // volatile ordering ensured by addSize(), but we need release here + // to ensure proper ordering with reads and other writes + prev.setNextRelease(new TableEntry<>(key, value)); + } + + this.addSize(1L); + return null; + } + } + } + + /** + * Atomically inserts a new mapping with {@code key} mapped to {@code value} if and only if {@code key} is not + * currently mapped to some value. + * @param key Specified key + * @param value Specified value + * @throws NullPointerException If value is null + * @return Value currently associated with key, or {@code null} if none and {@code value} was associated. + */ + public V putIfAbsent(final long key, final V value) { + Validate.notNull(value, "Value may not be null"); + + final int hash = getHash(key); + + TableEntry[] table = this.table; + table_loop: + for (;;) { + final int index = hash & (table.length - 1); + + TableEntry node = getAtIndexVolatile(table, index); + node_loop: + for (;;) { + if (node == null) { + if (null == (node = compareAndExchangeAtIndexVolatile(table, index, null, new TableEntry<>(key, value)))) { + // successfully inserted + this.addSize(1L); + return null; + } // else: node != null, fall through + } + + if (node.resize) { + table = (TableEntry[])node.getValuePlain(); + continue table_loop; + } + + // optimise ifAbsent calls: check if first node is key before attempting lock acquire + if (node.key == key) { + final V ret = node.getValueVolatile(); + if (ret != null) { + return ret; + } // else: fall back to lock to read the node + } + + synchronized (node) { + if (node != (node = getAtIndexVolatile(table, index))) { + continue node_loop; + } + // plain reads are fine during synchronised access, as we are the only writer + TableEntry prev = null; + for (; node != null; prev = node, node = node.getNextPlain()) { + if (node.key == key) { + return node.getValuePlain(); + } + } + + // volatile ordering ensured by addSize(), but we need release here + // to ensure proper ordering with reads and other writes + prev.setNextRelease(new TableEntry<>(key, value)); + } + + this.addSize(1L); + return null; + } + } + } + + /** + * Atomically updates the value associated with {@code key} to {@code value}, or does nothing if {@code key} is not + * associated with a value. + * @param key Specified key + * @param value Specified value + * @throws NullPointerException If value is null + * @return Old value previously associated with key, or {@code null} if none. + */ + public V replace(final long key, final V value) { + Validate.notNull(value, "Value may not be null"); + + final int hash = getHash(key); + + TableEntry[] table = this.table; + table_loop: + for (;;) { + final int index = hash & (table.length - 1); + + TableEntry node = getAtIndexVolatile(table, index); + node_loop: + for (;;) { + if (node == null) { + return null; + } + + if (node.resize) { + table = (TableEntry[])node.getValuePlain(); + continue table_loop; + } + + synchronized (node) { + if (node != (node = getAtIndexVolatile(table, index))) { + continue node_loop; + } + + // plain reads are fine during synchronised access, as we are the only writer + for (; node != null; node = node.getNextPlain()) { + if (node.key == key) { + final V ret = node.getValuePlain(); + node.setValueVolatile(value); + return ret; + } + } + } + + return null; + } + } + } + + /** + * Atomically updates the value associated with {@code key} to {@code update} if the currently associated + * value is reference equal to {@code expect}, otherwise does nothing. + * @param key Specified key + * @param expect Expected value to check current mapped value with + * @param update Update value to replace mapped value with + * @throws NullPointerException If value is null + * @return If the currently mapped value is not reference equal to {@code expect}, then returns the currently mapped + * value. If the key is not mapped to any value, then returns {@code null}. If neither of the two cases are + * true, then returns {@code expect}. + */ + public V replace(final long key, final V expect, final V update) { + Validate.notNull(expect, "Expect may not be null"); + Validate.notNull(update, "Update may not be null"); + + final int hash = getHash(key); + + TableEntry[] table = this.table; + table_loop: + for (;;) { + final int index = hash & (table.length - 1); + + TableEntry node = getAtIndexVolatile(table, index); + node_loop: + for (;;) { + if (node == null) { + return null; + } + + if (node.resize) { + table = (TableEntry[])node.getValuePlain(); + continue table_loop; + } + + synchronized (node) { + if (node != (node = getAtIndexVolatile(table, index))) { + continue node_loop; + } + + // plain reads are fine during synchronised access, as we are the only writer + for (; node != null; node = node.getNextPlain()) { + if (node.key == key) { + final V ret = node.getValuePlain(); + + if (ret != expect) { + return ret; + } + + node.setValueVolatile(update); + return ret; + } + } + } + + return null; + } + } + } + + /** + * Atomically removes the mapping for the specified key and returns the value it was associated with. If the key + * is not mapped to a value, then does nothing and returns {@code null}. + * @param key Specified key + * @return Old value previously associated with key, or {@code null} if none. + */ + public V remove(final long key) { + final int hash = getHash(key); + + TableEntry[] table = this.table; + table_loop: + for (;;) { + final int index = hash & (table.length - 1); + + TableEntry node = getAtIndexVolatile(table, index); + node_loop: + for (;;) { + if (node == null) { + return null; + } + + if (node.resize) { + table = (TableEntry[])node.getValuePlain(); + continue table_loop; + } + + boolean removed = false; + V ret = null; + + synchronized (node) { + if (node != (node = getAtIndexVolatile(table, index))) { + continue node_loop; + } + + TableEntry prev = null; + + // plain reads are fine during synchronised access, as we are the only writer + for (; node != null; prev = node, node = node.getNextPlain()) { + if (node.key == key) { + ret = node.getValuePlain(); + removed = true; + + // volatile ordering ensured by addSize(), but we need release here + // to ensure proper ordering with reads and other writes + if (prev == null) { + setAtIndexRelease(table, index, node.getNextPlain()); + } else { + prev.setNextRelease(node.getNextPlain()); + } + + break; + } + } + } + + if (removed) { + this.subSize(1L); + } + + return ret; + } + } + } + + /** + * Atomically removes the mapping for the specified key if it is mapped to {@code expect} and returns {@code expect}. If the key + * is not mapped to a value, then does nothing and returns {@code null}. If the key is mapped to a value that is not reference + * equal to {@code expect}, then returns that value. + * @param key Specified key + * @param expect Specified expected value + * @return The specified expected value if the key was mapped to {@code expect}. If + * the key is not mapped to any value, then returns {@code null}. If neither of those cases are true, + * then returns the current (non-null) mapped value for key. + */ + public V remove(final long key, final V expect) { + final int hash = getHash(key); + + TableEntry[] table = this.table; + table_loop: + for (;;) { + final int index = hash & (table.length - 1); + + TableEntry node = getAtIndexVolatile(table, index); + node_loop: + for (;;) { + if (node == null) { + return null; + } + + if (node.resize) { + table = (TableEntry[])node.getValuePlain(); + continue table_loop; + } + + boolean removed = false; + V ret = null; + + synchronized (node) { + if (node != (node = getAtIndexVolatile(table, index))) { + continue node_loop; + } + + TableEntry prev = null; + + // plain reads are fine during synchronised access, as we are the only writer + for (; node != null; prev = node, node = node.getNextPlain()) { + if (node.key == key) { + ret = node.getValuePlain(); + if (ret == expect) { + removed = true; + + // volatile ordering ensured by addSize(), but we need release here + // to ensure proper ordering with reads and other writes + if (prev == null) { + setAtIndexRelease(table, index, node.getNextPlain()); + } else { + prev.setNextRelease(node.getNextPlain()); + } + } + break; + } + } + } + + if (removed) { + this.subSize(1L); + } + + return ret; + } + } + } + + /** + * Atomically removes the mapping for the specified key the predicate returns true for its currently mapped value. If the key + * is not mapped to a value, then does nothing and returns {@code null}. + * + *

+ * This function is a "functional methods" as defined by {@link ConcurrentLong2ReferenceChainedHashTable}. + *

+ * + * @param key Specified key + * @param predicate Specified predicate + * @throws NullPointerException If predicate is null + * @return The specified expected value if the key was mapped to {@code expect}. If + * the key is not mapped to any value, then returns {@code null}. If neither of those cases are true, + * then returns the current (non-null) mapped value for key. + */ + public V removeIf(final long key, final Predicate predicate) { + Validate.notNull(predicate, "Predicate may not be null"); + + final int hash = getHash(key); + + TableEntry[] table = this.table; + table_loop: + for (;;) { + final int index = hash & (table.length - 1); + + TableEntry node = getAtIndexVolatile(table, index); + node_loop: + for (;;) { + if (node == null) { + return null; + } + + if (node.resize) { + table = (TableEntry[])node.getValuePlain(); + continue table_loop; + } + + boolean removed = false; + V ret = null; + + synchronized (node) { + if (node != (node = getAtIndexVolatile(table, index))) { + continue node_loop; + } + + TableEntry prev = null; + + // plain reads are fine during synchronised access, as we are the only writer + for (; node != null; prev = node, node = node.getNextPlain()) { + if (node.key == key) { + ret = node.getValuePlain(); + if (predicate.test(ret)) { + removed = true; + + // volatile ordering ensured by addSize(), but we need release here + // to ensure proper ordering with reads and other writes + if (prev == null) { + setAtIndexRelease(table, index, node.getNextPlain()); + } else { + prev.setNextRelease(node.getNextPlain()); + } + } + break; + } + } + } + + if (removed) { + this.subSize(1L); + } + + return ret; + } + } + } + + /** + * See {@link java.util.concurrent.ConcurrentMap#compute(Object, BiFunction)} + *

+ * This function is a "functional methods" as defined by {@link ConcurrentLong2ReferenceChainedHashTable}. + *

+ */ + public V compute(final long key, final BiLong1Function function) { + final int hash = getHash(key); + + TableEntry[] table = this.table; + table_loop: + for (;;) { + final int index = hash & (table.length - 1); + + TableEntry node = getAtIndexVolatile(table, index); + node_loop: + for (;;) { + V ret = null; + if (node == null) { + final TableEntry insert = new TableEntry<>(key, null); + + boolean added = false; + + synchronized (insert) { + if (null == (node = compareAndExchangeAtIndexVolatile(table, index, null, insert))) { + try { + ret = function.apply(key, null); + } catch (final Throwable throwable) { + setAtIndexVolatile(table, index, null); + ThrowUtil.throwUnchecked(throwable); + // unreachable + return null; + } + + if (ret == null) { + setAtIndexVolatile(table, index, null); + return ret; + } else { + // volatile ordering ensured by addSize(), but we need release here + // to ensure proper ordering with reads and other writes + insert.setValueRelease(ret); + added = true; + } + } // else: node != null, fall through + } + + if (added) { + this.addSize(1L); + return ret; + } + } + + if (node.resize) { + table = (TableEntry[])node.getValuePlain(); + continue table_loop; + } + + boolean removed = false; + boolean added = false; + + synchronized (node) { + if (node != (node = getAtIndexVolatile(table, index))) { + continue node_loop; + } + // plain reads are fine during synchronised access, as we are the only writer + TableEntry prev = null; + for (; node != null; prev = node, node = node.getNextPlain()) { + if (node.key == key) { + final V old = node.getValuePlain(); + + final V computed = function.apply(key, old); + + if (computed != null) { + node.setValueVolatile(computed); + return computed; + } + + // volatile ordering ensured by addSize(), but we need release here + // to ensure proper ordering with reads and other writes + if (prev == null) { + setAtIndexRelease(table, index, node.getNextPlain()); + } else { + prev.setNextRelease(node.getNextPlain()); + } + + removed = true; + break; + } + } + + if (!removed) { + final V computed = function.apply(key, null); + if (computed != null) { + // volatile ordering ensured by addSize(), but we need release here + // to ensure proper ordering with reads and other writes + prev.setNextRelease(new TableEntry<>(key, computed)); + ret = computed; + added = true; + } + } + } + + if (removed) { + this.subSize(1L); + } + if (added) { + this.addSize(1L); + } + + return ret; + } + } + } + + /** + * See {@link java.util.concurrent.ConcurrentMap#computeIfAbsent(Object, Function)} + *

+ * This function is a "functional methods" as defined by {@link ConcurrentLong2ReferenceChainedHashTable}. + *

+ */ + public V computeIfAbsent(final long key, final LongFunction function) { + final int hash = getHash(key); + + TableEntry[] table = this.table; + table_loop: + for (;;) { + final int index = hash & (table.length - 1); + + TableEntry node = getAtIndexVolatile(table, index); + node_loop: + for (;;) { + V ret = null; + if (node == null) { + final TableEntry insert = new TableEntry<>(key, null); + + boolean added = false; + + synchronized (insert) { + if (null == (node = compareAndExchangeAtIndexVolatile(table, index, null, insert))) { + try { + ret = function.apply(key); + } catch (final Throwable throwable) { + setAtIndexVolatile(table, index, null); + ThrowUtil.throwUnchecked(throwable); + // unreachable + return null; + } + + if (ret == null) { + setAtIndexVolatile(table, index, null); + return null; + } else { + // volatile ordering ensured by addSize(), but we need release here + // to ensure proper ordering with reads and other writes + insert.setValueRelease(ret); + added = true; + } + } // else: node != null, fall through + } + + if (added) { + this.addSize(1L); + return ret; + } + } + + if (node.resize) { + table = (TableEntry[])node.getValuePlain(); + continue table_loop; + } + + // optimise ifAbsent calls: check if first node is key before attempting lock acquire + if (node.key == key) { + ret = node.getValueVolatile(); + if (ret != null) { + return ret; + } // else: fall back to lock to read the node + } + + boolean added = false; + + synchronized (node) { + if (node != (node = getAtIndexVolatile(table, index))) { + continue node_loop; + } + // plain reads are fine during synchronised access, as we are the only writer + TableEntry prev = null; + for (; node != null; prev = node, node = node.getNextPlain()) { + if (node.key == key) { + ret = node.getValuePlain(); + return ret; + } + } + + final V computed = function.apply(key); + if (computed != null) { + // volatile ordering ensured by addSize(), but we need release here + // to ensure proper ordering with reads and other writes + prev.setNextRelease(new TableEntry<>(key, computed)); + ret = computed; + added = true; + } + } + + if (added) { + this.addSize(1L); + } + + return ret; + } + } + } + + /** + * See {@link java.util.concurrent.ConcurrentMap#computeIfPresent(Object, BiFunction)} + *

+ * This function is a "functional methods" as defined by {@link ConcurrentLong2ReferenceChainedHashTable}. + *

+ */ + public V computeIfPresent(final long key, final BiLong1Function function) { + final int hash = getHash(key); + + TableEntry[] table = this.table; + table_loop: + for (;;) { + final int index = hash & (table.length - 1); + + TableEntry node = getAtIndexVolatile(table, index); + node_loop: + for (;;) { + if (node == null) { + return null; + } + + if (node.resize) { + table = (TableEntry[])node.getValuePlain(); + continue table_loop; + } + + boolean removed = false; + + synchronized (node) { + if (node != (node = getAtIndexVolatile(table, index))) { + continue node_loop; + } + // plain reads are fine during synchronised access, as we are the only writer + TableEntry prev = null; + for (; node != null; prev = node, node = node.getNextPlain()) { + if (node.key == key) { + final V old = node.getValuePlain(); + + final V computed = function.apply(key, old); + + if (computed != null) { + node.setValueVolatile(computed); + return computed; + } + + // volatile ordering ensured by addSize(), but we need release here + // to ensure proper ordering with reads and other writes + if (prev == null) { + setAtIndexRelease(table, index, node.getNextPlain()); + } else { + prev.setNextRelease(node.getNextPlain()); + } + + removed = true; + break; + } + } + } + + if (removed) { + this.subSize(1L); + } + + return null; + } + } + } + + /** + * See {@link java.util.concurrent.ConcurrentMap#merge(Object, Object, BiFunction)} + *

+ * This function is a "functional methods" as defined by {@link ConcurrentLong2ReferenceChainedHashTable}. + *

+ */ + public V merge(final long key, final V def, final BiFunction function) { + Validate.notNull(def, "Default value may not be null"); + + final int hash = getHash(key); + + TableEntry[] table = this.table; + table_loop: + for (;;) { + final int index = hash & (table.length - 1); + + TableEntry node = getAtIndexVolatile(table, index); + node_loop: + for (;;) { + if (node == null) { + if (null == (node = compareAndExchangeAtIndexVolatile(table, index, null, new TableEntry<>(key, def)))) { + // successfully inserted + this.addSize(1L); + return def; + } // else: node != null, fall through + } + + if (node.resize) { + table = (TableEntry[])node.getValuePlain(); + continue table_loop; + } + + boolean removed = false; + boolean added = false; + V ret = null; + + synchronized (node) { + if (node != (node = getAtIndexVolatile(table, index))) { + continue node_loop; + } + // plain reads are fine during synchronised access, as we are the only writer + TableEntry prev = null; + for (; node != null; prev = node, node = node.getNextPlain()) { + if (node.key == key) { + final V old = node.getValuePlain(); + + final V computed = function.apply(old, def); + + if (computed != null) { + node.setValueVolatile(computed); + return computed; + } + + // volatile ordering ensured by addSize(), but we need release here + // to ensure proper ordering with reads and other writes + if (prev == null) { + setAtIndexRelease(table, index, node.getNextPlain()); + } else { + prev.setNextRelease(node.getNextPlain()); + } + + removed = true; + break; + } + } + + if (!removed) { + // volatile ordering ensured by addSize(), but we need release here + // to ensure proper ordering with reads and other writes + prev.setNextRelease(new TableEntry<>(key, def)); + ret = def; + added = true; + } + } + + if (removed) { + this.subSize(1L); + } + if (added) { + this.addSize(1L); + } + + return ret; + } + } + } + + /** + * Removes at least all entries currently mapped at the beginning of this call. May not remove entries added during + * this call. As a result, only if this map is not modified during the call, that all entries will be removed by + * the end of the call. + * + *

+ * This function is not atomic. + *

+ */ + public void clear() { + // it is possible to optimise this to directly interact with the table, + // but we do need to be careful when interacting with resized tables, + // and the NodeIterator already does this logic + final NodeIterator nodeIterator = new NodeIterator<>(this.table); + + TableEntry node; + while ((node = nodeIterator.findNext()) != null) { + this.remove(node.key); + } + } + + /** + * Returns an iterator over the entries in this map. The iterator is only guaranteed to see entries that were + * added before the beginning of this call, but it may see entries added during. + */ + public Iterator> entryIterator() { + return new EntryIterator<>(this); + } + + /** + * Returns an iterator over the keys in this map. The iterator is only guaranteed to see keys that were + * added before the beginning of this call, but it may see keys added during. + */ + public PrimitiveIterator.OfLong keyIterator() { + return new KeyIterator<>(this); + } + + /** + * Returns an iterator over the values in this map. The iterator is only guaranteed to see values that were + * added before the beginning of this call, but it may see values added during. + */ + public Iterator valueIterator() { + return new ValueIterator<>(this); + } + + protected static final class EntryIterator extends BaseIteratorImpl> { + + protected EntryIterator(final ConcurrentLong2ReferenceChainedHashTable map) { + super(map); + } + + @Override + public TableEntry next() throws NoSuchElementException { + return this.nextNode(); + } + + @Override + public void forEachRemaining(final Consumer> action) { + Validate.notNull(action, "Action may not be null"); + while (this.hasNext()) { + action.accept(this.next()); + } + } + } + + protected static final class KeyIterator extends BaseIteratorImpl implements PrimitiveIterator.OfLong { + + protected KeyIterator(final ConcurrentLong2ReferenceChainedHashTable map) { + super(map); + } + + @Override + public Long next() throws NoSuchElementException { + return Long.valueOf(this.nextNode().key); + } + + @Override + public long nextLong() { + return this.nextNode().key; + } + + @Override + public void forEachRemaining(final Consumer action) { + Validate.notNull(action, "Action may not be null"); + + if (action instanceof LongConsumer longConsumer) { + this.forEachRemaining(longConsumer); + return; + } + + while (this.hasNext()) { + action.accept(this.next()); + } + } + + @Override + public void forEachRemaining(final LongConsumer action) { + Validate.notNull(action, "Action may not be null"); + while (this.hasNext()) { + action.accept(this.nextLong()); + } + } + } + + protected static final class ValueIterator extends BaseIteratorImpl { + + protected ValueIterator(final ConcurrentLong2ReferenceChainedHashTable map) { + super(map); + } + + @Override + public V next() throws NoSuchElementException { + return this.nextNode().getValueVolatile(); + } + + @Override + public void forEachRemaining(final Consumer action) { + Validate.notNull(action, "Action may not be null"); + while (this.hasNext()) { + action.accept(this.next()); + } + } + } + + protected static abstract class BaseIteratorImpl extends NodeIterator implements Iterator { + + protected final ConcurrentLong2ReferenceChainedHashTable map; + protected TableEntry lastReturned; + protected TableEntry nextToReturn; + + protected BaseIteratorImpl(final ConcurrentLong2ReferenceChainedHashTable map) { + super(map.table); + this.map = map; + } + + @Override + public final boolean hasNext() { + if (this.nextToReturn != null) { + return true; + } + + return (this.nextToReturn = this.findNext()) != null; + } + + protected final TableEntry nextNode() throws NoSuchElementException { + TableEntry ret = this.nextToReturn; + if (ret != null) { + this.lastReturned = ret; + this.nextToReturn = null; + return ret; + } + ret = this.findNext(); + if (ret != null) { + this.lastReturned = ret; + return ret; + } + throw new NoSuchElementException(); + } + + @Override + public final void remove() { + final TableEntry lastReturned = this.nextToReturn; + if (lastReturned == null) { + throw new NoSuchElementException(); + } + this.lastReturned = null; + this.map.remove(lastReturned.key); + } + + @Override + public abstract T next() throws NoSuchElementException; + + // overwritten by subclasses to avoid indirection on hasNext() and next() + @Override + public abstract void forEachRemaining(final Consumer action); + } + + protected static class NodeIterator { + + protected TableEntry[] currentTable; + protected ResizeChain resizeChain; + protected TableEntry last; + protected int nextBin; + protected int increment; + + protected NodeIterator(final TableEntry[] baseTable) { + this.currentTable = baseTable; + this.increment = 1; + } + + private TableEntry[] pullResizeChain(final int index) { + final ResizeChain resizeChain = this.resizeChain; + if (resizeChain == null) { + this.currentTable = null; + return null; + } + + final ResizeChain prevChain = resizeChain.prev; + this.resizeChain = prevChain; + if (prevChain == null) { + this.currentTable = null; + return null; + } + + final TableEntry[] newTable = prevChain.table; + + // we recover the original index by modding by the new table length, as the increments applied to the index + // are a multiple of the new table's length + int newIdx = index & (newTable.length - 1); + + // the increment is always the previous table's length + final ResizeChain nextPrevChain = prevChain.prev; + final int increment; + if (nextPrevChain == null) { + increment = 1; + } else { + increment = nextPrevChain.table.length; + } + + // done with the upper table, so we can skip the resize node + newIdx += increment; + + this.increment = increment; + this.nextBin = newIdx; + this.currentTable = newTable; + + return newTable; + } + + private TableEntry[] pushResizeChain(final TableEntry[] table, final TableEntry entry) { + final ResizeChain chain = this.resizeChain; + + if (chain == null) { + final TableEntry[] nextTable = (TableEntry[])entry.getValuePlain(); + + final ResizeChain oldChain = new ResizeChain<>(table, null, null); + final ResizeChain currChain = new ResizeChain<>(nextTable, oldChain, null); + oldChain.next = currChain; + + this.increment = table.length; + this.resizeChain = currChain; + this.currentTable = nextTable; + + return nextTable; + } else { + ResizeChain currChain = chain.next; + if (currChain == null) { + final TableEntry[] ret = (TableEntry[])entry.getValuePlain(); + currChain = new ResizeChain<>(ret, chain, null); + chain.next = currChain; + + this.increment = table.length; + this.resizeChain = currChain; + this.currentTable = ret; + + return ret; + } else { + this.increment = table.length; + this.resizeChain = currChain; + return this.currentTable = currChain.table; + } + } + } + + protected final TableEntry findNext() { + for (;;) { + final TableEntry last = this.last; + if (last != null) { + final TableEntry next = last.getNextVolatile(); + if (next != null) { + this.last = next; + if (next.getValuePlain() == null) { + // compute() node not yet available + continue; + } + return next; + } + } + + TableEntry[] table = this.currentTable; + + if (table == null) { + return null; + } + + int idx = this.nextBin; + int increment = this.increment; + for (;;) { + if (idx >= table.length) { + table = this.pullResizeChain(idx); + idx = this.nextBin; + increment = this.increment; + if (table != null) { + continue; + } else { + this.last = null; + return null; + } + } + + final TableEntry entry = getAtIndexVolatile(table, idx); + if (entry == null) { + idx += increment; + continue; + } + + if (entry.resize) { + // push onto resize chain + table = this.pushResizeChain(table, entry); + increment = this.increment; + continue; + } + + this.last = entry; + this.nextBin = idx + increment; + if (entry.getValuePlain() != null) { + return entry; + } else { + // compute() node not yet available + break; + } + } + } + } + + protected static final class ResizeChain { + + protected final TableEntry[] table; + protected final ResizeChain prev; + protected ResizeChain next; + + protected ResizeChain(final TableEntry[] table, final ResizeChain prev, final ResizeChain next) { + this.table = table; + this.prev = prev; + this.next = next; + } + } + } + + public static final class TableEntry { + + protected static final VarHandle TABLE_ENTRY_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(TableEntry[].class); + + protected final boolean resize; + + protected final long key; + + protected volatile V value; + protected static final VarHandle VALUE_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "value", Object.class); + + protected final V getValuePlain() { + //noinspection unchecked + return (V)VALUE_HANDLE.get(this); + } + + protected final V getValueAcquire() { + //noinspection unchecked + return (V)VALUE_HANDLE.getAcquire(this); + } + + protected final V getValueVolatile() { + //noinspection unchecked + return (V)VALUE_HANDLE.getVolatile(this); + } + + protected final void setValuePlain(final V value) { + VALUE_HANDLE.set(this, (Object)value); + } + + protected final void setValueRelease(final V value) { + VALUE_HANDLE.setRelease(this, (Object)value); + } + + protected final void setValueVolatile(final V value) { + VALUE_HANDLE.setVolatile(this, (Object)value); + } + + protected volatile TableEntry next; + protected static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "next", TableEntry.class); + + protected final TableEntry getNextPlain() { + //noinspection unchecked + return (TableEntry)NEXT_HANDLE.get(this); + } + + protected final TableEntry getNextVolatile() { + //noinspection unchecked + return (TableEntry)NEXT_HANDLE.getVolatile(this); + } + + protected final void setNextPlain(final TableEntry next) { + NEXT_HANDLE.set(this, next); + } + + protected final void setNextRelease(final TableEntry next) { + NEXT_HANDLE.setRelease(this, next); + } + + protected final void setNextVolatile(final TableEntry next) { + NEXT_HANDLE.setVolatile(this, next); + } + + public TableEntry(final long key, final V value) { + this.resize = false; + this.key = key; + this.setValuePlain(value); + } + + public TableEntry(final long key, final V value, final boolean resize) { + this.resize = resize; + this.key = key; + this.setValuePlain(value); + } + + public long getKey() { + return this.key; + } + + public V getValue() { + return this.getValueVolatile(); + } + } +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRHashTable.java b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRHashTable.java new file mode 100644 index 0000000000000000000000000000000000000000..83965350d292ccf42a34520d84dcda3f88146cff --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRHashTable.java @@ -0,0 +1,1656 @@ +package ca.spottedleaf.concurrentutil.map; + +import ca.spottedleaf.concurrentutil.util.CollectionUtil; +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.concurrentutil.util.HashUtil; +import ca.spottedleaf.concurrentutil.util.IntegerUtil; +import ca.spottedleaf.concurrentutil.util.Validate; +import java.lang.invoke.VarHandle; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.BiPredicate; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.IntFunction; +import java.util.function.Predicate; + +/** + *

+ * Note: Not really tested, use at your own risk. + *

+ * This map is safe for reading from multiple threads, however it is only safe to write from a single thread. + * {@code null} keys or values are not permitted. Writes to values in this map are guaranteed to be ordered by release semantics, + * however immediate visibility to other threads is not guaranteed. However, writes are guaranteed to be made visible eventually. + * Reads are ordered by acquire semantics. + *

+ * Iterators cannot be modified concurrently, and its backing map cannot be modified concurrently. There is no + * fast-fail attempt made by iterators, thus modifying the iterator's backing map while iterating will have undefined + * behaviour. + *

+ *

+ * Subclasses should override {@link #clone()} to return correct instances of this class. + *

+ * @param {@inheritDoc} + * @param {@inheritDoc} + */ +public class SWMRHashTable implements Map, Iterable> { + + protected int size; + + protected TableEntry[] table; + + protected final float loadFactor; + + protected static final VarHandle SIZE_HANDLE = ConcurrentUtil.getVarHandle(SWMRHashTable.class, "size", int.class); + protected static final VarHandle TABLE_HANDLE = ConcurrentUtil.getVarHandle(SWMRHashTable.class, "table", TableEntry[].class); + + /* size */ + + protected final int getSizePlain() { + return (int)SIZE_HANDLE.get(this); + } + + protected final int getSizeOpaque() { + return (int)SIZE_HANDLE.getOpaque(this); + } + + protected final int getSizeAcquire() { + return (int)SIZE_HANDLE.getAcquire(this); + } + + protected final void setSizePlain(final int value) { + SIZE_HANDLE.set(this, value); + } + + protected final void setSizeOpaque(final int value) { + SIZE_HANDLE.setOpaque(this, value); + } + + protected final void setSizeRelease(final int value) { + SIZE_HANDLE.setRelease(this, value); + } + + /* table */ + + protected final TableEntry[] getTablePlain() { + //noinspection unchecked + return (TableEntry[])TABLE_HANDLE.get(this); + } + + protected final TableEntry[] getTableAcquire() { + //noinspection unchecked + return (TableEntry[])TABLE_HANDLE.getAcquire(this); + } + + protected final void setTablePlain(final TableEntry[] table) { + TABLE_HANDLE.set(this, table); + } + + protected final void setTableRelease(final TableEntry[] table) { + TABLE_HANDLE.setRelease(this, table); + } + + protected static final int DEFAULT_CAPACITY = 16; + protected static final float DEFAULT_LOAD_FACTOR = 0.75f; + protected static final int MAXIMUM_CAPACITY = Integer.MIN_VALUE >>> 1; + + /** + * Constructs this map with a capacity of {@code 16} and load factor of {@code 0.75f}. + */ + public SWMRHashTable() { + this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR); + } + + /** + * Constructs this map with the specified capacity and load factor of {@code 0.75f}. + * @param capacity specified initial capacity, > 0 + */ + public SWMRHashTable(final int capacity) { + this(capacity, DEFAULT_LOAD_FACTOR); + } + + /** + * Constructs this map with the specified capacity and load factor. + * @param capacity specified capacity, > 0 + * @param loadFactor specified load factor, > 0 && finite + */ + public SWMRHashTable(final int capacity, final float loadFactor) { + final int tableSize = getCapacityFor(capacity); + + if (loadFactor <= 0.0 || !Float.isFinite(loadFactor)) { + throw new IllegalArgumentException("Invalid load factor: " + loadFactor); + } + + //noinspection unchecked + final TableEntry[] table = new TableEntry[tableSize]; + this.setTablePlain(table); + + if (tableSize == MAXIMUM_CAPACITY) { + this.threshold = -1; + } else { + this.threshold = getTargetCapacity(tableSize, loadFactor); + } + + this.loadFactor = loadFactor; + } + + /** + * Constructs this map with a capacity of {@code 16} or the specified map's size, whichever is larger, and + * with a load factor of {@code 0.75f}. + * All of the specified map's entries are copied into this map. + * @param other The specified map. + */ + public SWMRHashTable(final Map other) { + this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR, other); + } + + /** + * Constructs this map with a minimum capacity of the specified capacity or the specified map's size, whichever is larger, and + * with a load factor of {@code 0.75f}. + * All of the specified map's entries are copied into this map. + * @param capacity specified capacity, > 0 + * @param other The specified map. + */ + public SWMRHashTable(final int capacity, final Map other) { + this(capacity, DEFAULT_LOAD_FACTOR, other); + } + + /** + * Constructs this map with a min capacity of the specified capacity or the specified map's size, whichever is larger, and + * with the specified load factor. + * All of the specified map's entries are copied into this map. + * @param capacity specified capacity, > 0 + * @param loadFactor specified load factor, > 0 && finite + * @param other The specified map. + */ + public SWMRHashTable(final int capacity, final float loadFactor, final Map other) { + this(Math.max(Validate.notNull(other, "Null map").size(), capacity), loadFactor); + this.putAll(other); + } + + protected static TableEntry getAtIndexOpaque(final TableEntry[] table, final int index) { + // noinspection unchecked + return (TableEntry)TableEntry.TABLE_ENTRY_ARRAY_HANDLE.getOpaque(table, index); + } + + protected static void setAtIndexRelease(final TableEntry[] table, final int index, final TableEntry value) { + TableEntry.TABLE_ENTRY_ARRAY_HANDLE.setRelease(table, index, value); + } + + public final float getLoadFactor() { + return this.loadFactor; + } + + protected static int getCapacityFor(final int capacity) { + if (capacity <= 0) { + throw new IllegalArgumentException("Invalid capacity: " + capacity); + } + if (capacity >= MAXIMUM_CAPACITY) { + return MAXIMUM_CAPACITY; + } + return IntegerUtil.roundCeilLog2(capacity); + } + + /** Callers must still use acquire when reading the value of the entry. */ + protected final TableEntry getEntryForOpaque(final K key) { + final int hash = SWMRHashTable.getHash(key); + final TableEntry[] table = this.getTableAcquire(); + + for (TableEntry curr = getAtIndexOpaque(table, hash & (table.length - 1)); curr != null; curr = curr.getNextOpaque()) { + if (hash == curr.hash && (key == curr.key || curr.key.equals(key))) { + return curr; + } + } + + return null; + } + + protected final TableEntry getEntryForPlain(final K key) { + final int hash = SWMRHashTable.getHash(key); + final TableEntry[] table = this.getTablePlain(); + + for (TableEntry curr = table[hash & (table.length - 1)]; curr != null; curr = curr.getNextPlain()) { + if (hash == curr.hash && (key == curr.key || curr.key.equals(key))) { + return curr; + } + } + + return null; + } + + /* MT-Safe */ + + /** must be deterministic given a key */ + private static int getHash(final Object key) { + int hash = key == null ? 0 : key.hashCode(); + return HashUtil.mix(hash); + } + + // rets -1 if capacity*loadFactor is too large + protected static int getTargetCapacity(final int capacity, final float loadFactor) { + final double ret = (double)capacity * (double)loadFactor; + if (Double.isInfinite(ret) || ret >= ((double)Integer.MAX_VALUE)) { + return -1; + } + + return (int)ret; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + /* Make no attempt to deal with concurrent modifications */ + if (!(obj instanceof Map other)) { + return false; + } + + if (this.size() != other.size()) { + return false; + } + + final TableEntry[] table = this.getTableAcquire(); + + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + final V value = curr.getValueAcquire(); + + final Object otherValue = other.get(curr.key); + if (otherValue == null || (value != otherValue && value.equals(otherValue))) { + return false; + } + } + } + + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + /* Make no attempt to deal with concurrent modifications */ + int hash = 0; + final TableEntry[] table = this.getTableAcquire(); + + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + hash += curr.hashCode(); + } + } + + return hash; + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(64); + builder.append("SWMRHashTable:{"); + + this.forEach((final K key, final V value) -> { + builder.append("{key: \"").append(key).append("\", value: \"").append(value).append("\"}"); + }); + + return builder.append('}').toString(); + } + + /** + * {@inheritDoc} + */ + @Override + public SWMRHashTable clone() { + return new SWMRHashTable<>(this.getTableAcquire().length, this.loadFactor, this); + } + + /** + * {@inheritDoc} + */ + @Override + public Iterator> iterator() { + return new EntryIterator<>(this.getTableAcquire(), this); + } + + /** + * {@inheritDoc} + */ + @Override + public void forEach(final Consumer> action) { + Validate.notNull(action, "Null action"); + + final TableEntry[] table = this.getTableAcquire(); + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + action.accept(curr); + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public void forEach(final BiConsumer action) { + Validate.notNull(action, "Null action"); + + final TableEntry[] table = this.getTableAcquire(); + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + final V value = curr.getValueAcquire(); + + action.accept(curr.key, value); + } + } + } + + /** + * Provides the specified consumer with all keys contained within this map. + * @param action The specified consumer. + */ + public void forEachKey(final Consumer action) { + Validate.notNull(action, "Null action"); + + final TableEntry[] table = this.getTableAcquire(); + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + action.accept(curr.key); + } + } + } + + /** + * Provides the specified consumer with all values contained within this map. Equivalent to {@code map.values().forEach(Consumer)}. + * @param action The specified consumer. + */ + public void forEachValue(final Consumer action) { + Validate.notNull(action, "Null action"); + + final TableEntry[] table = this.getTableAcquire(); + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + final V value = curr.getValueAcquire(); + + action.accept(value); + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public V get(final Object key) { + Validate.notNull(key, "Null key"); + + //noinspection unchecked + final TableEntry entry = this.getEntryForOpaque((K)key); + return entry == null ? null : entry.getValueAcquire(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean containsKey(final Object key) { + Validate.notNull(key, "Null key"); + + // note: we need to use getValueAcquire, so that the reads from this map are ordered by acquire semantics + return this.get(key) != null; + } + + /** + * Returns {@code true} if this map contains an entry with the specified key and value at some point during this call. + * @param key The specified key. + * @param value The specified value. + * @return {@code true} if this map contains an entry with the specified key and value. + */ + public boolean contains(final Object key, final Object value) { + Validate.notNull(key, "Null key"); + + //noinspection unchecked + final TableEntry entry = this.getEntryForOpaque((K)key); + + if (entry == null) { + return false; + } + + final V entryVal = entry.getValueAcquire(); + return entryVal == value || entryVal.equals(value); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean containsValue(final Object value) { + Validate.notNull(value, "Null value"); + + final TableEntry[] table = this.getTableAcquire(); + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + final V currVal = curr.getValueAcquire(); + if (currVal == value || currVal.equals(value)) { + return true; + } + } + } + + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public V getOrDefault(final Object key, final V defaultValue) { + Validate.notNull(key, "Null key"); + + //noinspection unchecked + final TableEntry entry = this.getEntryForOpaque((K)key); + + return entry == null ? defaultValue : entry.getValueAcquire(); + } + + /** + * {@inheritDoc} + */ + @Override + public int size() { + return this.getSizeAcquire(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isEmpty() { + return this.getSizeAcquire() == 0; + } + + protected KeySet keyset; + protected ValueCollection values; + protected EntrySet entrySet; + + @Override + public Set keySet() { + return this.keyset == null ? this.keyset = new KeySet<>(this) : this.keyset; + } + + @Override + public Collection values() { + return this.values == null ? this.values = new ValueCollection<>(this) : this.values; + } + + @Override + public Set> entrySet() { + return this.entrySet == null ? this.entrySet = new EntrySet<>(this) : this.entrySet; + } + + /* Non-MT-Safe */ + + protected int threshold; + + protected final void checkResize(final int minCapacity) { + if (minCapacity <= this.threshold || this.threshold < 0) { + return; + } + + final TableEntry[] table = this.getTablePlain(); + int newCapacity = minCapacity >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY : IntegerUtil.roundCeilLog2(minCapacity); + if (newCapacity < 0) { + newCapacity = MAXIMUM_CAPACITY; + } + if (newCapacity <= table.length) { + if (newCapacity == MAXIMUM_CAPACITY) { + return; + } + newCapacity = table.length << 1; + } + + //noinspection unchecked + final TableEntry[] newTable = new TableEntry[newCapacity]; + final int indexMask = newCapacity - 1; + + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry entry = table[i]; entry != null; entry = entry.getNextPlain()) { + final int hash = entry.hash; + final int index = hash & indexMask; + + /* we need to create a new entry since there could be reading threads */ + final TableEntry insert = new TableEntry<>(hash, entry.key, entry.getValuePlain()); + + final TableEntry prev = newTable[index]; + + newTable[index] = insert; + insert.setNextPlain(prev); + } + } + + if (newCapacity == MAXIMUM_CAPACITY) { + this.threshold = -1; /* No more resizing */ + } else { + this.threshold = getTargetCapacity(newCapacity, this.loadFactor); + } + this.setTableRelease(newTable); /* use release to publish entries in table */ + } + + protected final int addToSize(final int num) { + final int newSize = this.getSizePlain() + num; + + this.setSizeOpaque(newSize); + this.checkResize(newSize); + + return newSize; + } + + protected final int removeFromSize(final int num) { + final int newSize = this.getSizePlain() - num; + + this.setSizeOpaque(newSize); + + return newSize; + } + + /* Cannot be used to perform downsizing */ + protected final int removeFromSizePlain(final int num) { + final int newSize = this.getSizePlain() - num; + + this.setSizePlain(newSize); + + return newSize; + } + + protected final V put(final K key, final V value, final boolean onlyIfAbsent) { + final TableEntry[] table = this.getTablePlain(); + final int hash = SWMRHashTable.getHash(key); + final int index = hash & (table.length - 1); + + final TableEntry head = table[index]; + if (head == null) { + final TableEntry insert = new TableEntry<>(hash, key, value); + setAtIndexRelease(table, index, insert); + this.addToSize(1); + return null; + } + + for (TableEntry curr = head;;) { + if (curr.hash == hash && (key == curr.key || curr.key.equals(key))) { + if (onlyIfAbsent) { + return curr.getValuePlain(); + } + + final V currVal = curr.getValuePlain(); + curr.setValueRelease(value); + return currVal; + } + + final TableEntry next = curr.getNextPlain(); + if (next != null) { + curr = next; + continue; + } + + final TableEntry insert = new TableEntry<>(hash, key, value); + + curr.setNextRelease(insert); + this.addToSize(1); + return null; + } + } + + /** + * Removes a key-value pair from this map if the specified predicate returns true. The specified predicate is + * tested with every entry in this map. Returns the number of key-value pairs removed. + * @param predicate The predicate to test key-value pairs against. + * @return The total number of key-value pairs removed from this map. + */ + public int removeIf(final BiPredicate predicate) { + Validate.notNull(predicate, "Null predicate"); + + int removed = 0; + + final TableEntry[] table = this.getTablePlain(); + + bin_iteration_loop: + for (int i = 0, len = table.length; i < len; ++i) { + TableEntry curr = table[i]; + if (curr == null) { + continue; + } + + /* Handle bin nodes first */ + while (predicate.test(curr.key, curr.getValuePlain())) { + ++removed; + this.removeFromSizePlain(1); /* required in case predicate throws an exception */ + + setAtIndexRelease(table, i, curr = curr.getNextPlain()); + + if (curr == null) { + continue bin_iteration_loop; + } + } + + TableEntry prev; + + /* curr at this point is the bin node */ + + for (prev = curr, curr = curr.getNextPlain(); curr != null;) { + /* If we want to remove, then we should hold prev, as it will be a valid entry to link on */ + if (predicate.test(curr.key, curr.getValuePlain())) { + ++removed; + this.removeFromSizePlain(1); /* required in case predicate throws an exception */ + + prev.setNextRelease(curr = curr.getNextPlain()); + } else { + prev = curr; + curr = curr.getNextPlain(); + } + } + } + + return removed; + } + + /** + * Removes a key-value pair from this map if the specified predicate returns true. The specified predicate is + * tested with every entry in this map. Returns the number of key-value pairs removed. + * @param predicate The predicate to test key-value pairs against. + * @return The total number of key-value pairs removed from this map. + */ + public int removeEntryIf(final Predicate> predicate) { + Validate.notNull(predicate, "Null predicate"); + + int removed = 0; + + final TableEntry[] table = this.getTablePlain(); + + bin_iteration_loop: + for (int i = 0, len = table.length; i < len; ++i) { + TableEntry curr = table[i]; + if (curr == null) { + continue; + } + + /* Handle bin nodes first */ + while (predicate.test(curr)) { + ++removed; + this.removeFromSizePlain(1); /* required in case predicate throws an exception */ + + setAtIndexRelease(table, i, curr = curr.getNextPlain()); + + if (curr == null) { + continue bin_iteration_loop; + } + } + + TableEntry prev; + + /* curr at this point is the bin node */ + + for (prev = curr, curr = curr.getNextPlain(); curr != null;) { + /* If we want to remove, then we should hold prev, as it will be a valid entry to link on */ + if (predicate.test(curr)) { + ++removed; + this.removeFromSizePlain(1); /* required in case predicate throws an exception */ + + prev.setNextRelease(curr = curr.getNextPlain()); + } else { + prev = curr; + curr = curr.getNextPlain(); + } + } + } + + return removed; + } + + /** + * {@inheritDoc} + */ + @Override + public V put(final K key, final V value) { + Validate.notNull(key, "Null key"); + Validate.notNull(value, "Null value"); + + return this.put(key, value, false); + } + + /** + * {@inheritDoc} + */ + @Override + public V putIfAbsent(final K key, final V value) { + Validate.notNull(key, "Null key"); + Validate.notNull(value, "Null value"); + + return this.put(key, value, true); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean remove(final Object key, final Object value) { + Validate.notNull(key, "Null key"); + Validate.notNull(value, "Null value"); + + final TableEntry[] table = this.getTablePlain(); + final int hash = SWMRHashTable.getHash(key); + final int index = hash & (table.length - 1); + + final TableEntry head = table[index]; + if (head == null) { + return false; + } + + if (head.hash == hash && (head.key == key || head.key.equals(key))) { + final V currVal = head.getValuePlain(); + + if (currVal != value && !currVal.equals(value)) { + return false; + } + + setAtIndexRelease(table, index, head.getNextPlain()); + this.removeFromSize(1); + + return true; + } + + for (TableEntry curr = head.getNextPlain(), prev = head; curr != null; prev = curr, curr = curr.getNextPlain()) { + if (curr.hash == hash && (curr.key == key || curr.key.equals(key))) { + final V currVal = curr.getValuePlain(); + + if (currVal != value && !currVal.equals(value)) { + return false; + } + + prev.setNextRelease(curr.getNextPlain()); + this.removeFromSize(1); + + return true; + } + } + + return false; + } + + protected final V remove(final Object key, final int hash) { + final TableEntry[] table = this.getTablePlain(); + final int index = (table.length - 1) & hash; + + final TableEntry head = table[index]; + if (head == null) { + return null; + } + + if (hash == head.hash && (head.key == key || head.key.equals(key))) { + setAtIndexRelease(table, index, head.getNextPlain()); + this.removeFromSize(1); + + return head.getValuePlain(); + } + + for (TableEntry curr = head.getNextPlain(), prev = head; curr != null; prev = curr, curr = curr.getNextPlain()) { + if (curr.hash == hash && (key == curr.key || curr.key.equals(key))) { + prev.setNextRelease(curr.getNextPlain()); + this.removeFromSize(1); + + return curr.getValuePlain(); + } + } + + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public V remove(final Object key) { + Validate.notNull(key, "Null key"); + + return this.remove(key, SWMRHashTable.getHash(key)); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean replace(final K key, final V oldValue, final V newValue) { + Validate.notNull(key, "Null key"); + Validate.notNull(oldValue, "Null oldValue"); + Validate.notNull(newValue, "Null newValue"); + + final TableEntry entry = this.getEntryForPlain(key); + if (entry == null) { + return false; + } + + final V currValue = entry.getValuePlain(); + if (currValue == oldValue || currValue.equals(oldValue)) { + entry.setValueRelease(newValue); + return true; + } + + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public V replace(final K key, final V value) { + Validate.notNull(key, "Null key"); + Validate.notNull(value, "Null value"); + + final TableEntry entry = this.getEntryForPlain(key); + if (entry == null) { + return null; + } + + final V prev = entry.getValuePlain(); + entry.setValueRelease(value); + return prev; + } + + /** + * {@inheritDoc} + */ + @Override + public void replaceAll(final BiFunction 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) { + setAtIndexRelease(table, index, insert); + } else { + prev.setNextRelease(insert); + } + + this.addToSize(1); + + return newVal; + } + + if (curr.hash == hash && (curr.key == key || curr.key.equals(key))) { + final V newVal = remappingFunction.apply(key, curr.getValuePlain()); + + if (newVal != null) { + curr.setValueRelease(newVal); + return newVal; + } + + if (prev == null) { + setAtIndexRelease(table, index, curr.getNextPlain()); + } else { + prev.setNextRelease(curr.getNextPlain()); + } + + this.removeFromSize(1); + + return null; + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public V computeIfPresent(final K key, final BiFunction remappingFunction) { + Validate.notNull(key, "Null key"); + Validate.notNull(remappingFunction, "Null remappingFunction"); + + final int hash = SWMRHashTable.getHash(key); + final TableEntry[] table = this.getTablePlain(); + final int index = hash & (table.length - 1); + + for (TableEntry curr = table[index], prev = null; curr != null; prev = curr, curr = curr.getNextPlain()) { + if (curr.hash != hash || (curr.key != key && !curr.key.equals(key))) { + continue; + } + + final V newVal = remappingFunction.apply(key, curr.getValuePlain()); + if (newVal != null) { + curr.setValueRelease(newVal); + return newVal; + } + + if (prev == null) { + setAtIndexRelease(table, index, curr.getNextPlain()); + } else { + prev.setNextRelease(curr.getNextPlain()); + } + + this.removeFromSize(1); + + return null; + } + + return null; + } + + /** + * {@inheritDoc} + */ + @Override + public V computeIfAbsent(final K key, final Function mappingFunction) { + Validate.notNull(key, "Null key"); + Validate.notNull(mappingFunction, "Null mappingFunction"); + + final int hash = SWMRHashTable.getHash(key); + final TableEntry[] table = this.getTablePlain(); + final int index = hash & (table.length - 1); + + for (TableEntry curr = table[index], prev = null;;prev = curr, curr = curr.getNextPlain()) { + if (curr != null) { + if (curr.hash == hash && (curr.key == key || curr.key.equals(key))) { + return curr.getValuePlain(); + } + continue; + } + + final V newVal = mappingFunction.apply(key); + + if (newVal == null) { + return null; + } + + final TableEntry insert = new TableEntry<>(hash, key, newVal); + if (prev == null) { + setAtIndexRelease(table, index, insert); + } else { + prev.setNextRelease(insert); + } + + this.addToSize(1); + + return newVal; + } + } + + /** + * {@inheritDoc} + */ + @Override + public V merge(final K key, final V value, final BiFunction remappingFunction) { + Validate.notNull(key, "Null key"); + Validate.notNull(value, "Null value"); + Validate.notNull(remappingFunction, "Null remappingFunction"); + + final int hash = SWMRHashTable.getHash(key); + final TableEntry[] table = this.getTablePlain(); + final int index = hash & (table.length - 1); + + for (TableEntry curr = table[index], prev = null;;prev = curr, curr = curr.getNextPlain()) { + if (curr == null) { + final TableEntry insert = new TableEntry<>(hash, key, value); + if (prev == null) { + setAtIndexRelease(table, index, insert); + } else { + prev.setNextRelease(insert); + } + + this.addToSize(1); + + return value; + } + + if (curr.hash == hash && (curr.key == key || curr.key.equals(key))) { + final V newVal = remappingFunction.apply(curr.getValuePlain(), value); + + if (newVal != null) { + curr.setValueRelease(newVal); + return newVal; + } + + if (prev == null) { + setAtIndexRelease(table, index, curr.getNextPlain()); + } else { + prev.setNextRelease(curr.getNextPlain()); + } + + this.removeFromSize(1); + + return null; + } + } + } + + protected static final class TableEntry implements Map.Entry { + + protected static final VarHandle TABLE_ENTRY_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(TableEntry[].class); + + protected final int hash; + protected final K key; + protected V value; + + protected TableEntry 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; + } + + @Override + public K getKey() { + return this.key; + } + + @Override + public V getValue() { + return this.getValueAcquire(); + } + + @Override + public V setValue(final V value) { + throw new UnsupportedOperationException(); + } + + protected static int hash(final Object key, final Object value) { + return key.hashCode() ^ (value == null ? 0 : value.hashCode()); + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return hash(this.key, this.getValueAcquire()); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + + if (!(obj instanceof Map.Entry other)) { + return false; + } + final Object otherKey = other.getKey(); + final Object otherValue = other.getValue(); + + final K thisKey = this.getKey(); + final V thisVal = this.getValueAcquire(); + return (thisKey == otherKey || thisKey.equals(otherKey)) && + (thisVal == otherValue || thisVal.equals(otherValue)); + } + } + + + protected static abstract class TableEntryIterator 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 = getAtIndexOpaque(table, tableIndex); + if (entry != null) { + this.nextEntry = entry; + this.tableIndex = tableIndex + 1; + return; + } + } + this.tableIndex = tableIndex; + } + + @Override + public boolean hasNext() { + return this.nextEntry != null; + } + + protected final TableEntry 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 = getAtIndexOpaque(table, tableIndex); + if (next != null) { + this.nextEntry = next; + this.tableIndex = tableIndex + 1; + return curr; + } + } + + this.nextEntry = null; + this.tableIndex = tableIndex; + return curr; + } + + @Override + public void remove() { + final TableEntry 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 entry)) { + return false; + } + + final Object key; + final Object value; + + try { + key = entry.getKey(); + value = entry.getValue(); + } catch (final IllegalStateException ex) { + return false; + } + + return this.map.remove(key, value); + } + + @Override + public boolean removeIf(final Predicate> 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 entry)) { + return false; + } + + final Object key; + final Object value; + + try { + key = entry.getKey(); + value = entry.getValue(); + } catch (final IllegalStateException ex) { + return false; + } + + return this.map.contains(key, value); + } + + @Override + public String toString() { + return CollectionUtil.toString(this, "SWMRHashTableEntrySet"); + } + } + + protected static final class KeySet 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..bb301a9f4e3ac919552eef68afc73569d50674db --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRLong2ObjectHashTable.java @@ -0,0 +1,674 @@ +package ca.spottedleaf.concurrentutil.map; + +import ca.spottedleaf.concurrentutil.function.BiLongObjectConsumer; +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.concurrentutil.util.HashUtil; +import ca.spottedleaf.concurrentutil.util.IntegerUtil; +import ca.spottedleaf.concurrentutil.util.Validate; +import java.lang.invoke.VarHandle; +import java.util.Arrays; +import java.util.function.Consumer; +import java.util.function.LongConsumer; + +// trimmed down version of SWMRHashTable +public class SWMRLong2ObjectHashTable { + + 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); + } + + protected static TableEntry getAtIndexOpaque(final TableEntry[] table, final int index) { + // noinspection unchecked + return (TableEntry)TableEntry.TABLE_ENTRY_ARRAY_HANDLE.getOpaque(table, index); + } + + protected static void setAtIndexRelease(final TableEntry[] table, final int index, final TableEntry value) { + TableEntry.TABLE_ENTRY_ARRAY_HANDLE.setRelease(table, index, value); + } + + public final float getLoadFactor() { + return this.loadFactor; + } + + protected static int getCapacityFor(final int capacity) { + if (capacity <= 0) { + throw new IllegalArgumentException("Invalid capacity: " + capacity); + } + if (capacity >= MAXIMUM_CAPACITY) { + return MAXIMUM_CAPACITY; + } + return IntegerUtil.roundCeilLog2(capacity); + } + + /** Callers must still use acquire when reading the value of the entry. */ + protected final TableEntry getEntryForOpaque(final long key) { + final int hash = SWMRLong2ObjectHashTable.getHash(key); + final TableEntry[] table = this.getTableAcquire(); + + for (TableEntry curr = getAtIndexOpaque(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)HashUtil.mix(key); + } + + // rets -1 if capacity*loadFactor is too large + protected static int getTargetCapacity(final int capacity, final float loadFactor) { + final double ret = (double)capacity * (double)loadFactor; + if (Double.isInfinite(ret) || ret >= ((double)Integer.MAX_VALUE)) { + return -1; + } + + return (int)ret; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + /* Make no attempt to deal with concurrent modifications */ + if (!(obj instanceof SWMRLong2ObjectHashTable other)) { + return false; + } + + if (this.size() != other.size()) { + return false; + } + + final TableEntry[] table = this.getTableAcquire(); + + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + final V value = curr.getValueAcquire(); + + final Object otherValue = other.get(curr.key); + if (otherValue == null || (value != otherValue && value.equals(otherValue))) { + return false; + } + } + } + + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + /* Make no attempt to deal with concurrent modifications */ + int hash = 0; + final TableEntry[] table = this.getTableAcquire(); + + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + hash += curr.hashCode(); + } + } + + return hash; + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(64); + builder.append("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 = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + action.accept(curr); + } + } + } + + /** + * {@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 = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + final V value = curr.getValueAcquire(); + + action.accept(curr.key, value); + } + } + } + + /** + * Provides the specified consumer with all keys contained within this map. + * @param action The specified consumer. + */ + public void forEachKey(final LongConsumer action) { + Validate.notNull(action, "Null action"); + + final TableEntry[] table = this.getTableAcquire(); + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + action.accept(curr.key); + } + } + } + + /** + * Provides the specified consumer with all values contained within this map. Equivalent to {@code map.values().forEach(Consumer)}. + * @param action The specified consumer. + */ + public void forEachValue(final Consumer action) { + Validate.notNull(action, "Null action"); + + final TableEntry[] table = this.getTableAcquire(); + for (int i = 0, len = table.length; i < len; ++i) { + for (TableEntry curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) { + final V value = curr.getValueAcquire(); + + action.accept(value); + } + } + } + + /** + * {@inheritDoc} + */ + 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); + setAtIndexRelease(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) { + setAtIndexRelease(table, index, head.getNextPlain()); + this.removeFromSize(1); + + return head.getValuePlain(); + } + + for (TableEntry curr = head.getNextPlain(), prev = head; curr != null; prev = curr, curr = curr.getNextPlain()) { + if (key == curr.key) { + prev.setNextRelease(curr.getNextPlain()); + this.removeFromSize(1); + + return curr.getValuePlain(); + } + } + + return null; + } + + protected final V remove(final long key, final int hash, final V expect) { + final TableEntry[] table = this.getTablePlain(); + final int index = (table.length - 1) & hash; + + final TableEntry head = table[index]; + if (head == null) { + return null; + } + + if (head.key == key) { + final V val = head.value; + if (val == expect || val.equals(expect)) { + setAtIndexRelease(table, index, head.getNextPlain()); + this.removeFromSize(1); + + return head.getValuePlain(); + } else { + return null; + } + } + + for (TableEntry curr = head.getNextPlain(), prev = head; curr != null; prev = curr, curr = curr.getNextPlain()) { + if (key == curr.key) { + final V val = curr.value; + if (val == expect || val.equals(expect)) { + prev.setNextRelease(curr.getNextPlain()); + this.removeFromSize(1); + + return curr.getValuePlain(); + } else { + return null; + } + } + } + + return null; + } + + /** + * {@inheritDoc} + */ + public V remove(final long key) { + return this.remove(key, SWMRLong2ObjectHashTable.getHash(key)); + } + + public boolean remove(final long key, final V expect) { + return this.remove(key, SWMRLong2ObjectHashTable.getHash(key), expect) != null; + } + + /** + * {@inheritDoc} + */ + public void putAll(final SWMRLong2ObjectHashTable 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 static final VarHandle TABLE_ENTRY_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(TableEntry[].class); + + 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(); + } + } +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/scheduler/SchedulerThreadPool.java b/src/main/java/ca/spottedleaf/concurrentutil/scheduler/SchedulerThreadPool.java new file mode 100644 index 0000000000000000000000000000000000000000..8197ccb1c4e5878dbd8007b5fb514640765ec8e4 --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/scheduler/SchedulerThreadPool.java @@ -0,0 +1,558 @@ +package ca.spottedleaf.concurrentutil.scheduler; + +import ca.spottedleaf.concurrentutil.set.LinkedSortedSet; +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.concurrentutil.util.TimeUtil; +import java.lang.invoke.VarHandle; +import java.util.BitSet; +import java.util.Comparator; +import java.util.PriorityQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.LockSupport; +import java.util.function.BooleanSupplier; + +public class SchedulerThreadPool { + + public static final long DEADLINE_NOT_SET = Long.MIN_VALUE; + + private static final Comparator TICK_COMPARATOR_BY_TIME = (final SchedulableTick t1, final SchedulableTick t2) -> { + final int timeCompare = TimeUtil.compareTimes(t1.scheduledStart, t2.scheduledStart); + if (timeCompare != 0) { + return timeCompare; + } + + return Long.compare(t1.id, t2.id); + }; + + private final TickThreadRunner[] runners; + private final Thread[] threads; + private final LinkedSortedSet awaiting = new LinkedSortedSet<>(TICK_COMPARATOR_BY_TIME); + private final PriorityQueue queued = new PriorityQueue<>(TICK_COMPARATOR_BY_TIME); + private final BitSet idleThreads; + + private final Object scheduleLock = new Object(); + + private volatile boolean halted; + + /** + * Creates, but does not start, a scheduler thread pool with the specified number of threads + * created using the specified thread factory. + * @param threads Specified number of threads + * @param threadFactory Specified thread factory + * @see #start() + */ + public SchedulerThreadPool(final int threads, final ThreadFactory threadFactory) { + final BitSet idleThreads = new BitSet(threads); + for (int i = 0; i < threads; ++i) { + idleThreads.set(i); + } + this.idleThreads = idleThreads; + + final TickThreadRunner[] runners = new TickThreadRunner[threads]; + final Thread[] t = new Thread[threads]; + for (int i = 0; i < threads; ++i) { + runners[i] = new TickThreadRunner(i, this); + t[i] = threadFactory.newThread(runners[i]); + } + + this.threads = t; + this.runners = runners; + } + + /** + * Starts the threads in this pool. + */ + public void start() { + for (final Thread thread : this.threads) { + thread.start(); + } + } + + /** + * Attempts to prevent further execution of tasks, optionally waiting for the scheduler threads to die. + * + * @param sync Whether to wait for the scheduler threads to die. + * @param maxWaitNS The maximum time, in ns, to wait for the scheduler threads to die. + * @return {@code true} if sync was false, or if sync was true and the scheduler threads died before the timeout. + * Otherwise, returns {@code false} if the time elapsed exceeded the maximum wait time. + */ + public boolean halt(final boolean sync, final long maxWaitNS) { + this.halted = true; + for (final Thread thread : this.threads) { + // force response to halt + LockSupport.unpark(thread); + } + final long time = System.nanoTime(); + if (sync) { + // start at 10 * 0.5ms -> 5ms + for (long failures = 9L;; failures = ConcurrentUtil.linearLongBackoff(failures, 500_000L, 50_000_000L)) { + boolean allDead = true; + for (final Thread thread : this.threads) { + if (thread.isAlive()) { + allDead = false; + break; + } + } + if (allDead) { + return true; + } + if ((System.nanoTime() - time) >= maxWaitNS) { + return false; + } + } + } + + return true; + } + + /** + * Returns an array of the underlying scheduling threads. + */ + public Thread[] getThreads() { + return this.threads.clone(); + } + + private void insertFresh(final SchedulableTick task) { + final TickThreadRunner[] runners = this.runners; + + final int firstIdleThread = this.idleThreads.nextSetBit(0); + + if (firstIdleThread != -1) { + // push to idle thread + this.idleThreads.clear(firstIdleThread); + final TickThreadRunner runner = runners[firstIdleThread]; + task.awaitingLink = this.awaiting.addLast(task); + runner.acceptTask(task); + return; + } + + // try to replace the last awaiting task + final SchedulableTick last = this.awaiting.last(); + + if (last != null && TICK_COMPARATOR_BY_TIME.compare(task, last) < 0) { + // need to replace the last task + this.awaiting.pollLast(); + last.awaitingLink = null; + task.awaitingLink = this.awaiting.addLast(task); + // need to add task to queue to be picked up later + this.queued.add(last); + + final TickThreadRunner runner = last.ownedBy; + runner.replaceTask(task); + + return; + } + + // add to queue, will be picked up later + this.queued.add(task); + } + + private void takeTask(final TickThreadRunner runner, final SchedulableTick tick) { + if (!this.awaiting.remove(tick.awaitingLink)) { + throw new IllegalStateException("Task is not in awaiting"); + } + tick.awaitingLink = null; + } + + private SchedulableTick returnTask(final TickThreadRunner runner, final SchedulableTick reschedule) { + if (reschedule != null) { + this.queued.add(reschedule); + } + final SchedulableTick ret = this.queued.poll(); + if (ret == null) { + this.idleThreads.set(runner.id); + } else { + ret.awaitingLink = this.awaiting.addLast(ret); + } + + return ret; + } + + /** + * Schedules the specified task to be executed on this thread pool. + * @param task Specified task + * @throws IllegalStateException If the task is already scheduled + * @see SchedulableTick + */ + public void schedule(final SchedulableTick task) { + synchronized (this.scheduleLock) { + if (!task.tryMarkScheduled()) { + throw new IllegalStateException("Task " + task + " is already scheduled or cancelled"); + } + + task.schedulerOwnedBy = this; + + this.insertFresh(task); + } + } + + /** + * Updates the tasks scheduled start to the maximum of its current scheduled start and the specified + * new start. If the task is not scheduled, returns {@code false}. Otherwise, returns whether the + * scheduled start was updated. Undefined behavior of the specified task is scheduled in another executor. + * @param task Specified task + * @param newStart Specified new start + */ + public boolean updateTickStartToMax(final SchedulableTick task, final long newStart) { + synchronized (this.scheduleLock) { + if (TimeUtil.compareTimes(newStart, task.getScheduledStart()) <= 0) { + return false; + } + if (this.queued.remove(task)) { + task.setScheduledStart(newStart); + this.queued.add(task); + return true; + } + if (task.awaitingLink != null) { + this.awaiting.remove(task.awaitingLink); + task.awaitingLink = null; + + // re-queue task + task.setScheduledStart(newStart); + this.queued.add(task); + + // now we need to replace the task the runner was waiting for + final TickThreadRunner runner = task.ownedBy; + final SchedulableTick replace = this.queued.poll(); + + // replace cannot be null, since we have added a task to queued + if (replace != task) { + runner.replaceTask(replace); + } + + return true; + } + + return false; + } + } + + /** + * Returns {@code null} if the task is not scheduled, returns {@code TRUE} if the task was cancelled + * and was queued to execute, returns {@code FALSE} if the task was cancelled but was executing. + */ + public Boolean tryRetire(final SchedulableTick task) { + if (task.schedulerOwnedBy != this) { + return null; + } + + synchronized (this.scheduleLock) { + if (this.queued.remove(task)) { + // cancelled, and no runner owns it - so return + return Boolean.TRUE; + } + if (task.awaitingLink != null) { + this.awaiting.remove(task.awaitingLink); + task.awaitingLink = null; + // here we need to replace the task the runner was waiting for + final TickThreadRunner runner = task.ownedBy; + final SchedulableTick replace = this.queued.poll(); + + if (replace == null) { + // nothing to replace with, set to idle + this.idleThreads.set(runner.id); + runner.forceIdle(); + } else { + runner.replaceTask(replace); + } + + return Boolean.TRUE; + } + + // could not find it in queue + return task.tryMarkCancelled() ? Boolean.FALSE : null; + } + } + + /** + * Indicates that intermediate tasks are available to be executed by the task. + *

+ * Note: currently a no-op + *

+ * @param task The specified task + * @see SchedulableTick + */ + public void notifyTasks(final SchedulableTick task) { + // Not implemented + } + + /** + * Represents a tickable task that can be scheduled into a {@link SchedulerThreadPool}. + *

+ * A tickable task is expected to run on a fixed interval, which is determined by + * the {@link SchedulerThreadPool}. + *

+ *

+ * A tickable task can have intermediate tasks that can be executed before its tick method is ran. Instead of + * the {@link SchedulerThreadPool} parking in-between ticks, the scheduler will instead drain + * intermediate tasks from scheduled tasks. The parsing of intermediate tasks allows the scheduler to take + * advantage of downtime to reduce the intermediate task load from tasks once they begin ticking. + *

+ *

+ * It is guaranteed that {@link #runTick()} and {@link #runTasks(BooleanSupplier)} are never + * invoked in parallel. + * It is required that when intermediate tasks are scheduled, that {@link SchedulerThreadPool#notifyTasks(SchedulableTick)} + * is invoked for any scheduled task - otherwise, {@link #runTasks(BooleanSupplier)} may not be invoked to + * parse intermediate tasks. + *

+ */ + public static abstract class SchedulableTick { + private static final AtomicLong ID_GENERATOR = new AtomicLong(); + public final long id = ID_GENERATOR.getAndIncrement(); + + private static final int SCHEDULE_STATE_NOT_SCHEDULED = 0; + private static final int SCHEDULE_STATE_SCHEDULED = 1; + private static final int SCHEDULE_STATE_CANCELLED = 2; + + private final AtomicInteger scheduled = new AtomicInteger(); + private SchedulerThreadPool schedulerOwnedBy; + private long scheduledStart = DEADLINE_NOT_SET; + private TickThreadRunner ownedBy; + + private LinkedSortedSet.Link awaitingLink; + + private boolean tryMarkScheduled() { + return this.scheduled.compareAndSet(SCHEDULE_STATE_NOT_SCHEDULED, SCHEDULE_STATE_SCHEDULED); + } + + private boolean tryMarkCancelled() { + return this.scheduled.compareAndSet(SCHEDULE_STATE_SCHEDULED, SCHEDULE_STATE_CANCELLED); + } + + private boolean isScheduled() { + return this.scheduled.get() == SCHEDULE_STATE_SCHEDULED; + } + + protected final long getScheduledStart() { + return this.scheduledStart; + } + + /** + * If this task is scheduled, then this may only be invoked during {@link #runTick()}, + * and {@link #runTasks(BooleanSupplier)} + */ + protected final void setScheduledStart(final long value) { + this.scheduledStart = value; + } + + /** + * Executes the tick. + *

+ * It is the callee's responsibility to invoke {@link #setScheduledStart(long)} to adjust the start of + * the next tick. + *

+ * @return {@code true} if the task should continue to be scheduled, {@code false} otherwise. + */ + public abstract boolean runTick(); + + /** + * Returns whether this task has any intermediate tasks that can be executed. + */ + public abstract boolean hasTasks(); + + /** + * Returns {@code null} if this task should not be scheduled, otherwise returns + * {@code Boolean.TRUE} if there are more intermediate tasks to execute and + * {@code Boolean.FALSE} if there are no more intermediate tasks to execute. + */ + public abstract Boolean runTasks(final BooleanSupplier canContinue); + + @Override + public String toString() { + return "SchedulableTick:{" + + "class=" + this.getClass().getName() + "," + + "scheduled_state=" + this.scheduled.get() + "," + + "}"; + } + } + + private static final class TickThreadRunner implements Runnable { + + /** + * There are no tasks in this thread's runqueue, so it is parked. + *

+ * stateTarget = null + *

+ */ + private static final int STATE_IDLE = 0; + + /** + * The runner is waiting to tick a task, as it has no intermediate tasks to execute. + *

+ * stateTarget = the task awaiting tick + *

+ */ + private static final int STATE_AWAITING_TICK = 1; + + /** + * The runner is executing a tick for one of the tasks that was in its runqueue. + *

+ * stateTarget = the task being ticked + *

+ */ + private static final int STATE_EXECUTING_TICK = 2; + + public final int id; + public final SchedulerThreadPool scheduler; + + private volatile Thread thread; + private volatile TickThreadRunnerState state = new TickThreadRunnerState(null, STATE_IDLE); + private static final VarHandle STATE_HANDLE = ConcurrentUtil.getVarHandle(TickThreadRunner.class, "state", TickThreadRunnerState.class); + + private void setStatePlain(final TickThreadRunnerState state) { + STATE_HANDLE.set(this, state); + } + + private void setStateOpaque(final TickThreadRunnerState state) { + STATE_HANDLE.setOpaque(this, state); + } + + private void setStateVolatile(final TickThreadRunnerState state) { + STATE_HANDLE.setVolatile(this, state); + } + + private static record TickThreadRunnerState(SchedulableTick stateTarget, int state) {} + + public TickThreadRunner(final int id, final SchedulerThreadPool scheduler) { + this.id = id; + this.scheduler = scheduler; + } + + private Thread getRunnerThread() { + return this.thread; + } + + private void acceptTask(final SchedulableTick task) { + if (task.ownedBy != null) { + throw new IllegalStateException("Already owned by another runner"); + } + task.ownedBy = this; + final TickThreadRunnerState state = this.state; + if (state.state != STATE_IDLE) { + throw new IllegalStateException("Cannot accept task in state " + state); + } + this.setStateVolatile(new TickThreadRunnerState(task, STATE_AWAITING_TICK)); + LockSupport.unpark(this.getRunnerThread()); + } + + private void replaceTask(final SchedulableTick task) { + final TickThreadRunnerState state = this.state; + if (state.state != STATE_AWAITING_TICK) { + throw new IllegalStateException("Cannot replace task in state " + state); + } + if (task.ownedBy != null) { + throw new IllegalStateException("Already owned by another runner"); + } + task.ownedBy = this; + + state.stateTarget.ownedBy = null; + + this.setStateVolatile(new TickThreadRunnerState(task, STATE_AWAITING_TICK)); + LockSupport.unpark(this.getRunnerThread()); + } + + private void forceIdle() { + final TickThreadRunnerState state = this.state; + if (state.state != STATE_AWAITING_TICK) { + throw new IllegalStateException("Cannot replace task in state " + state); + } + state.stateTarget.ownedBy = null; + this.setStateOpaque(new TickThreadRunnerState(null, STATE_IDLE)); + // no need to unpark + } + + private boolean takeTask(final TickThreadRunnerState state, final SchedulableTick task) { + synchronized (this.scheduler.scheduleLock) { + if (this.state != state) { + return false; + } + this.setStatePlain(new TickThreadRunnerState(task, STATE_EXECUTING_TICK)); + this.scheduler.takeTask(this, task); + return true; + } + } + + private void returnTask(final SchedulableTick task, final boolean reschedule) { + synchronized (this.scheduler.scheduleLock) { + task.ownedBy = null; + + final SchedulableTick newWait = this.scheduler.returnTask(this, reschedule && task.isScheduled() ? task : null); + if (newWait == null) { + this.setStatePlain(new TickThreadRunnerState(null, STATE_IDLE)); + } else { + if (newWait.ownedBy != null) { + throw new IllegalStateException("Already owned by another runner"); + } + newWait.ownedBy = this; + this.setStatePlain(new TickThreadRunnerState(newWait, STATE_AWAITING_TICK)); + } + } + } + + @Override + public void run() { + this.thread = Thread.currentThread(); + + main_state_loop: + for (;;) { + final TickThreadRunnerState startState = this.state; + final int startStateType = startState.state; + final SchedulableTick startStateTask = startState.stateTarget; + + if (this.scheduler.halted) { + return; + } + + switch (startStateType) { + case STATE_IDLE: { + while (this.state.state == STATE_IDLE) { + LockSupport.park(); + if (this.scheduler.halted) { + return; + } + } + continue main_state_loop; + } + + case STATE_AWAITING_TICK: { + final long deadline = startStateTask.getScheduledStart(); + for (;;) { + if (this.state != startState) { + continue main_state_loop; + } + final long diff = deadline - System.nanoTime(); + if (diff <= 0L) { + break; + } + LockSupport.parkNanos(startState, diff); + if (this.scheduler.halted) { + return; + } + } + + if (!this.takeTask(startState, startStateTask)) { + continue main_state_loop; + } + + // TODO exception handling + final boolean reschedule = startStateTask.runTick(); + + this.returnTask(startStateTask, reschedule); + + continue main_state_loop; + } + + case STATE_EXECUTING_TICK: { + throw new IllegalStateException("Tick execution must be set by runner thread, not by any other thread"); + } + + default: { + throw new IllegalStateException("Unknown state: " + startState); + } + } + } + } + } +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/set/LinkedSortedSet.java b/src/main/java/ca/spottedleaf/concurrentutil/set/LinkedSortedSet.java new file mode 100644 index 0000000000000000000000000000000000000000..212bc9ae2fc7d37d4a089a2921b00de1e97f7cc1 --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/set/LinkedSortedSet.java @@ -0,0 +1,272 @@ +package ca.spottedleaf.concurrentutil.set; + +import java.util.Comparator; +import java.util.Iterator; +import java.util.NoSuchElementException; + +public final class LinkedSortedSet implements Iterable { + + public final Comparator comparator; + + protected Link head; + protected Link tail; + + public LinkedSortedSet() { + this((Comparator)Comparator.naturalOrder()); + } + + public LinkedSortedSet(final Comparator comparator) { + this.comparator = comparator; + } + + public void clear() { + this.head = this.tail = null; + } + + public boolean isEmpty() { + return this.head == null; + } + + public E first() { + final Link head = this.head; + return head == null ? null : head.element; + } + + public E last() { + final Link tail = this.tail; + return tail == null ? null : tail.element; + } + + public boolean containsFirst(final E element) { + final Comparator comparator = this.comparator; + for (Link curr = this.head; curr != null; curr = curr.next) { + if (comparator.compare(element, curr.element) == 0) { + return true; + } + } + return false; + } + + public boolean containsLast(final E element) { + final Comparator comparator = this.comparator; + for (Link curr = this.tail; curr != null; curr = curr.prev) { + if (comparator.compare(element, curr.element) == 0) { + return true; + } + } + return false; + } + + private void removeNode(final Link node) { + final Link prev = node.prev; + final Link next = node.next; + + // help GC + node.element = null; + node.prev = null; + node.next = null; + + if (prev == null) { + this.head = next; + } else { + prev.next = next; + } + + if (next == null) { + this.tail = prev; + } else { + next.prev = prev; + } + } + + public boolean remove(final Link link) { + if (link.element == null) { + return false; + } + + this.removeNode(link); + return true; + } + + public boolean removeFirst(final E element) { + final Comparator comparator = this.comparator; + for (Link curr = this.head; curr != null; curr = curr.next) { + if (comparator.compare(element, curr.element) == 0) { + this.removeNode(curr); + return true; + } + } + return false; + } + + public boolean removeLast(final E element) { + final Comparator comparator = this.comparator; + for (Link curr = this.tail; curr != null; curr = curr.prev) { + if (comparator.compare(element, curr.element) == 0) { + this.removeNode(curr); + return true; + } + } + return false; + } + + @Override + public Iterator iterator() { + return new Iterator<>() { + private Link next = LinkedSortedSet.this.head; + + @Override + public boolean hasNext() { + return this.next != null; + } + + @Override + public E next() { + final Link next = this.next; + if (next == null) { + throw new NoSuchElementException(); + } + this.next = next.next; + return next.element; + } + }; + } + + public E pollFirst() { + final Link head = this.head; + if (head == null) { + return null; + } + + final E ret = head.element; + final Link next = head.next; + + // unlink head + this.head = next; + if (next == null) { + this.tail = null; + } else { + next.prev = null; + } + + // help GC + head.element = null; + head.next = null; + + return ret; + } + + public E pollLast() { + final Link tail = this.tail; + if (tail == null) { + return null; + } + + final E ret = tail.element; + final Link prev = tail.prev; + + // unlink tail + this.tail = prev; + if (prev == null) { + this.head = null; + } else { + prev.next = null; + } + + // help GC + tail.element = null; + tail.prev = null; + + return ret; + } + + public Link addLast(final E element) { + final Comparator comparator = this.comparator; + + Link curr = this.tail; + if (curr != null) { + int compare; + + while ((compare = comparator.compare(element, curr.element)) < 0) { + Link prev = curr; + curr = curr.prev; + if (curr != null) { + continue; + } + return this.head = prev.prev = new Link<>(element, null, prev); + } + + if (compare != 0) { + // insert after curr + final Link next = curr.next; + final Link insert = new Link<>(element, curr, next); + curr.next = insert; + + if (next == null) { + this.tail = insert; + } else { + next.prev = insert; + } + return insert; + } + + return null; + } else { + return this.head = this.tail = new Link<>(element); + } + } + + public Link addFirst(final E element) { + final Comparator comparator = this.comparator; + + Link curr = this.head; + if (curr != null) { + int compare; + + while ((compare = comparator.compare(element, curr.element)) > 0) { + Link prev = curr; + curr = curr.next; + if (curr != null) { + continue; + } + return this.tail = prev.next = new Link<>(element, prev, null); + } + + if (compare != 0) { + // insert before curr + final Link prev = curr.prev; + final Link insert = new Link<>(element, prev, curr); + curr.prev = insert; + + if (prev == null) { + this.head = insert; + } else { + prev.next = insert; + } + return insert; + } + + return null; + } else { + return this.head = this.tail = new Link<>(element); + } + } + + public static final class Link { + private E element; + private Link prev; + private Link next; + + private Link() {} + + private Link(final E element) { + this.element = element; + } + + private Link(final E element, final Link prev, final Link next) { + this.element = element; + this.prev = prev; + this.next = next; + } + } +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/ArrayUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/ArrayUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..ebb1ab06165addb173fea4d295001fe37f4e79d3 --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/util/ArrayUtil.java @@ -0,0 +1,816 @@ +package ca.spottedleaf.concurrentutil.util; + +import java.lang.invoke.VarHandle; + +public final class ArrayUtil { + + public static final VarHandle BOOLEAN_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(boolean[].class); + + public static final VarHandle BYTE_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(byte[].class); + + public static final VarHandle SHORT_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(short[].class); + + public static final VarHandle INT_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(int[].class); + + public static final VarHandle LONG_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(long[].class); + + public static final VarHandle OBJECT_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(Object[].class); + + private ArrayUtil() { + throw new RuntimeException(); + } + + /* byte array */ + + public static byte getPlain(final byte[] array, final int index) { + return (byte)BYTE_ARRAY_HANDLE.get(array, index); + } + + public static byte getOpaque(final byte[] array, final int index) { + return (byte)BYTE_ARRAY_HANDLE.getOpaque(array, index); + } + + public static byte getAcquire(final byte[] array, final int index) { + return (byte)BYTE_ARRAY_HANDLE.getAcquire(array, index); + } + + public static byte getVolatile(final byte[] array, final int index) { + return (byte)BYTE_ARRAY_HANDLE.getVolatile(array, index); + } + + public static void setPlain(final byte[] array, final int index, final byte value) { + BYTE_ARRAY_HANDLE.set(array, index, value); + } + + public static void setOpaque(final byte[] array, final int index, final byte value) { + BYTE_ARRAY_HANDLE.setOpaque(array, index, value); + } + + public static void setRelease(final byte[] array, final int index, final byte value) { + BYTE_ARRAY_HANDLE.setRelease(array, index, value); + } + + public static void setVolatile(final byte[] array, final int index, final byte value) { + BYTE_ARRAY_HANDLE.setVolatile(array, index, value); + } + + public static void setVolatileContended(final byte[] array, final int index, final byte param) { + int failures = 0; + + for (byte curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { + return; + } + } + } + + public static byte compareAndExchangeVolatile(final byte[] array, final int index, final byte expect, final byte update) { + return (byte)BYTE_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); + } + + public static byte getAndAddVolatile(final byte[] array, final int index, final byte param) { + return (byte)BYTE_ARRAY_HANDLE.getAndAdd(array, index, param); + } + + public static byte getAndAndVolatile(final byte[] array, final int index, final byte param) { + return (byte)BYTE_ARRAY_HANDLE.getAndBitwiseAnd(array, index, param); + } + + public static byte getAndOrVolatile(final byte[] array, final int index, final byte param) { + return (byte)BYTE_ARRAY_HANDLE.getAndBitwiseOr(array, index, param); + } + + public static byte getAndXorVolatile(final byte[] array, final int index, final byte param) { + return (byte)BYTE_ARRAY_HANDLE.getAndBitwiseXor(array, index, param); + } + + public static byte getAndSetVolatile(final byte[] array, final int index, final byte param) { + return (byte)BYTE_ARRAY_HANDLE.getAndSet(array, index, param); + } + + public static byte compareAndExchangeVolatileContended(final byte[] array, final int index, final byte expect, final byte update) { + return (byte)BYTE_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); + } + + public static byte getAndAddVolatileContended(final byte[] array, final int index, final byte param) { + int failures = 0; + + for (byte curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (byte) (curr + param)))) { + return curr; + } + } + } + + public static byte getAndAndVolatileContended(final byte[] array, final int index, final byte param) { + int failures = 0; + + for (byte curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (byte) (curr & param)))) { + return curr; + } + } + } + + public static byte getAndOrVolatileContended(final byte[] array, final int index, final byte param) { + int failures = 0; + + for (byte curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (byte) (curr | param)))) { + return curr; + } + } + } + + public static byte getAndXorVolatileContended(final byte[] array, final int index, final byte param) { + int failures = 0; + + for (byte curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (byte) (curr ^ param)))) { + return curr; + } + } + } + + public static byte getAndSetVolatileContended(final byte[] array, final int index, final byte param) { + int failures = 0; + + for (byte curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { + return curr; + } + } + } + + /* short array */ + + public static short getPlain(final short[] array, final int index) { + return (short)SHORT_ARRAY_HANDLE.get(array, index); + } + + public static short getOpaque(final short[] array, final int index) { + return (short)SHORT_ARRAY_HANDLE.getOpaque(array, index); + } + + public static short getAcquire(final short[] array, final int index) { + return (short)SHORT_ARRAY_HANDLE.getAcquire(array, index); + } + + public static short getVolatile(final short[] array, final int index) { + return (short)SHORT_ARRAY_HANDLE.getVolatile(array, index); + } + + public static void setPlain(final short[] array, final int index, final short value) { + SHORT_ARRAY_HANDLE.set(array, index, value); + } + + public static void setOpaque(final short[] array, final int index, final short value) { + SHORT_ARRAY_HANDLE.setOpaque(array, index, value); + } + + public static void setRelease(final short[] array, final int index, final short value) { + SHORT_ARRAY_HANDLE.setRelease(array, index, value); + } + + public static void setVolatile(final short[] array, final int index, final short value) { + SHORT_ARRAY_HANDLE.setVolatile(array, index, value); + } + + public static void setVolatileContended(final short[] array, final int index, final short param) { + int failures = 0; + + for (short curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { + return; + } + } + } + + public static short compareAndExchangeVolatile(final short[] array, final int index, final short expect, final short update) { + return (short)SHORT_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); + } + + public static short getAndAddVolatile(final short[] array, final int index, final short param) { + return (short)SHORT_ARRAY_HANDLE.getAndAdd(array, index, param); + } + + public static short getAndAndVolatile(final short[] array, final int index, final short param) { + return (short)SHORT_ARRAY_HANDLE.getAndBitwiseAnd(array, index, param); + } + + public static short getAndOrVolatile(final short[] array, final int index, final short param) { + return (short)SHORT_ARRAY_HANDLE.getAndBitwiseOr(array, index, param); + } + + public static short getAndXorVolatile(final short[] array, final int index, final short param) { + return (short)SHORT_ARRAY_HANDLE.getAndBitwiseXor(array, index, param); + } + + public static short getAndSetVolatile(final short[] array, final int index, final short param) { + return (short)SHORT_ARRAY_HANDLE.getAndSet(array, index, param); + } + + public static short compareAndExchangeVolatileContended(final short[] array, final int index, final short expect, final short update) { + return (short)SHORT_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); + } + + public static short getAndAddVolatileContended(final short[] array, final int index, final short param) { + int failures = 0; + + for (short curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (short) (curr + param)))) { + return curr; + } + } + } + + public static short getAndAndVolatileContended(final short[] array, final int index, final short param) { + int failures = 0; + + for (short curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (short) (curr & param)))) { + return curr; + } + } + } + + public static short getAndOrVolatileContended(final short[] array, final int index, final short param) { + int failures = 0; + + for (short curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (short) (curr | param)))) { + return curr; + } + } + } + + public static short getAndXorVolatileContended(final short[] array, final int index, final short param) { + int failures = 0; + + for (short curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (short) (curr ^ param)))) { + return curr; + } + } + } + + public static short getAndSetVolatileContended(final short[] array, final int index, final short param) { + int failures = 0; + + for (short curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { + return curr; + } + } + } + + /* int array */ + + public static int getPlain(final int[] array, final int index) { + return (int)INT_ARRAY_HANDLE.get(array, index); + } + + public static int getOpaque(final int[] array, final int index) { + return (int)INT_ARRAY_HANDLE.getOpaque(array, index); + } + + public static int getAcquire(final int[] array, final int index) { + return (int)INT_ARRAY_HANDLE.getAcquire(array, index); + } + + public static int getVolatile(final int[] array, final int index) { + return (int)INT_ARRAY_HANDLE.getVolatile(array, index); + } + + public static void setPlain(final int[] array, final int index, final int value) { + INT_ARRAY_HANDLE.set(array, index, value); + } + + public static void setOpaque(final int[] array, final int index, final int value) { + INT_ARRAY_HANDLE.setOpaque(array, index, value); + } + + public static void setRelease(final int[] array, final int index, final int value) { + INT_ARRAY_HANDLE.setRelease(array, index, value); + } + + public static void setVolatile(final int[] array, final int index, final int value) { + INT_ARRAY_HANDLE.setVolatile(array, index, value); + } + + public static void setVolatileContended(final int[] array, final int index, final int param) { + int failures = 0; + + for (int curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { + return; + } + } + } + + public static int compareAndExchangeVolatile(final int[] array, final int index, final int expect, final int update) { + return (int)INT_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); + } + + public static int getAndAddVolatile(final int[] array, final int index, final int param) { + return (int)INT_ARRAY_HANDLE.getAndAdd(array, index, param); + } + + public static int getAndAndVolatile(final int[] array, final int index, final int param) { + return (int)INT_ARRAY_HANDLE.getAndBitwiseAnd(array, index, param); + } + + public static int getAndOrVolatile(final int[] array, final int index, final int param) { + return (int)INT_ARRAY_HANDLE.getAndBitwiseOr(array, index, param); + } + + public static int getAndXorVolatile(final int[] array, final int index, final int param) { + return (int)INT_ARRAY_HANDLE.getAndBitwiseXor(array, index, param); + } + + public static int getAndSetVolatile(final int[] array, final int index, final int param) { + return (int)INT_ARRAY_HANDLE.getAndSet(array, index, param); + } + + public static int compareAndExchangeVolatileContended(final int[] array, final int index, final int expect, final int update) { + return (int)INT_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); + } + + public static int getAndAddVolatileContended(final int[] array, final int index, final int param) { + int failures = 0; + + for (int curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (int) (curr + param)))) { + return curr; + } + } + } + + public static int getAndAndVolatileContended(final int[] array, final int index, final int param) { + int failures = 0; + + for (int curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (int) (curr & param)))) { + return curr; + } + } + } + + public static int getAndOrVolatileContended(final int[] array, final int index, final int param) { + int failures = 0; + + for (int curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (int) (curr | param)))) { + return curr; + } + } + } + + public static int getAndXorVolatileContended(final int[] array, final int index, final int param) { + int failures = 0; + + for (int curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (int) (curr ^ param)))) { + return curr; + } + } + } + + public static int getAndSetVolatileContended(final int[] array, final int index, final int param) { + int failures = 0; + + for (int curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { + return curr; + } + } + } + + /* long array */ + + public static long getPlain(final long[] array, final int index) { + return (long)LONG_ARRAY_HANDLE.get(array, index); + } + + public static long getOpaque(final long[] array, final int index) { + return (long)LONG_ARRAY_HANDLE.getOpaque(array, index); + } + + public static long getAcquire(final long[] array, final int index) { + return (long)LONG_ARRAY_HANDLE.getAcquire(array, index); + } + + public static long getVolatile(final long[] array, final int index) { + return (long)LONG_ARRAY_HANDLE.getVolatile(array, index); + } + + public static void setPlain(final long[] array, final int index, final long value) { + LONG_ARRAY_HANDLE.set(array, index, value); + } + + public static void setOpaque(final long[] array, final int index, final long value) { + LONG_ARRAY_HANDLE.setOpaque(array, index, value); + } + + public static void setRelease(final long[] array, final int index, final long value) { + LONG_ARRAY_HANDLE.setRelease(array, index, value); + } + + public static void setVolatile(final long[] array, final int index, final long value) { + LONG_ARRAY_HANDLE.setVolatile(array, index, value); + } + + public static void setVolatileContended(final long[] array, final int index, final long param) { + int failures = 0; + + for (long curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { + return; + } + } + } + + public static long compareAndExchangeVolatile(final long[] array, final int index, final long expect, final long update) { + return (long)LONG_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); + } + + public static long getAndAddVolatile(final long[] array, final int index, final long param) { + return (long)LONG_ARRAY_HANDLE.getAndAdd(array, index, param); + } + + public static long getAndAndVolatile(final long[] array, final int index, final long param) { + return (long)LONG_ARRAY_HANDLE.getAndBitwiseAnd(array, index, param); + } + + public static long getAndOrVolatile(final long[] array, final int index, final long param) { + return (long)LONG_ARRAY_HANDLE.getAndBitwiseOr(array, index, param); + } + + public static long getAndXorVolatile(final long[] array, final int index, final long param) { + return (long)LONG_ARRAY_HANDLE.getAndBitwiseXor(array, index, param); + } + + public static long getAndSetVolatile(final long[] array, final int index, final long param) { + return (long)LONG_ARRAY_HANDLE.getAndSet(array, index, param); + } + + public static long compareAndExchangeVolatileContended(final long[] array, final int index, final long expect, final long update) { + return (long)LONG_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); + } + + public static long getAndAddVolatileContended(final long[] array, final int index, final long param) { + int failures = 0; + + for (long curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (long) (curr + param)))) { + return curr; + } + } + } + + public static long getAndAndVolatileContended(final long[] array, final int index, final long param) { + int failures = 0; + + for (long curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (long) (curr & param)))) { + return curr; + } + } + } + + public static long getAndOrVolatileContended(final long[] array, final int index, final long param) { + int failures = 0; + + for (long curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (long) (curr | param)))) { + return curr; + } + } + } + + public static long getAndXorVolatileContended(final long[] array, final int index, final long param) { + int failures = 0; + + for (long curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (long) (curr ^ param)))) { + return curr; + } + } + } + + public static long getAndSetVolatileContended(final long[] array, final int index, final long param) { + int failures = 0; + + for (long curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { + return curr; + } + } + } + + /* boolean array */ + + public static boolean getPlain(final boolean[] array, final int index) { + return (boolean)BOOLEAN_ARRAY_HANDLE.get(array, index); + } + + public static boolean getOpaque(final boolean[] array, final int index) { + return (boolean)BOOLEAN_ARRAY_HANDLE.getOpaque(array, index); + } + + public static boolean getAcquire(final boolean[] array, final int index) { + return (boolean)BOOLEAN_ARRAY_HANDLE.getAcquire(array, index); + } + + public static boolean getVolatile(final boolean[] array, final int index) { + return (boolean)BOOLEAN_ARRAY_HANDLE.getVolatile(array, index); + } + + public static void setPlain(final boolean[] array, final int index, final boolean value) { + BOOLEAN_ARRAY_HANDLE.set(array, index, value); + } + + public static void setOpaque(final boolean[] array, final int index, final boolean value) { + BOOLEAN_ARRAY_HANDLE.setOpaque(array, index, value); + } + + public static void setRelease(final boolean[] array, final int index, final boolean value) { + BOOLEAN_ARRAY_HANDLE.setRelease(array, index, value); + } + + public static void setVolatile(final boolean[] array, final int index, final boolean value) { + BOOLEAN_ARRAY_HANDLE.setVolatile(array, index, value); + } + + public static void setVolatileContended(final boolean[] array, final int index, final boolean param) { + int failures = 0; + + for (boolean curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { + return; + } + } + } + + public static boolean compareAndExchangeVolatile(final boolean[] array, final int index, final boolean expect, final boolean update) { + return (boolean)BOOLEAN_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); + } + + public static boolean getAndOrVolatile(final boolean[] array, final int index, final boolean param) { + return (boolean)BOOLEAN_ARRAY_HANDLE.getAndBitwiseOr(array, index, param); + } + + public static boolean getAndXorVolatile(final boolean[] array, final int index, final boolean param) { + return (boolean)BOOLEAN_ARRAY_HANDLE.getAndBitwiseXor(array, index, param); + } + + public static boolean getAndSetVolatile(final boolean[] array, final int index, final boolean param) { + return (boolean)BOOLEAN_ARRAY_HANDLE.getAndSet(array, index, param); + } + + public static boolean compareAndExchangeVolatileContended(final boolean[] array, final int index, final boolean expect, final boolean update) { + return (boolean)BOOLEAN_ARRAY_HANDLE.compareAndExchange(array, index, expect, update); + } + + public static boolean getAndAndVolatileContended(final boolean[] array, final int index, final boolean param) { + int failures = 0; + + for (boolean curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (boolean) (curr & param)))) { + return curr; + } + } + } + + public static boolean getAndOrVolatileContended(final boolean[] array, final int index, final boolean param) { + int failures = 0; + + for (boolean curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (boolean) (curr | param)))) { + return curr; + } + } + } + + public static boolean getAndXorVolatileContended(final boolean[] array, final int index, final boolean param) { + int failures = 0; + + for (boolean curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (boolean) (curr ^ param)))) { + return curr; + } + } + } + + public static boolean getAndSetVolatileContended(final boolean[] array, final int index, final boolean param) { + int failures = 0; + + for (boolean curr = getVolatile(array, index);;++failures) { + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) { + return curr; + } + } + } + + @SuppressWarnings("unchecked") + public static T 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/HashUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/HashUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..2b9f36211d1cbb4fcf1457c0a83592499e9aa23b --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/util/HashUtil.java @@ -0,0 +1,111 @@ +package ca.spottedleaf.concurrentutil.util; + +public final class HashUtil { + + // Copied from fastutil HashCommon + + /** 232 · φ, φ = (√5 − 1)/2. */ + private static final int INT_PHI = 0x9E3779B9; + /** The reciprocal of {@link #INT_PHI} modulo 232. */ + private static final int INV_INT_PHI = 0x144cbc89; + /** 264 · φ, φ = (√5 − 1)/2. */ + private static final long LONG_PHI = 0x9E3779B97F4A7C15L; + /** The reciprocal of {@link #LONG_PHI} modulo 264. */ + private static final long INV_LONG_PHI = 0xf1de83e19937733dL; + + /** Avalanches the bits of an integer by applying the finalisation step of MurmurHash3. + * + *

This method implements the finalisation step of Austin Appleby's MurmurHash3. + * Its purpose is to avalanche the bits of the argument to within 0.25% bias. + * + * @param x an integer. + * @return a hash value with good avalanching properties. + */ + // additional note: this function is a bijection onto all integers + public static int murmurHash3(int x) { + x ^= x >>> 16; + x *= 0x85ebca6b; + x ^= x >>> 13; + x *= 0xc2b2ae35; + x ^= x >>> 16; + return x; + } + + + /** Avalanches the bits of a long integer by applying the finalisation step of MurmurHash3. + * + *

This method implements the finalisation step of Austin Appleby's MurmurHash3. + * Its purpose is to avalanche the bits of the argument to within 0.25% bias. + * + * @param x a long integer. + * @return a hash value with good avalanching properties. + */ + // additional note: this function is a bijection onto all longs + public static long murmurHash3(long x) { + x ^= x >>> 33; + x *= 0xff51afd7ed558ccdL; + x ^= x >>> 33; + x *= 0xc4ceb9fe1a85ec53L; + x ^= x >>> 33; + return x; + } + + /** Quickly mixes the bits of an integer. + * + *

This method mixes the bits of the argument by multiplying by the golden ratio and + * xorshifting the result. It is borrowed from Koloboke, and + * it has slightly worse behaviour than {@link #murmurHash3(int)} (in open-addressing hash tables the average number of probes + * is slightly larger), but it's much faster. + * + * @param x an integer. + * @return a hash value obtained by mixing the bits of {@code x}. + * @see #invMix(int) + */ + // additional note: this function is a bijection onto all integers + public static int mix(final int x) { + final int h = x * INT_PHI; + return h ^ (h >>> 16); + } + + /** The inverse of {@link #mix(int)}. This method is mainly useful to create unit tests. + * + * @param x an integer. + * @return a value that passed through {@link #mix(int)} would give {@code x}. + */ + // additional note: this function is a bijection onto all integers + public static int invMix(final int x) { + return (x ^ x >>> 16) * INV_INT_PHI; + } + + /** Quickly mixes the bits of a long integer. + * + *

This method mixes the bits of the argument by multiplying by the golden ratio and + * xorshifting twice the result. It is borrowed from Koloboke, and + * it has slightly worse behaviour than {@link #murmurHash3(long)} (in open-addressing hash tables the average number of probes + * is slightly larger), but it's much faster. + * + * @param x a long integer. + * @return a hash value obtained by mixing the bits of {@code x}. + */ + // additional note: this function is a bijection onto all longs + public static long mix(final long x) { + long h = x * LONG_PHI; + h ^= h >>> 32; + return h ^ (h >>> 16); + } + + /** The inverse of {@link #mix(long)}. This method is mainly useful to create unit tests. + * + * @param x a long integer. + * @return a value that passed through {@link #mix(long)} would give {@code x}. + */ + // additional note: this function is a bijection onto all longs + public static long invMix(long x) { + x ^= x >>> 32; + x ^= x >>> 16; + return (x ^ x >>> 32) * INV_LONG_PHI; + } + + + private HashUtil() {} +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/IntPairUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/IntPairUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..4e61c477a56e645228d5a2015c26816954d17bf8 --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/util/IntPairUtil.java @@ -0,0 +1,46 @@ +package ca.spottedleaf.concurrentutil.util; + +public final class IntPairUtil { + + /** + * Packs the specified integers into one long value. + */ + public static long key(final int left, final int right) { + return ((long)right << 32) | (left & 0xFFFFFFFFL); + } + + /** + * Retrieves the left packed integer from the key + */ + public static int left(final long key) { + return (int)key; + } + + /** + * Retrieves the right packed integer from the key + */ + public static int right(final long key) { + return (int)(key >>> 32); + } + + public static String toString(final long key) { + return "{left:" + left(key) + ", right:" + right(key) + "}"; + } + + public static String toString(final long[] array, final int from, final int to) { + final StringBuilder ret = new StringBuilder(); + ret.append("["); + + for (int i = from; i < to; ++i) { + if (i != from) { + ret.append(", "); + } + ret.append(toString(array[i])); + } + + ret.append("]"); + return ret.toString(); + } + + private IntPairUtil() {} +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/IntegerUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/IntegerUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..77699c5fa9681f9ec7aa05cbb50eb60828e193ab --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/util/IntegerUtil.java @@ -0,0 +1,176 @@ +package ca.spottedleaf.concurrentutil.util; + +public final class IntegerUtil { + + public static final int HIGH_BIT_U32 = Integer.MIN_VALUE; + public static final long HIGH_BIT_U64 = Long.MIN_VALUE; + + public static int ceilLog2(final int value) { + return Integer.SIZE - Integer.numberOfLeadingZeros(value - 1); // see doc of numberOfLeadingZeros + } + + public static long ceilLog2(final long value) { + return Long.SIZE - Long.numberOfLeadingZeros(value - 1); // see doc of numberOfLeadingZeros + } + + public static int floorLog2(final int value) { + // xor is optimized subtract for 2^n -1 + // note that (2^n -1) - k = (2^n -1) ^ k for k <= (2^n - 1) + return (Integer.SIZE - 1) ^ Integer.numberOfLeadingZeros(value); // see doc of numberOfLeadingZeros + } + + public static int floorLog2(final long value) { + // xor is optimized subtract for 2^n -1 + // note that (2^n -1) - k = (2^n -1) ^ k for k <= (2^n - 1) + return (Long.SIZE - 1) ^ Long.numberOfLeadingZeros(value); // see doc of numberOfLeadingZeros + } + + public static int roundCeilLog2(final int value) { + // optimized variant of 1 << (32 - leading(val - 1)) + // given + // 1 << n = HIGH_BIT_32 >>> (31 - n) for n [0, 32) + // 1 << (32 - leading(val - 1)) = HIGH_BIT_32 >>> (31 - (32 - leading(val - 1))) + // HIGH_BIT_32 >>> (31 - (32 - leading(val - 1))) + // HIGH_BIT_32 >>> (31 - 32 + leading(val - 1)) + // HIGH_BIT_32 >>> (-1 + leading(val - 1)) + return HIGH_BIT_U32 >>> (Integer.numberOfLeadingZeros(value - 1) - 1); + } + + public static long roundCeilLog2(final long value) { + // see logic documented above + return HIGH_BIT_U64 >>> (Long.numberOfLeadingZeros(value - 1) - 1); + } + + public static int roundFloorLog2(final int value) { + // optimized variant of 1 << (31 - leading(val)) + // given + // 1 << n = HIGH_BIT_32 >>> (31 - n) for n [0, 32) + // 1 << (31 - leading(val)) = HIGH_BIT_32 >> (31 - (31 - leading(val))) + // HIGH_BIT_32 >> (31 - (31 - leading(val))) + // HIGH_BIT_32 >> (31 - 31 + leading(val)) + return HIGH_BIT_U32 >>> Integer.numberOfLeadingZeros(value); + } + + public static long roundFloorLog2(final long value) { + // see logic documented above + return HIGH_BIT_U64 >>> Long.numberOfLeadingZeros(value); + } + + public static boolean isPowerOfTwo(final int n) { + // 2^n has one bit + // note: this rets true for 0 still + return IntegerUtil.getTrailingBit(n) == n; + } + + public static boolean isPowerOfTwo(final long n) { + // 2^n has one bit + // note: this rets true for 0 still + return IntegerUtil.getTrailingBit(n) == n; + } + + public static int getTrailingBit(final int n) { + return -n & n; + } + + public static long getTrailingBit(final long n) { + return -n & n; + } + + public static int trailingZeros(final int n) { + return Integer.numberOfTrailingZeros(n); + } + + public static int trailingZeros(final long n) { + return Long.numberOfTrailingZeros(n); + } + + // from hacker's delight (signed division magic value) + public static int getDivisorMultiple(final long numbers) { + return (int)(numbers >>> 32); + } + + // from hacker's delight (signed division magic value) + public static int getDivisorShift(final long numbers) { + return (int)numbers; + } + + // copied from hacker's delight (signed division magic value) + // http://www.hackersdelight.org/hdcodetxt/magic.c.txt + public static long getDivisorNumbers(final int d) { + final int ad = branchlessAbs(d); + + if (ad < 2) { + throw new IllegalArgumentException("|number| must be in [2, 2^31 -1], not: " + d); + } + + final int two31 = 0x80000000; + final long mask = 0xFFFFFFFFL; // mask for enforcing unsigned behaviour + + /* + Signed usage: + int number; + long magic = getDivisorNumbers(div); + long mul = magic >>> 32; + int sign = number >> 31; + int result = (int)(((long)number * mul) >>> magic) - sign; + */ + /* + Unsigned usage: (note: fails for input > Integer.MAX_VALUE, only use when input < Integer.MAX_VALUE to avoid sign calculation) + int number; + long magic = getDivisorNumbers(div); + long mul = magic >>> 32; + int result = (int)(((long)number * mul) >>> magic); + */ + + int p = 31; + + // all these variables are UNSIGNED! + int t = two31 + (d >>> 31); + int anc = t - 1 - (int)((t & mask)%ad); + int q1 = (int)((two31 & mask)/(anc & mask)); + int r1 = two31 - q1*anc; + int q2 = (int)((two31 & mask)/(ad & mask)); + int r2 = two31 - q2*ad; + int delta; + + do { + p = p + 1; + q1 = 2*q1; // Update q1 = 2**p/|nc|. + r1 = 2*r1; // Update r1 = rem(2**p, |nc|). + if ((r1 & mask) >= (anc & mask)) {// (Must be an unsigned comparison here) + q1 = q1 + 1; + r1 = r1 - anc; + } + q2 = 2*q2; // Update q2 = 2**p/|d|. + r2 = 2*r2; // Update r2 = rem(2**p, |d|). + if ((r2 & mask) >= (ad & mask)) {// (Must be an unsigned comparison here) + q2 = q2 + 1; + r2 = r2 - ad; + } + delta = ad - r2; + } while ((q1 & mask) < (delta & mask) || (q1 == delta && r1 == 0)); + + int magicNum = q2 + 1; + if (d < 0) { + magicNum = -magicNum; + } + int shift = p; + return ((long)magicNum << 32) | shift; + } + + public static int branchlessAbs(final int val) { + // -n = -1 ^ n + 1 + final int mask = val >> (Integer.SIZE - 1); // -1 if < 0, 0 if >= 0 + return (mask ^ val) - mask; // if val < 0, then (0 ^ val) - 0 else (-1 ^ val) + 1 + } + + public static long branchlessAbs(final long val) { + // -n = -1 ^ n + 1 + final long mask = val >> (Long.SIZE - 1); // -1 if < 0, 0 if >= 0 + return (mask ^ val) - mask; // if val < 0, then (0 ^ val) - 0 else (-1 ^ val) + 1 + } + + private IntegerUtil() { + throw new RuntimeException(); + } +} \ No newline at end of file diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/ThrowUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/ThrowUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..a3a8b5c6795c4d116e094e4c910553416f565b93 --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/util/ThrowUtil.java @@ -0,0 +1,11 @@ +package ca.spottedleaf.concurrentutil.util; + +public final class ThrowUtil { + + private ThrowUtil() {} + + public static void throwUnchecked(final Throwable thr) throws T { + throw (T)thr; + } + +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/TimeUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/TimeUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..63688716244066581d5b505703576e3340e3baf3 --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/util/TimeUtil.java @@ -0,0 +1,60 @@ +package ca.spottedleaf.concurrentutil.util; + +public final class TimeUtil { + + /* + * The comparator is not a valid comparator for every long value. To prove where it is valid, see below. + * + * For reflexivity, we have that x - x = 0. We then have that for any long value x that + * compareTimes(x, x) == 0, as expected. + * + * For symmetry, we have that x - y = -(y - x) except for when y - x = Long.MIN_VALUE. + * So, the difference between any times x and y must not be equal to Long.MIN_VALUE. + * + * As for the transitive relation, consider we have x,y such that x - y = a > 0 and z such that + * y - z = b > 0. Then, we will have that the x - z > 0 is equivalent to a + b > 0. For long values, + * this holds as long as a + b <= Long.MAX_VALUE. + * + * Also consider we have x, y such that x - y = a < 0 and z such that y - z = b < 0. Then, we will have + * that x - z < 0 is equivalent to a + b < 0. For long values, this holds as long as a + b >= -Long.MAX_VALUE. + * + * Thus, the comparator is only valid for timestamps such that abs(c - d) <= Long.MAX_VALUE for all timestamps + * c and d. + */ + + /** + * This function is appropriate to be used as a {@link java.util.Comparator} between two timestamps, which + * indicates whether the timestamps represented by t1, t2 that t1 is before, equal to, or after t2. + */ + public static int compareTimes(final long t1, final long t2) { + final long diff = t1 - t2; + + // HD, Section 2-7 + return (int) ((diff >> 63) | (-diff >>> 63)); + } + + public static long getGreatestTime(final long t1, final long t2) { + final long diff = t1 - t2; + return diff < 0L ? t2 : t1; + } + + public static long getLeastTime(final long t1, final long t2) { + final long diff = t1 - t2; + return diff > 0L ? t2 : t1; + } + + public static long clampTime(final long value, final long min, final long max) { + final long diffMax = value - max; + final long diffMin = value - min; + + if (diffMax > 0L) { + return max; + } + if (diffMin < 0L) { + return min; + } + return value; + } + + private TimeUtil() {} +} diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/Validate.java b/src/main/java/ca/spottedleaf/concurrentutil/util/Validate.java new file mode 100644 index 0000000000000000000000000000000000000000..382177d0d162fa3139c9078a873ce2504a2b17b2 --- /dev/null +++ b/src/main/java/ca/spottedleaf/concurrentutil/util/Validate.java @@ -0,0 +1,28 @@ +package ca.spottedleaf.concurrentutil.util; + +public final class Validate { + + public static T 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(); + } +}