From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Spottedleaf <Spottedleaf@users.noreply.github.com>
Date: Sun, 23 Jan 2022 22:58:11 -0800
Subject: [PATCH] ConcurrentUtil


diff --git a/src/main/java/ca/spottedleaf/concurrentutil/collection/MultiThreadedQueue.java b/src/main/java/ca/spottedleaf/concurrentutil/collection/MultiThreadedQueue.java
new file mode 100644
index 0000000000000000000000000000000000000000..f84a622dc29750139ac280f480b7cd132b036287
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/concurrentutil/collection/MultiThreadedQueue.java
@@ -0,0 +1,1421 @@
+package ca.spottedleaf.concurrentutil.collection;
+
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
+import ca.spottedleaf.concurrentutil.util.Validate;
+
+import java.lang.invoke.VarHandle;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Queue;
+import java.util.Spliterator;
+import java.util.Spliterators;
+import java.util.function.Consumer;
+import java.util.function.IntFunction;
+import java.util.function.Predicate;
+
+/**
+ * MT-Safe linked first in first out ordered queue.
+ *
+ * This queue should out-perform {@link java.util.concurrent.ConcurrentLinkedQueue} in high-contention reads/writes, and is
+ * not any slower in lower contention reads/writes.
+ * <p>
+ * Note that this queue breaks the specification laid out by {@link Collection}, see {@link #preventAdds()} and {@link Collection#add(Object)}.
+ * </p>
+ * <p><b>
+ * This queue will only unlink linked nodes through the {@link #peek()} and {@link #poll()} methods, and this is only if
+ * they are at the head of the queue.
+ * </b></p>
+ * @param <E> Type of element in this queue.
+ */
+public class MultiThreadedQueue<E> implements Queue<E> {
+
+    protected volatile LinkedNode<E> head; /* Always non-null, high chance of being the actual head */
+
+    protected volatile LinkedNode<E> tail; /* Always non-null, high chance of being the actual tail */
+
+    /* Note that it is possible to reach head from tail. */
+
+    /* IMPL NOTE: Leave hashCode and equals to their defaults */
+
+    protected static final VarHandle HEAD_HANDLE = ConcurrentUtil.getVarHandle(MultiThreadedQueue.class, "head", LinkedNode.class);
+    protected static final VarHandle TAIL_HANDLE = ConcurrentUtil.getVarHandle(MultiThreadedQueue.class, "tail", LinkedNode.class);
+
+    /* head */
+
+    protected final void setHeadPlain(final LinkedNode<E> newHead) {
+        HEAD_HANDLE.set(this, newHead);
+    }
+
+    protected final void setHeadOpaque(final LinkedNode<E> newHead) {
+        HEAD_HANDLE.setOpaque(this, newHead);
+    }
+
+    @SuppressWarnings("unchecked")
+    protected final LinkedNode<E> getHeadPlain() {
+        return (LinkedNode<E>)HEAD_HANDLE.get(this);
+    }
+
+    @SuppressWarnings("unchecked")
+    protected final LinkedNode<E> getHeadOpaque() {
+        return (LinkedNode<E>)HEAD_HANDLE.getOpaque(this);
+    }
+
+    @SuppressWarnings("unchecked")
+    protected final LinkedNode<E> getHeadAcquire() {
+        return (LinkedNode<E>)HEAD_HANDLE.getAcquire(this);
+    }
+
+    /* tail */
+
+    protected final void setTailPlain(final LinkedNode<E> newTail) {
+        TAIL_HANDLE.set(this, newTail);
+    }
+
+    protected final void setTailOpaque(final LinkedNode<E> newTail) {
+        TAIL_HANDLE.setOpaque(this, newTail);
+    }
+
+    @SuppressWarnings("unchecked")
+    protected final LinkedNode<E> getTailPlain() {
+        return (LinkedNode<E>)TAIL_HANDLE.get(this);
+    }
+
+    @SuppressWarnings("unchecked")
+    protected final LinkedNode<E> getTailOpaque() {
+        return (LinkedNode<E>)TAIL_HANDLE.getOpaque(this);
+    }
+
+    /**
+     * Constructs a {@code MultiThreadedQueue}, initially empty.
+     * <p>
+     * The returned object may not be published without synchronization.
+     * </p>
+     */
+    public MultiThreadedQueue() {
+        final LinkedNode<E> value = new LinkedNode<>(null, null);
+        this.setHeadPlain(value);
+        this.setTailPlain(value);
+    }
+
+    /**
+     * Constructs a {@code MultiThreadedQueue}, initially containing all elements in the specified {@code collection}.
+     * <p>
+     * The returned object may not be published without synchronization.
+     * </p>
+     * @param collection The specified collection.
+     * @throws NullPointerException If {@code collection} is {@code null} or contains {@code null} elements.
+     */
+    public MultiThreadedQueue(final Iterable<? extends E> collection) {
+        final Iterator<? extends E> elements = collection.iterator();
+
+        if (!elements.hasNext()) {
+            final LinkedNode<E> value = new LinkedNode<>(null, null);
+            this.setHeadPlain(value);
+            this.setTailPlain(value);
+            return;
+        }
+
+        final LinkedNode<E> head = new LinkedNode<>(Validate.notNull(elements.next(), "Null element"), null);
+        LinkedNode<E> tail = head;
+
+        while (elements.hasNext()) {
+            final LinkedNode<E> next = new LinkedNode<>(Validate.notNull(elements.next(), "Null element"), null);
+            tail.setNextPlain(next);
+            tail = next;
+        }
+
+        this.setHeadPlain(head);
+        this.setTailPlain(tail);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public E remove() throws NoSuchElementException {
+        final E ret = this.poll();
+
+        if (ret == null) {
+            throw new NoSuchElementException();
+        }
+
+        return ret;
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * Contrary to the specification of {@link Collection#add}, this method will fail to add the element to this queue
+     * and return {@code false} if this queue is add-blocked.
+     * </p>
+     */
+    @Override
+    public boolean add(final E element) {
+        return this.offer(element);
+    }
+
+    /**
+     * Adds the specified element to the tail of this queue. If this queue is currently add-locked, then the queue is
+     * released from that lock and this element is added. The unlock operation and addition of the specified
+     * element is atomic.
+     * @param element The specified element.
+     * @return {@code true} if this queue previously allowed additions
+     */
+    public boolean forceAdd(final E element) {
+        final LinkedNode<E> node = new LinkedNode<>(element, null);
+
+        return !this.forceAppendList(node, node);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public E element() throws NoSuchElementException {
+        final E ret = this.peek();
+
+        if (ret == null) {
+            throw new NoSuchElementException();
+        }
+
+        return ret;
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * This method may also return {@code false} to indicate an element was not added if this queue is add-blocked.
+     * </p>
+     */
+    @Override
+    public boolean offer(final E element) {
+        Validate.notNull(element, "Null element");
+
+        final LinkedNode<E> node = new LinkedNode<>(element, null);
+
+        return this.appendList(node, node);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public E peek() {
+        for (LinkedNode<E> head = this.getHeadOpaque(), curr = head;;) {
+            final LinkedNode<E> next = curr.getNextVolatile();
+            final E element = curr.getElementPlain(); /* Likely in sync */
+
+            if (element != null) {
+                if (this.getHeadOpaque() == head && curr != head) {
+                    this.setHeadOpaque(curr);
+                }
+                return element;
+            }
+
+            if (next == null || curr == next) {
+                return null;
+            }
+            curr = next;
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public E poll() {
+        return this.removeHead();
+    }
+
+    /**
+     * Retrieves and removes the head of this queue if it matches the specified predicate. If this queue is empty
+     * or the head does not match the predicate, this function returns {@code null}.
+     * <p>
+     * The predicate may be invoked multiple or no times in this call.
+     * </p>
+     * @param predicate The specified predicate.
+     * @return The head if it matches the predicate, or {@code null} if it did not or this queue is empty.
+     */
+    public E pollIf(final Predicate<E> predicate) {
+        return this.removeHead(Validate.notNull(predicate, "Null predicate"));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void clear() {
+        //noinspection StatementWithEmptyBody
+        while (this.poll() != null);
+    }
+
+    /**
+     * Prevents elements from being added to this queue. Once this is called, any attempt to add to this queue will fail.
+     * <p>
+     * This function is MT-Safe.
+     * </p>
+     * @return {@code true} if the queue was modified to prevent additions, {@code false} if it already prevented additions.
+     */
+    public boolean preventAdds() {
+        final LinkedNode<E> deadEnd = new LinkedNode<>(null, null);
+        deadEnd.setNextPlain(deadEnd);
+
+        if (!this.appendList(deadEnd, deadEnd)) {
+            return false;
+        }
+
+        this.setTailPlain(deadEnd); /* (try to) Ensure tail is set for the following #allowAdds call */
+        return true;
+    }
+
+    /**
+     * Allows elements to be added to this queue once again. Note that this function has undefined behaviour if
+     * {@link #preventAdds()} is not called beforehand. The benefit of this function over {@link #tryAllowAdds()}
+     * is that this function might perform better.
+     * <p>
+     * This function is not MT-Safe.
+     * </p>
+     */
+    public void allowAdds() {
+        LinkedNode<E> tail = this.getTailPlain();
+
+        /* We need to find the tail given the cas on tail isn't atomic (nor volatile) in this.appendList */
+        /* Thus it is possible for an outdated tail to be set */
+        while (tail != (tail = tail.getNextPlain())) {}
+
+        tail.setNextVolatile(null);
+    }
+
+    /**
+     * Tries to allow elements to be added to this queue. Returns {@code true} if the queue was previous add-locked,
+     * {@code false} otherwise.
+     * <p>
+     * This function is MT-Safe, however it should not be used with {@link #allowAdds()}.
+     * </p>
+     * @return {@code true} if the queue was previously add-locked, {@code false} otherwise.
+     */
+    public boolean tryAllowAdds() {
+        LinkedNode<E> tail = this.getTailPlain();
+
+        for (int failures = 0;;) {
+            /* We need to find the tail given the cas on tail isn't atomic (nor volatile) in this.appendList */
+            /* Thus it is possible for an outdated tail to be set */
+            while (tail != (tail = tail.getNextAcquire())) {
+                if (tail == null) {
+                    return false;
+                }
+            }
+
+            for (int i = 0; i < failures; ++i) {
+                ConcurrentUtil.backoff();
+            }
+
+            if (tail == (tail = tail.compareExchangeNextVolatile(tail, null))) {
+                return true;
+            }
+
+            if (tail == null) {
+                return false;
+            }
+            ++failures;
+        }
+    }
+
+    /**
+     * Atomically adds the specified element to this queue or allows additions to the queue. If additions
+     * are not allowed, the element is not added.
+     * <p>
+     * This function is MT-Safe.
+     * </p>
+     * @param element The specified element.
+     * @return {@code true} if the queue now allows additions, {@code false} if the element was added.
+     */
+    public boolean addOrAllowAdds(final E element) {
+        Validate.notNull(element, "Null element");
+        int failures = 0;
+
+        final LinkedNode<E> append = new LinkedNode<>(element, null);
+
+        for (LinkedNode<E> currTail = this.getTailOpaque(), curr = currTail;;) {
+            /* It has been experimentally shown that placing the read before the backoff results in significantly greater performance */
+            /* It is likely due to a cache miss caused by another write to the next field */
+            final LinkedNode<E> next = curr.getNextVolatile();
+
+            for (int i = 0; i < failures; ++i) {
+                ConcurrentUtil.backoff();
+            }
+
+            if (next == null) {
+                final LinkedNode<E> compared = curr.compareExchangeNextVolatile(null, append);
+
+                if (compared == null) {
+                    /* Added */
+                    /* Avoid CASing on tail more than we need to */
+                    /* CAS to avoid setting an out-of-date tail */
+                    if (this.getTailOpaque() == currTail) {
+                        this.setTailOpaque(append);
+                    }
+                    return false; // we added
+                }
+
+                ++failures;
+                curr = compared;
+                continue;
+            } else if (next == curr) {
+                final LinkedNode<E> compared = curr.compareExchangeNextVolatile(curr, null);
+
+                if (compared == curr) {
+                    return true; // we let additions through
+                }
+
+                ++failures;
+
+                if (compared != null) {
+                    curr = compared;
+                }
+                continue;
+            }
+
+            if (curr == currTail) {
+                /* Tail is likely not up-to-date */
+                curr = next;
+            } else {
+                /* Try to update to tail */
+                if (currTail == (currTail = this.getTailOpaque())) {
+                    curr = next;
+                } else {
+                    curr = currTail;
+                }
+            }
+        }
+    }
+
+    /**
+     * Returns whether this queue is currently add-blocked. That is, whether {@link #add(Object)} and friends will return {@code false}.
+     */
+    public boolean isAddBlocked() {
+        for (LinkedNode<E> tail = this.getTailOpaque();;) {
+            LinkedNode<E> next = tail.getNextVolatile();
+            if (next == null) {
+                return false;
+            }
+
+            if (next == tail) {
+                return true;
+            }
+
+            tail = next;
+        }
+    }
+
+    /**
+     * Atomically removes the head from this queue if it exists, otherwise prevents additions to this queue if no
+     * head is removed.
+     * <p>
+     * This function is MT-Safe.
+     * </p>
+     * If the queue is already add-blocked and empty then no operation is performed.
+     * @return {@code null} if the queue is now add-blocked or was previously add-blocked, else returns
+     * an non-null value which was the previous head of queue.
+     */
+    public E pollOrBlockAdds() {
+        int failures = 0;
+        for (LinkedNode<E> head = this.getHeadOpaque(), curr = head;;) {
+            final E currentVal = curr.getElementVolatile();
+            final LinkedNode<E> next = curr.getNextOpaque();
+
+            if (next == curr) {
+                return null; /* Additions are already blocked */
+            }
+
+            for (int i = 0; i < failures; ++i) {
+                ConcurrentUtil.backoff();
+            }
+
+            if (currentVal != null) {
+                if (curr.getAndSetElementVolatile(null) == null) {
+                    ++failures;
+                    continue;
+                }
+
+                /* "CAS" to avoid setting an out-of-date head */
+                if (this.getHeadOpaque() == head) {
+                    this.setHeadOpaque(next != null ? next : curr);
+                }
+
+                return currentVal;
+            }
+
+            if (next == null) {
+                /* Try to update stale head */
+                if (curr != head && this.getHeadOpaque() == head) {
+                    this.setHeadOpaque(curr);
+                }
+
+                final LinkedNode<E> compared = curr.compareExchangeNextVolatile(null, curr);
+
+                if (compared != null) {
+                    // failed to block additions
+                    curr = compared;
+                    ++failures;
+                    continue;
+                }
+
+                return null; /* We blocked additions */
+            }
+
+            if (head == curr) {
+                /* head is likely not up-to-date */
+                curr = next;
+            } else {
+                /* Try to update to head */
+                if (head == (head = this.getHeadOpaque())) {
+                    curr = next;
+                } else {
+                    curr = head;
+                }
+            }
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean remove(final Object object) {
+        Validate.notNull(object, "Null object to remove");
+
+        for (LinkedNode<E> curr = this.getHeadOpaque();;) {
+            final LinkedNode<E> next = curr.getNextVolatile();
+            final E element = curr.getElementPlain(); /* Likely in sync */
+
+            if (element != null) {
+                if ((element == object || element.equals(object)) && curr.getAndSetElementVolatile(null) == element) {
+                    return true;
+                }
+            }
+
+            if (next == curr || next == null) {
+                break;
+            }
+            curr = next;
+        }
+
+        return false;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean removeIf(final Predicate<? super E> filter) {
+        Validate.notNull(filter, "Null filter");
+
+        boolean ret = false;
+
+        for (LinkedNode<E> curr = this.getHeadOpaque();;) {
+            final LinkedNode<E> next = curr.getNextVolatile();
+            final E element = curr.getElementPlain(); /* Likely in sync */
+
+            if (element != null) {
+                ret |= filter.test(element) && curr.getAndSetElementVolatile(null) == element;
+            }
+
+            if (next == null || next == curr) {
+                break;
+            }
+            curr = next;
+        }
+
+        return ret;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean removeAll(final Collection<?> collection) {
+        Validate.notNull(collection, "Null collection");
+
+        boolean ret = false;
+
+        /* Volatile is required to synchronize with the write to the first element */
+        for (LinkedNode<E> curr = this.getHeadOpaque();;) {
+            final LinkedNode<E> next = curr.getNextVolatile();
+            final E element = curr.getElementPlain(); /* Likely in sync */
+
+            if (element != null) {
+                ret |= collection.contains(element) && curr.getAndSetElementVolatile(null) == element;
+            }
+
+            if (next == null || next == curr) {
+                break;
+            }
+            curr = next;
+        }
+
+        return ret;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean retainAll(final Collection<?> collection) {
+        Validate.notNull(collection, "Null collection");
+
+        boolean ret = false;
+
+        for (LinkedNode<E> curr = this.getHeadOpaque();;) {
+            final LinkedNode<E> next = curr.getNextVolatile();
+            final E element = curr.getElementPlain(); /* Likely in sync */
+
+            if (element != null) {
+                ret |= !collection.contains(element) && curr.getAndSetElementVolatile(null) == element;
+            }
+
+            if (next == null || next == curr) {
+                break;
+            }
+            curr = next;
+        }
+
+        return ret;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public Object[] toArray() {
+        final List<E> ret = new ArrayList<>();
+
+        for (LinkedNode<E> curr = this.getHeadOpaque();;) {
+            final LinkedNode<E> next = curr.getNextVolatile();
+            final E element = curr.getElementPlain(); /* Likely in sync */
+
+            if (element != null) {
+                ret.add(element);
+            }
+
+            if (next == null || next == curr) {
+                break;
+            }
+            curr = next;
+        }
+
+        return ret.toArray();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public <T> T[] toArray(final T[] array) {
+        final List<T> ret = new ArrayList<>();
+
+        for (LinkedNode<E> curr = this.getHeadOpaque();;) {
+            final LinkedNode<E> next = curr.getNextVolatile();
+            final E element = curr.getElementPlain(); /* Likely in sync */
+
+            if (element != null) {
+                //noinspection unchecked
+                ret.add((T)element);
+            }
+
+            if (next == null || next == curr) {
+                break;
+            }
+            curr = next;
+        }
+
+        return ret.toArray(array);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public <T> T[] toArray(final IntFunction<T[]> generator) {
+        Validate.notNull(generator, "Null generator");
+
+        final List<T> ret = new ArrayList<>();
+
+        for (LinkedNode<E> curr = this.getHeadOpaque();;) {
+            final LinkedNode<E> next = curr.getNextVolatile();
+            final E element = curr.getElementPlain(); /* Likely in sync */
+
+            if (element != null) {
+                //noinspection unchecked
+                ret.add((T)element);
+            }
+
+            if (next == null || next == curr) {
+                break;
+            }
+            curr = next;
+        }
+
+        return ret.toArray(generator);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+
+        builder.append("MultiThreadedQueue: {elements: {");
+
+        int deadEntries = 0;
+        int totalEntries = 0;
+        int aliveEntries = 0;
+
+        boolean addLocked = false;
+
+        for (LinkedNode<E> curr = this.getHeadOpaque();; ++totalEntries) {
+            final LinkedNode<E> next = curr.getNextVolatile();
+            final E element = curr.getElementPlain(); /* Likely in sync */
+
+            if (element == null) {
+                ++deadEntries;
+            } else {
+                ++aliveEntries;
+            }
+
+            if (totalEntries != 0) {
+                builder.append(", ");
+            }
+
+            builder.append(totalEntries).append(": \"").append(element).append('"');
+
+            if (next == null) {
+                break;
+            }
+            if (curr == next) {
+                addLocked = true;
+                break;
+            }
+            curr = next;
+        }
+
+        builder.append("}, total_entries: \"").append(totalEntries).append("\", alive_entries: \"").append(aliveEntries)
+            .append("\", dead_entries:").append(deadEntries).append("\", add_locked: \"").append(addLocked)
+            .append("\"}");
+
+        return builder.toString();
+    }
+
+    /**
+     * Adds all elements from the specified collection to this queue. The addition is atomic.
+     * @param collection The specified collection.
+     * @return {@code true} if all elements were added successfully, or {@code false} if this queue is add-blocked, or
+     * {@code false} if the specified collection contains no elements.
+     */
+    @Override
+    public boolean addAll(final Collection<? extends E> collection) {
+        return this.addAll((Iterable<? extends E>)collection);
+    }
+
+    /**
+     * Adds all elements from the specified iterable object to this queue. The addition is atomic.
+     * @param iterable The specified iterable object.
+     * @return {@code true} if all elements were added successfully, or {@code false} if this queue is add-blocked, or
+     * {@code false} if the specified iterable contains no elements.
+     */
+    public boolean addAll(final Iterable<? extends E> iterable) {
+        Validate.notNull(iterable, "Null iterable");
+
+        final Iterator<? extends E> elements = iterable.iterator();
+        if (!elements.hasNext()) {
+            return false;
+        }
+
+        /* Build a list of nodes to append */
+        /* This is an much faster due to the fact that zero additional synchronization is performed */
+
+        final LinkedNode<E> head = new LinkedNode<>(Validate.notNull(elements.next(), "Null element"), null);
+        LinkedNode<E> tail = head;
+
+        while (elements.hasNext()) {
+            final LinkedNode<E> next = new LinkedNode<>(Validate.notNull(elements.next(), "Null element"), null);
+            tail.setNextPlain(next);
+            tail = next;
+        }
+
+        return this.appendList(head, tail);
+    }
+
+    /**
+     * Adds all of the elements from the specified array to this queue.
+     * @param items The specified array.
+     * @return {@code true} if all elements were added successfully, or {@code false} if this queue is add-blocked, or
+     * {@code false} if the specified array has a length of 0.
+     */
+    public boolean addAll(final E[] items) {
+        return this.addAll(items, 0, items.length);
+    }
+
+    /**
+     * Adds all of the elements from the specified array to this queue.
+     * @param items The specified array.
+     * @param off The offset in the array.
+     * @param len The number of items.
+     * @return {@code true} if all elements were added successfully, or {@code false} if this queue is add-blocked, or
+     * {@code false} if the specified array has a length of 0.
+     */
+    public boolean addAll(final E[] items, final int off, final int len) {
+        Validate.notNull(items, "Items may not be null");
+        Validate.arrayBounds(off, len, items.length, "Items array indices out of bounds");
+
+        if (len == 0) {
+            return false;
+        }
+
+        final LinkedNode<E> head = new LinkedNode<>(Validate.notNull(items[off], "Null element"), null);
+        LinkedNode<E> tail = head;
+
+        for (int i = 1; i < len; ++i) {
+            final LinkedNode<E> next = new LinkedNode<>(Validate.notNull(items[off + i], "Null element"), null);
+            tail.setNextPlain(next);
+            tail = next;
+        }
+
+        return this.appendList(head, tail);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean containsAll(final Collection<?> collection) {
+        Validate.notNull(collection, "Null collection");
+
+        for (final Object element : collection) {
+            if (!this.contains(element)) {
+                return false;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public Iterator<E> iterator() {
+        return new LinkedIterator<>(this.getHeadOpaque());
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * Note that this function is computed non-atomically and in O(n) time. The value returned may not be representative of
+     * the queue in its current state.
+     * </p>
+     */
+    @Override
+    public int size() {
+        int size = 0;
+
+        /* Volatile is required to synchronize with the write to the first element */
+        for (LinkedNode<E> curr = this.getHeadOpaque();;) {
+            final LinkedNode<E> next = curr.getNextVolatile();
+            final E element = curr.getElementPlain(); /* Likely in sync */
+
+            if (element != null) {
+                ++size;
+            }
+
+            if (next == null || next == curr) {
+                break;
+            }
+            curr = next;
+        }
+
+        return size;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean isEmpty() {
+        return this.peek() == null;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean contains(final Object object) {
+        Validate.notNull(object, "Null object");
+
+        for (LinkedNode<E> curr = this.getHeadOpaque();;) {
+            final LinkedNode<E> next = curr.getNextVolatile();
+            final E element = curr.getElementPlain(); /* Likely in sync */
+
+            if (element != null && (element == object || element.equals(object))) {
+                return true;
+            }
+
+            if (next == null || next == curr) {
+                break;
+            }
+            curr = next;
+        }
+
+        return false;
+    }
+
+    /**
+     * Finds the first element in this queue that matches the predicate.
+     * @param predicate The predicate to test elements against.
+     * @return The first element that matched the predicate, {@code null} if none matched.
+     */
+    public E find(final Predicate<E> predicate) {
+        Validate.notNull(predicate, "Null predicate");
+
+        for (LinkedNode<E> curr = this.getHeadOpaque();;) {
+            final LinkedNode<E> next = curr.getNextVolatile();
+            final E element = curr.getElementPlain(); /* Likely in sync */
+
+            if (element != null && predicate.test(element)) {
+                return element;
+            }
+
+            if (next == null || next == curr) {
+                break;
+            }
+            curr = next;
+        }
+
+        return null;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void forEach(final Consumer<? super E> action) {
+        Validate.notNull(action, "Null action");
+
+        for (LinkedNode<E> curr = this.getHeadOpaque();;) {
+            final LinkedNode<E> next = curr.getNextVolatile();
+            final E element = curr.getElementPlain(); /* Likely in sync */
+
+            if (element != null) {
+                action.accept(element);
+            }
+
+            if (next == null || next == curr) {
+                break;
+            }
+            curr = next;
+        }
+    }
+
+    // return true if normal addition, false if the queue previously disallowed additions
+    protected final boolean forceAppendList(final LinkedNode<E> head, final LinkedNode<E> tail) {
+        int failures = 0;
+
+        for (LinkedNode<E> currTail = this.getTailOpaque(), curr = currTail;;) {
+            /* It has been experimentally shown that placing the read before the backoff results in significantly greater performance */
+            /* It is likely due to a cache miss caused by another write to the next field */
+            final LinkedNode<E> next = curr.getNextVolatile();
+
+            for (int i = 0; i < failures; ++i) {
+                ConcurrentUtil.backoff();
+            }
+
+            if (next == null || next == curr) {
+                final LinkedNode<E> compared = curr.compareExchangeNextVolatile(next, head);
+
+                if (compared == next) {
+                    /* Added */
+                    /* Avoid CASing on tail more than we need to */
+                    /* "CAS" to avoid setting an out-of-date tail */
+                    if (this.getTailOpaque() == currTail) {
+                        this.setTailOpaque(tail);
+                    }
+                    return next != curr;
+                }
+
+                ++failures;
+                curr = compared;
+                continue;
+            }
+
+            if (curr == currTail) {
+                /* Tail is likely not up-to-date */
+                curr = next;
+            } else {
+                /* Try to update to tail */
+                if (currTail == (currTail = this.getTailOpaque())) {
+                    curr = next;
+                } else {
+                    curr = currTail;
+                }
+            }
+        }
+    }
+
+    // return true if successful, false otherwise
+    protected final boolean appendList(final LinkedNode<E> head, final LinkedNode<E> tail) {
+        int failures = 0;
+
+        for (LinkedNode<E> currTail = this.getTailOpaque(), curr = currTail;;) {
+            /* It has been experimentally shown that placing the read before the backoff results in significantly greater performance */
+            /* It is likely due to a cache miss caused by another write to the next field */
+            final LinkedNode<E> next = curr.getNextVolatile();
+
+            if (next == curr) {
+                /* Additions are stopped */
+                return false;
+            }
+
+            for (int i = 0; i < failures; ++i) {
+                ConcurrentUtil.backoff();
+            }
+
+            if (next == null) {
+                final LinkedNode<E> compared = curr.compareExchangeNextVolatile(null, head);
+
+                if (compared == null) {
+                    /* Added */
+                    /* Avoid CASing on tail more than we need to */
+                    /* CAS to avoid setting an out-of-date tail */
+                    if (this.getTailOpaque() == currTail) {
+                        this.setTailOpaque(tail);
+                    }
+                    return true;
+                }
+
+                ++failures;
+                curr = compared;
+                continue;
+            }
+
+            if (curr == currTail) {
+                /* Tail is likely not up-to-date */
+                curr = next;
+            } else {
+                /* Try to update to tail */
+                if (currTail == (currTail = this.getTailOpaque())) {
+                    curr = next;
+                } else {
+                    curr = currTail;
+                }
+            }
+        }
+    }
+
+    protected final E removeHead(final Predicate<E> predicate) {
+        int failures = 0;
+        for (LinkedNode<E> head = this.getHeadOpaque(), curr = head;;) {
+            // volatile here synchronizes-with writes to element
+            final LinkedNode<E> next = curr.getNextVolatile();
+            final E currentVal = curr.getElementPlain();
+
+            for (int i = 0; i < failures; ++i) {
+                ConcurrentUtil.backoff();
+            }
+
+            if (currentVal != null) {
+                if (!predicate.test(currentVal)) {
+                    /* Try to update stale head */
+                    if (curr != head && this.getHeadOpaque() == head) {
+                        this.setHeadOpaque(curr);
+                    }
+                    return null;
+                }
+                if (curr.getAndSetElementVolatile(null) == null) {
+                    /* Failed to get head */
+                    if (curr == (curr = next) || next == null) {
+                        return null;
+                    }
+                    ++failures;
+                    continue;
+                }
+
+                /* "CAS" to avoid setting an out-of-date head */
+                if (this.getHeadOpaque() == head) {
+                    this.setHeadOpaque(next != null ? next : curr);
+                }
+
+                return currentVal;
+            }
+
+            if (curr == next || next == null) {
+                /* Try to update stale head */
+                if (curr != head && this.getHeadOpaque() == head) {
+                    this.setHeadOpaque(curr);
+                }
+                return null; /* End of queue */
+            }
+
+            if (head == curr) {
+                /* head is likely not up-to-date */
+                curr = next;
+            } else {
+                /* Try to update to head */
+                if (head == (head = this.getHeadOpaque())) {
+                    curr = next;
+                } else {
+                    curr = head;
+                }
+            }
+        }
+    }
+
+    protected final E removeHead() {
+        int failures = 0;
+        for (LinkedNode<E> head = this.getHeadOpaque(), curr = head;;) {
+            final LinkedNode<E> next = curr.getNextVolatile();
+            final E currentVal = curr.getElementPlain();
+
+            for (int i = 0; i < failures; ++i) {
+                ConcurrentUtil.backoff();
+            }
+
+            if (currentVal != null) {
+                if (curr.getAndSetElementVolatile(null) == null) {
+                    /* Failed to get head */
+                    if (curr == (curr = next) || next == null) {
+                        return null;
+                    }
+                    ++failures;
+                    continue;
+                }
+
+                /* "CAS" to avoid setting an out-of-date head */
+                if (this.getHeadOpaque() == head) {
+                    this.setHeadOpaque(next != null ? next : curr);
+                }
+
+                return currentVal;
+            }
+
+            if (curr == next || next == null) {
+                /* Try to update stale head */
+                if (curr != head && this.getHeadOpaque() == head) {
+                    this.setHeadOpaque(curr);
+                }
+                return null; /* End of queue */
+            }
+
+            if (head == curr) {
+                /* head is likely not up-to-date */
+                curr = next;
+            } else {
+                /* Try to update to head */
+                if (head == (head = this.getHeadOpaque())) {
+                    curr = next;
+                } else {
+                    curr = head;
+                }
+            }
+        }
+    }
+
+    /**
+     * Empties the queue into the specified consumer. This function is optimized for single-threaded reads, and should
+     * be faster than a loop on {@link #poll()}.
+     * <p>
+     * This function is not MT-Safe. This function cannot be called with other read operations ({@link #peek()}, {@link #poll()},
+     * {@link #clear()}, etc).
+     * Write operations are safe to be called concurrently.
+     * </p>
+     * @param consumer The consumer to accept the elements.
+     * @return The total number of elements drained.
+     */
+    public int drain(final Consumer<E> consumer) {
+        return this.drain(consumer, false, ConcurrentUtil::rethrow);
+    }
+
+    /**
+     * Empties the queue into the specified consumer. This function is optimized for single-threaded reads, and should
+     * be faster than a loop on {@link #poll()}.
+     * <p>
+     * If {@code preventAdds} is {@code true}, then after this function returns the queue is guaranteed to be empty and
+     * additions to the queue will fail.
+     * </p>
+     * <p>
+     * This function is not MT-Safe. This function cannot be called with other read operations ({@link #peek()}, {@link #poll()},
+     * {@link #clear()}, etc).
+     * Write operations are safe to be called concurrently.
+     * </p>
+     * @param consumer The consumer to accept the elements.
+     * @param preventAdds Whether to prevent additions to this queue after draining.
+     * @return The total number of elements drained.
+     */
+    public int drain(final Consumer<E> consumer, final boolean preventAdds) {
+        return this.drain(consumer, preventAdds, ConcurrentUtil::rethrow);
+    }
+
+    /**
+     * Empties the queue into the specified consumer. This function is optimized for single-threaded reads, and should
+     * be faster than a loop on {@link #poll()}.
+     * <p>
+     * If {@code preventAdds} is {@code true}, then after this function returns the queue is guaranteed to be empty and
+     * additions to the queue will fail.
+     * </p>
+     * <p>
+     * This function is not MT-Safe. This function cannot be called with other read operations ({@link #peek()}, {@link #poll()},
+     * {@link #clear()}, {@link #remove(Object)} etc).
+     * Only write operations are safe to be called concurrently.
+     * </p>
+     * @param consumer The consumer to accept the elements.
+     * @param preventAdds Whether to prevent additions to this queue after draining.
+     * @param exceptionHandler Invoked when the consumer raises an exception.
+     * @return The total number of elements drained.
+     */
+    public int drain(final Consumer<E> consumer, final boolean preventAdds, final Consumer<Throwable> exceptionHandler) {
+        Validate.notNull(consumer, "Null consumer");
+        Validate.notNull(exceptionHandler, "Null exception handler");
+
+        /* This function assumes proper synchronization is made to ensure drain and no other read function are called concurrently */
+        /* This allows plain write usages instead of opaque or higher */
+        int total = 0;
+
+        final LinkedNode<E> head = this.getHeadAcquire(); /* Required to synchronize with the write to the first element field */
+        LinkedNode<E> curr = head;
+
+        for (;;) {
+            /* Volatile acquires with the write to the element field */
+            final E currentVal = curr.getElementPlain();
+            LinkedNode<E> next = curr.getNextVolatile();
+
+            if (next == curr) {
+                /* Add-locked nodes always have a null value */
+                break;
+            }
+
+            if (currentVal == null) {
+                if (next == null) {
+                    if (preventAdds && (next = curr.compareExchangeNextVolatile(null, curr)) != null) {
+                        // failed to prevent adds, continue
+                        curr = next;
+                        continue;
+                    } else {
+                        // we're done here
+                        break;
+                    }
+                }
+                curr = next;
+                continue;
+            }
+
+            try {
+                consumer.accept(currentVal);
+            } catch (final Exception ex) {
+                this.setHeadOpaque(next != null ? next : curr); /* Avoid perf penalty (of reiterating) if the exception handler decides to re-throw */
+                curr.setElementOpaque(null); /* set here, we might re-throw */
+
+                exceptionHandler.accept(ex);
+            }
+
+            curr.setElementOpaque(null);
+
+            ++total;
+
+            if (next == null) {
+                if (preventAdds && (next = curr.compareExchangeNextVolatile(null, curr)) != null) {
+                    /* Retry with next value */
+                    curr = next;
+                    continue;
+                }
+                break;
+            }
+
+            curr = next;
+        }
+        if (curr != head) {
+            this.setHeadOpaque(curr); /* While this may be a plain write, eventually publish it for methods such as find. */
+        }
+        return total;
+    }
+
+    @Override
+    public Spliterator<E> spliterator() { // TODO implement
+        return Spliterators.spliterator(this, Spliterator.CONCURRENT |
+            Spliterator.NONNULL | Spliterator.ORDERED);
+    }
+
+    protected static final class LinkedNode<E> {
+
+        protected volatile Object element;
+        protected volatile LinkedNode<E> next;
+
+        protected static final VarHandle ELEMENT_HANDLE = ConcurrentUtil.getVarHandle(LinkedNode.class, "element", Object.class);
+        protected static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(LinkedNode.class, "next", LinkedNode.class);
+
+        protected LinkedNode(final Object element, final LinkedNode<E> next) {
+            ELEMENT_HANDLE.set(this, element);
+            NEXT_HANDLE.set(this, next);
+        }
+
+        /* element */
+
+        @SuppressWarnings("unchecked")
+        protected final E getElementPlain() {
+            return (E)ELEMENT_HANDLE.get(this);
+        }
+
+        @SuppressWarnings("unchecked")
+        protected final E getElementVolatile() {
+            return (E)ELEMENT_HANDLE.getVolatile(this);
+        }
+
+        protected final void setElementPlain(final E update) {
+            ELEMENT_HANDLE.set(this, (Object)update);
+        }
+
+        protected final void setElementOpaque(final E update) {
+            ELEMENT_HANDLE.setOpaque(this, (Object)update);
+        }
+
+        protected final void setElementVolatile(final E update) {
+            ELEMENT_HANDLE.setVolatile(this, (Object)update);
+        }
+
+        @SuppressWarnings("unchecked")
+        protected final E getAndSetElementVolatile(final E update) {
+            return (E)ELEMENT_HANDLE.getAndSet(this, update);
+        }
+
+        @SuppressWarnings("unchecked")
+        protected final E compareExchangeElementVolatile(final E expect, final E update) {
+            return (E)ELEMENT_HANDLE.compareAndExchange(this, expect, update);
+        }
+
+        /* next */
+
+        @SuppressWarnings("unchecked")
+        protected final LinkedNode<E> getNextPlain() {
+            return (LinkedNode<E>)NEXT_HANDLE.get(this);
+        }
+
+        @SuppressWarnings("unchecked")
+        protected final LinkedNode<E> getNextOpaque() {
+            return (LinkedNode<E>)NEXT_HANDLE.getOpaque(this);
+        }
+
+        @SuppressWarnings("unchecked")
+        protected final LinkedNode<E> getNextAcquire() {
+            return (LinkedNode<E>)NEXT_HANDLE.getAcquire(this);
+        }
+
+        @SuppressWarnings("unchecked")
+        protected final LinkedNode<E> getNextVolatile() {
+            return (LinkedNode<E>)NEXT_HANDLE.getVolatile(this);
+        }
+
+        protected final void setNextPlain(final LinkedNode<E> next) {
+            NEXT_HANDLE.set(this, next);
+        }
+
+        protected final void setNextVolatile(final LinkedNode<E> next) {
+            NEXT_HANDLE.setVolatile(this, next);
+        }
+
+        @SuppressWarnings("unchecked")
+        protected final LinkedNode<E> compareExchangeNextVolatile(final LinkedNode<E> expect, final LinkedNode<E> set) {
+            return (LinkedNode<E>)NEXT_HANDLE.compareAndExchange(this, expect, set);
+        }
+    }
+
+    protected static final class LinkedIterator<E> implements Iterator<E> {
+
+        protected LinkedNode<E> curr; /* last returned by next() */
+        protected LinkedNode<E> next; /* next to return from next() */
+        protected E nextElement; /* cached to avoid a race condition with removing or polling */
+
+        protected LinkedIterator(final LinkedNode<E> start) {
+            /* setup nextElement and next */
+            for (LinkedNode<E> curr = start;;) {
+                final LinkedNode<E> next = curr.getNextVolatile();
+
+                final E element = curr.getElementPlain();
+
+                if (element != null) {
+                    this.nextElement = element;
+                    this.next = curr;
+                    break;
+                }
+
+                if (next == null || next == curr) {
+                    break;
+                }
+                curr = next;
+            }
+        }
+
+        protected final void findNext() {
+            /* only called if this.nextElement != null, which means this.next != null */
+            for (LinkedNode<E> curr = this.next;;) {
+                final LinkedNode<E> next = curr.getNextVolatile();
+
+                if (next == null || next == curr) {
+                    break;
+                }
+
+                final E element = next.getElementPlain();
+
+                if (element != null) {
+                    this.nextElement = element;
+                    this.curr = this.next; /* this.next will be the value returned from next(), set this.curr for remove() */
+                    this.next = next;
+                    return;
+                }
+                curr = next;
+            }
+
+            /* out of nodes to iterate */
+            /* keep curr for remove() calls */
+            this.next = null;
+            this.nextElement = null;
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public boolean hasNext() {
+            return this.nextElement != null;
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public E next() {
+            final E element = this.nextElement;
+
+            if (element == null) {
+                throw new NoSuchElementException();
+            }
+
+            this.findNext();
+
+            return element;
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public void remove() {
+            if (this.curr == null) {
+                throw new IllegalStateException();
+            }
+
+            this.curr.setElementVolatile(null);
+            this.curr = null;
+        }
+    }
+}
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/collection/SRSWLinkedQueue.java b/src/main/java/ca/spottedleaf/concurrentutil/collection/SRSWLinkedQueue.java
new file mode 100644
index 0000000000000000000000000000000000000000..094eff418b4e3bffce020d650931b4d9e58fa9ed
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/concurrentutil/collection/SRSWLinkedQueue.java
@@ -0,0 +1,149 @@
+package ca.spottedleaf.concurrentutil.collection;
+
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
+import ca.spottedleaf.concurrentutil.util.Validate;
+
+import java.lang.invoke.VarHandle;
+import java.util.ConcurrentModificationException;
+
+/**
+ * Single reader thread single writer thread queue. The reader side of the queue is ordered by acquire semantics,
+ * and the writer side of the queue is ordered by release semantics.
+ */
+// TODO test
+public class SRSWLinkedQueue<E> {
+
+    // always non-null
+    protected LinkedNode<E> head;
+
+    // always non-null
+    protected LinkedNode<E> tail;
+
+    /* IMPL NOTE: Leave hashCode and equals to their defaults */
+
+    public SRSWLinkedQueue() {
+        final LinkedNode<E> dummy = new LinkedNode<>(null, null);
+        this.head = this.tail = dummy;
+    }
+
+    /**
+     * Must be the reader thread.
+     *
+     * <p>
+     * Returns, without removing, the first element of this queue.
+     * </p>
+     * @return Returns, without removing, the first element of this queue.
+     */
+    public E peekFirst() {
+        LinkedNode<E> head = this.head;
+        E ret = head.getElementPlain();
+        if (ret == null) {
+            head = head.getNextAcquire();
+            if (head == null) {
+                // empty
+                return null;
+            }
+            // update head reference for next poll() call
+            this.head = head;
+            // guaranteed to be non-null
+            ret = head.getElementPlain();
+            if (ret == null) {
+                throw new ConcurrentModificationException("Multiple reader threads");
+            }
+        }
+
+        return ret;
+    }
+
+    /**
+     * Must be the reader thread.
+     *
+     * <p>
+     * Returns and removes the first element of this queue.
+     * </p>
+     * @return Returns and removes the first element of this queue.
+     */
+    public E poll() {
+        LinkedNode<E> head = this.head;
+        E ret = head.getElementPlain();
+        if (ret == null) {
+            head = head.getNextAcquire();
+            if (head == null) {
+                // empty
+                return null;
+            }
+            // guaranteed to be non-null
+            ret = head.getElementPlain();
+            if (ret == null) {
+                throw new ConcurrentModificationException("Multiple reader threads");
+            }
+        }
+
+        head.setElementPlain(null);
+        LinkedNode<E> next = head.getNextAcquire();
+        this.head = next == null ? head : next;
+
+        return ret;
+    }
+
+    /**
+     * Must be the writer thread.
+     *
+     * <p>
+     * Adds the element to the end of the queue.
+     * </p>
+     *
+     * @throws NullPointerException If the provided element is null
+     */
+    public void addLast(final E element) {
+        Validate.notNull(element, "Provided element cannot be null");
+        final LinkedNode<E> append = new LinkedNode<>(element, null);
+
+        this.tail.setNextRelease(append);
+        this.tail = append;
+    }
+
+    protected static final class LinkedNode<E> {
+
+        protected volatile Object element;
+        protected volatile LinkedNode<E> next;
+
+        protected static final VarHandle ELEMENT_HANDLE = ConcurrentUtil.getVarHandle(LinkedNode.class, "element", Object.class);
+        protected static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(LinkedNode.class, "next", LinkedNode.class);
+
+        protected LinkedNode(final Object element, final LinkedNode<E> next) {
+            ELEMENT_HANDLE.set(this, element);
+            NEXT_HANDLE.set(this, next);
+        }
+
+        /* element */
+
+        @SuppressWarnings("unchecked")
+        protected final E getElementPlain() {
+            return (E)ELEMENT_HANDLE.get(this);
+        }
+
+        protected final void setElementPlain(final E update) {
+            ELEMENT_HANDLE.set(this, (Object)update);
+        }
+        /* next */
+
+        @SuppressWarnings("unchecked")
+        protected final LinkedNode<E> getNextPlain() {
+            return (LinkedNode<E>)NEXT_HANDLE.get(this);
+        }
+
+        @SuppressWarnings("unchecked")
+        protected final LinkedNode<E> getNextAcquire() {
+            return (LinkedNode<E>)NEXT_HANDLE.getAcquire(this);
+        }
+
+        protected final void setNextPlain(final LinkedNode<E> next) {
+            NEXT_HANDLE.set(this, next);
+        }
+
+        protected final void setNextRelease(final LinkedNode<E> next) {
+            NEXT_HANDLE.setRelease(this, next);
+        }
+    }
+}
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/completable/Completable.java b/src/main/java/ca/spottedleaf/concurrentutil/completable/Completable.java
new file mode 100644
index 0000000000000000000000000000000000000000..46d1bd01542ebeeffc0006a5c585a50dbbbff907
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/concurrentutil/completable/Completable.java
@@ -0,0 +1,112 @@
+package ca.spottedleaf.concurrentutil.completable;
+
+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
+import ca.spottedleaf.concurrentutil.executor.Cancellable;
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import java.util.function.BiConsumer;
+
+public final class Completable<T> {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(Completable.class);
+
+    private final MultiThreadedQueue<BiConsumer<T, Throwable>> waiters = new MultiThreadedQueue<>();
+    private T result;
+    private Throwable throwable;
+    private volatile boolean completed;
+
+    public boolean isCompleted() {
+        return this.completed;
+    }
+
+    /**
+     * Note: Can only use after calling {@link #addAsynchronousWaiter(BiConsumer)}, as this function performs zero
+     * synchronisation
+     */
+    public T getResult() {
+        return this.result;
+    }
+
+    /**
+     * Note: Can only use after calling {@link #addAsynchronousWaiter(BiConsumer)}, as this function performs zero
+     * synchronisation
+     */
+    public Throwable getThrowable() {
+        return this.throwable;
+    }
+
+    /**
+     * Adds a waiter that should only be completed asynchronously by the complete() calls. If complete()
+     * has already been called, returns {@code null} and does not invoke the specified consumer.
+     * @param consumer Consumer to be executed on completion
+     * @throws NullPointerException If consumer is null
+     * @return A cancellable which will control the execution of the specified consumer
+     */
+    public Cancellable addAsynchronousWaiter(final BiConsumer<T, Throwable> consumer) {
+        if (this.waiters.add(consumer)) {
+            return new CancellableImpl(consumer);
+        }
+        return null;
+    }
+
+    private void completeAllWaiters(final T result, final Throwable throwable) {
+        this.completed = true;
+        BiConsumer<T, Throwable> waiter;
+        while ((waiter = this.waiters.pollOrBlockAdds()) != null) {
+            this.completeWaiter(waiter, result, throwable);
+        }
+    }
+
+    private void completeWaiter(final BiConsumer<T, Throwable> consumer, final T result, final Throwable throwable) {
+        try {
+            consumer.accept(result, throwable);
+        } catch (final ThreadDeath death) {
+            throw death;
+        } catch (final Throwable throwable2) {
+            LOGGER.error("Failed to complete callback " + ConcurrentUtil.genericToString(consumer), throwable2);
+        }
+    }
+
+    /**
+     * Adds a waiter that will be completed asynchronously by the complete() calls. If complete()
+     * has already been called, then invokes the consumer synchronously with the completed result.
+     * @param consumer Consumer to be executed on completion
+     * @throws NullPointerException If consumer is null
+     * @return A cancellable which will control the execution of the specified consumer
+     */
+    public Cancellable addWaiter(final BiConsumer<T, Throwable> consumer) {
+        if (this.waiters.add(consumer)) {
+            return new CancellableImpl(consumer);
+        }
+        this.completeWaiter(consumer, this.result, this.throwable);
+        return new CancellableImpl(consumer);
+    }
+
+    public void complete(final T result) {
+        this.result = result;
+        this.completeAllWaiters(result, null);
+    }
+
+    public void completeWithThrowable(final Throwable throwable) {
+        if (throwable == null) {
+            throw new NullPointerException("Throwable cannot be null");
+        }
+        this.throwable = throwable;
+        this.completeAllWaiters(null, throwable);
+    }
+
+    private final class CancellableImpl implements Cancellable {
+
+        private final BiConsumer<T, Throwable> waiter;
+
+        private CancellableImpl(final BiConsumer<T, Throwable> waiter) {
+            this.waiter = waiter;
+        }
+
+        @Override
+        public boolean cancel() {
+            return Completable.this.waiters.remove(this.waiter);
+        }
+    }
+}
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/BaseExecutor.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/BaseExecutor.java
new file mode 100644
index 0000000000000000000000000000000000000000..18d646676fd022afd64afaac30ec1bd283a73b0e
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/BaseExecutor.java
@@ -0,0 +1,208 @@
+package ca.spottedleaf.concurrentutil.executor;
+
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
+import java.util.function.BooleanSupplier;
+
+/**
+ * Base implementation for an abstract queue of tasks which are executed either synchronously or asynchronously.
+ *
+ * <p>
+ * The implementation supports tracking task executions using {@link #getTotalTasksScheduled()} and
+ * {@link #getTotalTasksExecuted()}, and optionally shutting down the executor using {@link #shutdown()}
+ * </p>
+ *
+ * <p>
+ * The base implementation does not provide a method to queue a task for execution, rather that is specified in
+ * the specific implementation. However, it is required that a specific implementation provides a method to
+ * <i>queue</i> a task or <i>create</i> a task. A <i>queued</i> task is one which will eventually be executed,
+ * and a <i>created</i> task must be queued to execute via {@link BaseTask#queue()} or be executed manually via
+ * {@link BaseTask#execute()}. This choice of delaying the queueing of a task may be useful to provide a task handle
+ * which may be cancelled or adjusted before the actual real task logic is ready to be executed.
+ * </p>
+ */
+public interface BaseExecutor {
+
+    /**
+     * Returns whether every task scheduled to this queue has been removed and executed or cancelled. If no tasks have been queued,
+     * returns {@code true}.
+     *
+     * @return {@code true} if all tasks that have been queued have finished executing or no tasks have been queued, {@code false} otherwise.
+     */
+    public default boolean haveAllTasksExecuted() {
+        // order is important
+        // if new tasks are scheduled between the reading of these variables, scheduled is guaranteed to be higher -
+        // so our check fails, and we try again
+        final long completed = this.getTotalTasksExecuted();
+        final long scheduled = this.getTotalTasksScheduled();
+
+        return completed == scheduled;
+    }
+
+    /**
+     * Returns the number of tasks that have been scheduled or execute or are pending to be scheduled.
+     */
+    public long getTotalTasksScheduled();
+
+    /**
+     * Returns the number of tasks that have fully been executed.
+     */
+    public long getTotalTasksExecuted();
+
+    /**
+     * Waits until this queue has had all of its tasks executed (NOT removed). See {@link #haveAllTasksExecuted()}
+     * <p>
+     *     This call is most effective after a {@link #shutdown()} call, as the shutdown call guarantees no tasks can
+     *     be executed and the waitUntilAllExecuted call makes sure the queue is empty. Effectively, using shutdown then using
+     *     waitUntilAllExecuted ensures this queue is empty - and most importantly, will remain empty.
+     * </p>
+     * <p>
+     *     This method is not guaranteed to be immediately responsive to queue state, so calls may take significantly more
+     *     time than expected. Effectively, do not rely on this call being fast - even if there are few tasks scheduled.
+     * </p>
+     * <p>
+     *     Note: Interruptions to the the current thread have no effect. Interrupt status is also not affected by this call.
+     * </p>
+     *
+     * @throws IllegalStateException If the current thread is not allowed to wait
+     */
+    public default void waitUntilAllExecuted() throws IllegalStateException {
+        long failures = 1L; // start at 0.25ms
+
+        while (!this.haveAllTasksExecuted()) {
+            Thread.yield();
+            failures = ConcurrentUtil.linearLongBackoff(failures, 250_000L, 5_000_000L); // 500us, 5ms
+        }
+    }
+
+    /**
+     * Executes the next available task.
+     *
+     * @return {@code true} if a task was executed, {@code false} otherwise
+     * @throws IllegalStateException If the current thread is not allowed to execute a task
+     */
+    public boolean executeTask() throws IllegalStateException;
+
+    /**
+     * Executes all queued tasks.
+     *
+     * @return {@code true} if a task was executed, {@code false} otherwise
+     * @throws IllegalStateException If the current thread is not allowed to execute a task
+     */
+    public default boolean executeAll() {
+        if (!this.executeTask()) {
+            return false;
+        }
+
+        while (this.executeTask());
+
+        return true;
+    }
+
+    /**
+     * Waits and executes tasks until the condition returns {@code true}.
+     * <p>
+     *     WARNING: This function is <i>not</i> suitable for waiting until a deadline!
+     *     Use {@link #executeUntil(long)} or {@link #executeConditionally(BooleanSupplier, long)} instead.
+     * </p>
+     */
+    public default void executeConditionally(final BooleanSupplier condition) {
+        long failures = 0;
+        while (!condition.getAsBoolean()) {
+            if (this.executeTask()) {
+                failures = failures >>> 2;
+            } else {
+                failures = ConcurrentUtil.linearLongBackoff(failures, 100_000L, 10_000_000L); // 100us, 10ms
+            }
+        }
+    }
+
+    /**
+     * Waits and executes tasks until the condition returns {@code true} or {@code System.nanoTime() - deadline >= 0}.
+     */
+    public default void executeConditionally(final BooleanSupplier condition, final long deadline) {
+        long failures = 0;
+        // double check deadline; we don't know how expensive the condition is
+        while ((System.nanoTime() - deadline < 0L) && !condition.getAsBoolean() && (System.nanoTime() - deadline < 0L)) {
+            if (this.executeTask()) {
+                failures = failures >>> 2;
+            } else {
+                failures = ConcurrentUtil.linearLongBackoffDeadline(failures, 100_000L, 10_000_000L, deadline); // 100us, 10ms
+            }
+        }
+    }
+
+    /**
+     * Waits and executes tasks until {@code System.nanoTime() - deadline >= 0}.
+     */
+    public default void executeUntil(final long deadline) {
+        long failures = 0;
+        while (System.nanoTime() - deadline < 0L) {
+            if (this.executeTask()) {
+                failures = failures >>> 2;
+            } else {
+                failures = ConcurrentUtil.linearLongBackoffDeadline(failures, 100_000L, 10_000_000L, deadline); // 100us, 10ms
+            }
+        }
+    }
+
+    /**
+     * Prevent further additions to this queue. Attempts to add after this call has completed (potentially during) will
+     * result in {@link IllegalStateException} being thrown.
+     * <p>
+     *     This operation is atomic with respect to other shutdown calls
+     * </p>
+     * <p>
+     *     After this call has completed, regardless of return value, this queue will be shutdown.
+     * </p>
+     *
+     * @return {@code true} if the queue was shutdown, {@code false} if it has shut down already
+     * @throws UnsupportedOperationException If this queue does not support shutdown
+     * @see #isShutdown()
+     */
+    public default boolean shutdown() throws UnsupportedOperationException {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Returns whether this queue has shut down. Effectively, whether new tasks will be rejected - this method
+     * does not indicate whether all the tasks scheduled have been executed.
+     * @return Returns whether this queue has shut down.
+     * @see #waitUntilAllExecuted()
+     */
+    public default boolean isShutdown() {
+        return false;
+    }
+
+    /**
+     * Task object returned for any {@link BaseExecutor} scheduled task.
+     * @see BaseExecutor
+     */
+    public static interface BaseTask extends Cancellable {
+
+        /**
+         * Causes a lazily queued task to become queued or executed
+         *
+         * @throws IllegalStateException If the backing queue has shutdown
+         * @return {@code true} If the task was queued, {@code false} if the task was already queued/cancelled/executed
+         */
+        public boolean queue();
+
+        /**
+         * Forces this task to be marked as completed.
+         *
+         * @return {@code true} if the task was cancelled, {@code false} if the task has already completed or is being completed.
+         */
+        @Override
+        public boolean cancel();
+
+        /**
+         * Executes this task. This will also mark the task as completing.
+         * <p>
+         *     Exceptions thrown from the runnable will be rethrown.
+         * </p>
+         *
+         * @return {@code true} if this task was executed, {@code false} if it was already marked as completed.
+         */
+        public boolean execute();
+    }
+}
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/Cancellable.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/Cancellable.java
new file mode 100644
index 0000000000000000000000000000000000000000..11449056361bb6c5a055f543cdd135c4113757c6
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/Cancellable.java
@@ -0,0 +1,14 @@
+package ca.spottedleaf.concurrentutil.executor;
+
+/**
+ * Interface specifying that something can be cancelled.
+ */
+public interface Cancellable {
+
+    /**
+     * Tries to cancel this task. If the task is in a stage that is too late to be cancelled, then this function
+     * will return {@code false}. If the task is already cancelled, then this function returns {@code false}. Only
+     * when this function successfully stops this task from being completed will it return {@code true}.
+     */
+    public boolean cancel();
+}
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/DelayedPrioritisedTask.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/DelayedPrioritisedTask.java
new file mode 100644
index 0000000000000000000000000000000000000000..3ce10053d4ec51855ad7012abb5d97df1c0e557a
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/DelayedPrioritisedTask.java
@@ -0,0 +1,170 @@
+package ca.spottedleaf.concurrentutil.executor.standard;
+
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
+import java.lang.invoke.VarHandle;
+
+public class DelayedPrioritisedTask {
+
+    protected volatile int priority;
+    protected static final VarHandle PRIORITY_HANDLE = ConcurrentUtil.getVarHandle(DelayedPrioritisedTask.class, "priority", int.class);
+
+    protected static final int PRIORITY_SET = Integer.MIN_VALUE >>> 0;
+
+    protected final int getPriorityVolatile() {
+        return (int)PRIORITY_HANDLE.getVolatile((DelayedPrioritisedTask)this);
+    }
+
+    protected final int compareAndExchangePriorityVolatile(final int expect, final int update) {
+        return (int)PRIORITY_HANDLE.compareAndExchange((DelayedPrioritisedTask)this, (int)expect, (int)update);
+    }
+
+    protected final int getAndOrPriorityVolatile(final int val) {
+        return (int)PRIORITY_HANDLE.getAndBitwiseOr((DelayedPrioritisedTask)this, (int)val);
+    }
+
+    protected final void setPriorityPlain(final int val) {
+        PRIORITY_HANDLE.set((DelayedPrioritisedTask)this, (int)val);
+    }
+
+    protected volatile PrioritisedExecutor.PrioritisedTask task;
+    protected static final VarHandle TASK_HANDLE = ConcurrentUtil.getVarHandle(DelayedPrioritisedTask.class, "task", PrioritisedExecutor.PrioritisedTask.class);
+
+    protected PrioritisedExecutor.PrioritisedTask getTaskPlain() {
+        return (PrioritisedExecutor.PrioritisedTask)TASK_HANDLE.get((DelayedPrioritisedTask)this);
+    }
+
+    protected PrioritisedExecutor.PrioritisedTask getTaskVolatile() {
+        return (PrioritisedExecutor.PrioritisedTask)TASK_HANDLE.getVolatile((DelayedPrioritisedTask)this);
+    }
+
+    protected final PrioritisedExecutor.PrioritisedTask compareAndExchangeTaskVolatile(final PrioritisedExecutor.PrioritisedTask expect, final PrioritisedExecutor.PrioritisedTask update) {
+        return (PrioritisedExecutor.PrioritisedTask)TASK_HANDLE.compareAndExchange((DelayedPrioritisedTask)this, (PrioritisedExecutor.PrioritisedTask)expect, (PrioritisedExecutor.PrioritisedTask)update);
+    }
+
+    public DelayedPrioritisedTask(final PrioritisedExecutor.Priority priority) {
+        this.setPriorityPlain(priority.priority);
+    }
+
+    // only public for debugging
+    public int getPriorityInternal() {
+        return this.getPriorityVolatile();
+    }
+
+    public PrioritisedExecutor.PrioritisedTask getTask() {
+        return this.getTaskVolatile();
+    }
+
+    public void setTask(final PrioritisedExecutor.PrioritisedTask task) {
+        int priority = this.getPriorityVolatile();
+
+        if (this.compareAndExchangeTaskVolatile(null, task) != null) {
+            throw new IllegalStateException("setTask() called twice");
+        }
+
+        int failures = 0;
+        for (;;) {
+            task.setPriority(PrioritisedExecutor.Priority.getPriority(priority));
+
+            if (priority == (priority = this.compareAndExchangePriorityVolatile(priority, priority | PRIORITY_SET))) {
+                return;
+            }
+
+            ++failures;
+            for (int i = 0; i < failures; ++i) {
+                ConcurrentUtil.backoff();
+            }
+        }
+    }
+
+    public PrioritisedExecutor.Priority getPriority() {
+        final int priority = this.getPriorityVolatile();
+        if ((priority & PRIORITY_SET) != 0) {
+            return this.task.getPriority();
+        }
+
+        return PrioritisedExecutor.Priority.getPriority(priority);
+    }
+
+    public void raisePriority(final PrioritisedExecutor.Priority priority) {
+        if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
+            throw new IllegalArgumentException("Invalid priority " + priority);
+        }
+
+        int failures = 0;
+        for (int curr = this.getPriorityVolatile();;) {
+            if ((curr & PRIORITY_SET) != 0) {
+                this.getTaskPlain().raisePriority(priority);
+                return;
+            }
+
+            if (!priority.isLowerPriority(curr)) {
+                return;
+            }
+
+            if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) {
+                return;
+            }
+
+            // failed, retry
+
+            ++failures;
+            for (int i = 0; i < failures; ++i) {
+                ConcurrentUtil.backoff();
+            }
+        }
+    }
+
+    public void setPriority(final PrioritisedExecutor.Priority priority) {
+        if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
+            throw new IllegalArgumentException("Invalid priority " + priority);
+        }
+
+        int failures = 0;
+        for (int curr = this.getPriorityVolatile();;) {
+            if ((curr & PRIORITY_SET) != 0) {
+                this.getTaskPlain().setPriority(priority);
+                return;
+            }
+
+            if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) {
+                return;
+            }
+
+            // failed, retry
+
+            ++failures;
+            for (int i = 0; i < failures; ++i) {
+                ConcurrentUtil.backoff();
+            }
+        }
+    }
+
+    public void lowerPriority(final PrioritisedExecutor.Priority priority) {
+        if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
+            throw new IllegalArgumentException("Invalid priority " + priority);
+        }
+
+        int failures = 0;
+        for (int curr = this.getPriorityVolatile();;) {
+            if ((curr & PRIORITY_SET) != 0) {
+                this.getTaskPlain().lowerPriority(priority);
+                return;
+            }
+
+            if (!priority.isHigherPriority(curr)) {
+                return;
+            }
+
+            if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) {
+                return;
+            }
+
+            // failed, retry
+
+            ++failures;
+            for (int i = 0; i < failures; ++i) {
+                ConcurrentUtil.backoff();
+            }
+        }
+    }
+}
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedExecutor.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedExecutor.java
new file mode 100644
index 0000000000000000000000000000000000000000..91beb6f23f257cf265fe3150f760892e605f217a
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedExecutor.java
@@ -0,0 +1,276 @@
+package ca.spottedleaf.concurrentutil.executor.standard;
+
+import ca.spottedleaf.concurrentutil.executor.BaseExecutor;
+
+/**
+ * Implementation of {@link BaseExecutor} which schedules tasks to be executed by a given priority.
+ * @see BaseExecutor
+ */
+public interface PrioritisedExecutor extends BaseExecutor {
+
+    public static enum Priority {
+
+        /**
+         * Priority value indicating the task has completed or is being completed.
+         * This priority cannot be used to schedule tasks.
+         */
+        COMPLETING(-1),
+
+        /**
+         * Absolute highest priority, should only be used for when a task is blocking a time-critical thread.
+         */
+        BLOCKING(),
+
+        /**
+         * Should only be used for urgent but not time-critical tasks.
+         */
+        HIGHEST(),
+
+        /**
+         * Two priorities above normal.
+         */
+        HIGHER(),
+
+        /**
+         * One priority above normal.
+         */
+        HIGH(),
+
+        /**
+         * Default priority.
+         */
+        NORMAL(),
+
+        /**
+         * One priority below normal.
+         */
+        LOW(),
+
+        /**
+         * Two priorities below normal.
+         */
+        LOWER(),
+
+        /**
+         * Use for tasks that should eventually execute, but are not needed to.
+         */
+        LOWEST(),
+
+        /**
+         * Use for tasks that can be delayed indefinitely.
+         */
+        IDLE();
+
+        // returns whether the priority can be scheduled
+        public static boolean isValidPriority(final Priority priority) {
+            return priority != null && priority != Priority.COMPLETING;
+        }
+
+        // returns the higher priority of the two
+        public static Priority max(final Priority p1, final Priority p2) {
+            return p1.isHigherOrEqualPriority(p2) ? p1 : p2;
+        }
+
+        // returns the lower priroity of the two
+        public static Priority min(final Priority p1, final Priority p2) {
+            return p1.isLowerOrEqualPriority(p2) ? p1 : p2;
+        }
+
+        public boolean isHigherOrEqualPriority(final Priority than) {
+            return this.priority <= than.priority;
+        }
+
+        public boolean isHigherPriority(final Priority than) {
+            return this.priority < than.priority;
+        }
+
+        public boolean isLowerOrEqualPriority(final Priority than) {
+            return this.priority >= than.priority;
+        }
+
+        public boolean isLowerPriority(final Priority than) {
+            return this.priority > than.priority;
+        }
+
+        public boolean isHigherOrEqualPriority(final int than) {
+            return this.priority <= than;
+        }
+
+        public boolean isHigherPriority(final int than) {
+            return this.priority < than;
+        }
+
+        public boolean isLowerOrEqualPriority(final int than) {
+            return this.priority >= than;
+        }
+
+        public boolean isLowerPriority(final int than) {
+            return this.priority > than;
+        }
+
+        public static boolean isHigherOrEqualPriority(final int priority, final int than) {
+            return priority <= than;
+        }
+
+        public static boolean isHigherPriority(final int priority, final int than) {
+            return priority < than;
+        }
+
+        public static boolean isLowerOrEqualPriority(final int priority, final int than) {
+            return priority >= than;
+        }
+
+        public static boolean isLowerPriority(final int priority, final int than) {
+            return priority > than;
+        }
+
+        static final Priority[] PRIORITIES = Priority.values();
+
+        /** includes special priorities */
+        public static final int TOTAL_PRIORITIES = PRIORITIES.length;
+
+        public static final int TOTAL_SCHEDULABLE_PRIORITIES = TOTAL_PRIORITIES - 1;
+
+        public static Priority getPriority(final int priority) {
+            return PRIORITIES[priority + 1];
+        }
+
+        private static int priorityCounter;
+
+        private static int nextCounter() {
+            return priorityCounter++;
+        }
+
+        public final int priority;
+
+        Priority() {
+            this(nextCounter());
+        }
+
+        Priority(final int priority) {
+            this.priority = priority;
+        }
+    }
+
+    /**
+     * Executes the next available task.
+     * <p>
+     *     If there is a task with priority {@link PrioritisedExecutor.Priority#BLOCKING} available, then that such task is executed.
+     * </p>
+     * <p>
+     *     If there is a task with priority {@link PrioritisedExecutor.Priority#IDLE} available then that task is only executed
+     *     when there are no other tasks available with a higher priority.
+     * </p>
+     * <p>
+     *     If there are no tasks that have priority {@link PrioritisedExecutor.Priority#BLOCKING} or {@link PrioritisedExecutor.Priority#IDLE}, then
+     *     this function will be biased to execute tasks that have higher priorities.
+     * </p>
+     *
+     * @return {@code true} if a task was executed, {@code false} otherwise
+     * @throws IllegalStateException If the current thread is not allowed to execute a task
+     */
+    @Override
+    public boolean executeTask() throws IllegalStateException;
+
+    /**
+     * Queues or executes a task at {@link Priority#NORMAL} priority.
+     * @param task The task to run.
+     *
+     * @throws IllegalStateException If this queue has shutdown.
+     * @throws NullPointerException If the task is null
+     * @return {@code null} if the current thread immediately executed the task, else returns the prioritised task
+     * associated with the parameter
+     */
+    public default PrioritisedTask queueRunnable(final Runnable task) {
+        return this.queueRunnable(task, Priority.NORMAL);
+    }
+
+    /**
+     * Queues or executes a task.
+     *
+     * @param task The task to run.
+     * @param priority The priority for the task.
+     *
+     * @throws IllegalStateException If this queue has shutdown.
+     * @throws NullPointerException If the task is null
+     * @throws IllegalArgumentException If the priority is invalid.
+     * @return {@code null} if the current thread immediately executed the task, else returns the prioritised task
+     * associated with the parameter
+     */
+    public PrioritisedTask queueRunnable(final Runnable task, final Priority priority);
+
+    /**
+     * Creates, but does not execute or queue the task. The task must later be queued via {@link BaseTask#queue()}.
+     *
+     * @param task The task to run.
+     *
+     * @throws IllegalStateException If this queue has shutdown.
+     * @throws NullPointerException If the task is null
+     * @throws IllegalArgumentException If the priority is invalid.
+     * @throws UnsupportedOperationException If this executor does not support lazily queueing tasks
+     * @return The prioritised task associated with the parameters
+     */
+    public default PrioritisedTask createTask(final Runnable task) {
+        return this.createTask(task, Priority.NORMAL);
+    }
+
+    /**
+     * Creates, but does not execute or queue the task. The task must later be queued via {@link BaseTask#queue()}.
+     *
+     * @param task The task to run.
+     * @param priority The priority for the task.
+     *
+     * @throws IllegalStateException If this queue has shutdown.
+     * @throws NullPointerException If the task is null
+     * @throws IllegalArgumentException If the priority is invalid.
+     * @throws UnsupportedOperationException If this executor does not support lazily queueing tasks
+     * @return The prioritised task associated with the parameters
+     */
+    public PrioritisedTask createTask(final Runnable task, final Priority priority);
+
+    /**
+     * Extension of {@link ca.spottedleaf.concurrentutil.executor.BaseExecutor.BaseTask} which adds functions
+     * to retrieve and modify the task's associated priority.
+     *
+     * @see ca.spottedleaf.concurrentutil.executor.BaseExecutor.BaseTask
+     */
+    public static interface PrioritisedTask extends BaseTask {
+
+        /**
+         * Returns the current priority. Note that {@link Priority#COMPLETING} will be returned
+         * if this task is completing or has completed.
+         */
+        public Priority getPriority();
+
+        /**
+         * Attempts to set this task's priority level to the level specified.
+         *
+         * @param priority Specified priority level.
+         *
+         * @throws IllegalArgumentException If the priority is invalid
+         * @return {@code true} if successful, {@code false} if this task is completing or has completed or the queue
+         * this task was scheduled on was shutdown, or if the priority was already at the specified level.
+         */
+        public boolean setPriority(final Priority priority);
+
+        /**
+         * Attempts to raise the priority to the priority level specified.
+         *
+         * @param priority Priority specified
+         *
+         * @throws IllegalArgumentException If the priority is invalid
+         * @return {@code false} if the current task is completing, {@code true} if the priority was raised to the specified level or was already at the specified level or higher.
+         */
+        public boolean raisePriority(final Priority priority);
+
+        /**
+         * Attempts to lower the priority to the priority level specified.
+         *
+         * @param priority Priority specified
+         *
+         * @throws IllegalArgumentException If the priority is invalid
+         * @return {@code false} if the current task is completing, {@code true} if the priority was lowered to the specified level or was already at the specified level or lower.
+         */
+        public boolean lowerPriority(final Priority priority);
+    }
+}
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedQueueExecutorThread.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedQueueExecutorThread.java
new file mode 100644
index 0000000000000000000000000000000000000000..d1683ba6350e530373944f98192c0f2baf241e70
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedQueueExecutorThread.java
@@ -0,0 +1,301 @@
+package ca.spottedleaf.concurrentutil.executor.standard;
+
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import java.lang.invoke.VarHandle;
+import java.util.concurrent.locks.LockSupport;
+
+/**
+ * Thread which will continuously drain from a specified queue.
+ * <p>
+ *     Note: When using this thread, queue additions to the underlying {@link #queue} are not sufficient to get this thread
+ *     to execute the task. The function {@link #notifyTasks()} must be used after scheduling a task. For expected behaviour
+ *     of task scheduling (thread wakes up after tasks are scheduled), use the methods provided on {@link PrioritisedExecutor}
+ *     methods.
+ * </p>
+ */
+public class PrioritisedQueueExecutorThread extends Thread implements PrioritisedExecutor {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(PrioritisedQueueExecutorThread.class);
+
+    protected final PrioritisedExecutor queue;
+
+    protected volatile boolean threadShutdown;
+
+    protected volatile boolean threadParked;
+    protected static final VarHandle THREAD_PARKED_HANDLE = ConcurrentUtil.getVarHandle(PrioritisedQueueExecutorThread.class, "threadParked", boolean.class);
+
+    protected volatile boolean halted;
+
+    protected final long spinWaitTime;
+
+    static final long DEFAULT_SPINWAIT_TIME = (long)(0.1e6);// 0.1ms
+
+    public PrioritisedQueueExecutorThread(final PrioritisedExecutor queue) {
+        this(queue, DEFAULT_SPINWAIT_TIME); // 0.1ms
+    }
+
+    public PrioritisedQueueExecutorThread(final PrioritisedExecutor queue, final long spinWaitTime) { // in ns
+        this.queue = queue;
+        this.spinWaitTime = spinWaitTime;
+    }
+
+    @Override
+    public void run() {
+        final long spinWaitTime = this.spinWaitTime;
+
+        main_loop:
+        for (;;) {
+            this.pollTasks();
+
+            // spinwait
+
+            final long start = System.nanoTime();
+
+            for (;;) {
+                // If we are interrupted for any reason, park() will always return immediately. Clear so that we don't needlessly use cpu in such an event.
+                Thread.interrupted();
+                Thread.yield();
+                LockSupport.parkNanos("Spinwaiting on tasks", 10_000L); // 10us
+
+                if (this.pollTasks()) {
+                    // restart loop, found tasks
+                    continue main_loop;
+                }
+
+                if (this.handleClose()) {
+                    return; // we're done
+                }
+
+                if ((System.nanoTime() - start) >= spinWaitTime) {
+                    break;
+                }
+            }
+
+            if (this.handleClose()) {
+                return;
+            }
+
+            this.setThreadParkedVolatile(true);
+
+            // We need to parse here to avoid a race condition where a thread queues a task before we set parked to true
+            // (i.e it will not notify us)
+            if (this.pollTasks()) {
+                this.setThreadParkedVolatile(false);
+                continue;
+            }
+
+            if (this.handleClose()) {
+                return;
+            }
+
+            // we don't need to check parked before sleeping, but we do need to check parked in a do-while loop
+            // LockSupport.park() can fail for any reason
+            while (this.getThreadParkedVolatile()) {
+                Thread.interrupted();
+                LockSupport.park("Waiting on tasks");
+            }
+        }
+    }
+
+    /**
+     * Attempts to poll as many tasks as possible, returning when finished.
+     * @return Whether any tasks were executed.
+     */
+    protected boolean pollTasks() {
+        boolean ret = false;
+
+        for (;;) {
+            if (this.halted) {
+                break;
+            }
+            try {
+                if (!this.queue.executeTask()) {
+                    break;
+                }
+                ret = true;
+            } catch (final ThreadDeath death) {
+                throw death; // goodbye world...
+            } catch (final Throwable throwable) {
+                LOGGER.error("Exception thrown from prioritized runnable task in thread '" + this.getName() + "'", throwable);
+            }
+        }
+
+        return ret;
+    }
+
+    protected boolean handleClose() {
+        if (this.threadShutdown) {
+            this.pollTasks(); // this ensures we've emptied the queue
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Notify this thread that a task has been added to its queue
+     * @return {@code true} if this thread was waiting for tasks, {@code false} if it is executing tasks
+     */
+    public boolean notifyTasks() {
+        if (this.getThreadParkedVolatile() && this.exchangeThreadParkedVolatile(false)) {
+            LockSupport.unpark(this);
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public PrioritisedTask createTask(final Runnable task, final Priority priority) {
+        final PrioritisedTask queueTask = this.queue.createTask(task, priority);
+
+        // need to override queue() to notify us of tasks
+        return new PrioritisedTask() {
+            @Override
+            public Priority getPriority() {
+                return queueTask.getPriority();
+            }
+
+            @Override
+            public boolean setPriority(final Priority priority) {
+                return queueTask.setPriority(priority);
+            }
+
+            @Override
+            public boolean raisePriority(final Priority priority) {
+                return queueTask.raisePriority(priority);
+            }
+
+            @Override
+            public boolean lowerPriority(final Priority priority) {
+                return queueTask.lowerPriority(priority);
+            }
+
+            @Override
+            public boolean queue() {
+                final boolean ret = queueTask.queue();
+                if (ret) {
+                    PrioritisedQueueExecutorThread.this.notifyTasks();
+                }
+                return ret;
+            }
+
+            @Override
+            public boolean cancel() {
+                return queueTask.cancel();
+            }
+
+            @Override
+            public boolean execute() {
+                return queueTask.execute();
+            }
+        };
+    }
+
+    @Override
+    public PrioritisedTask queueRunnable(final Runnable task, final Priority priority) {
+        final PrioritisedTask ret = this.queue.queueRunnable(task, priority);
+
+        this.notifyTasks();
+
+        return ret;
+    }
+
+    @Override
+    public boolean haveAllTasksExecuted() {
+        return this.queue.haveAllTasksExecuted();
+    }
+
+    @Override
+    public long getTotalTasksExecuted() {
+        return this.queue.getTotalTasksExecuted();
+    }
+
+    @Override
+    public long getTotalTasksScheduled() {
+        return this.queue.getTotalTasksScheduled();
+    }
+
+    /**
+     * {@inheritDoc}
+     * @throws IllegalStateException If the current thread is {@code this} thread, or the underlying queue throws this exception.
+     */
+    @Override
+    public void waitUntilAllExecuted() throws IllegalStateException {
+        if (Thread.currentThread() == this) {
+            throw new IllegalStateException("Cannot block on our own queue");
+        }
+        this.queue.waitUntilAllExecuted();
+    }
+
+    /**
+     * {@inheritDoc}
+     * @throws IllegalStateException Always
+     */
+    @Override
+    public boolean executeTask() throws IllegalStateException {
+        throw new IllegalStateException();
+    }
+
+    /**
+     * Closes this queue executor's queue. Optionally waits for all tasks in queue to be executed if {@code wait} is true.
+     * <p>
+     *     This function is MT-Safe.
+     * </p>
+     * @param wait If this call is to wait until the queue is empty and there are no tasks executing in the queue.
+     * @param killQueue Whether to shutdown this thread's queue
+     * @return whether this thread shut down the queue
+     * @see #halt(boolean)
+     */
+    public boolean close(final boolean wait, final boolean killQueue) {
+        final boolean ret = killQueue && this.queue.shutdown();
+        this.threadShutdown = true;
+
+        // force thread to respond to the shutdown
+        this.setThreadParkedVolatile(false);
+        LockSupport.unpark(this);
+
+        if (wait) {
+            this.waitUntilAllExecuted();
+        }
+
+        return ret;
+    }
+
+
+    /**
+     * Causes this thread to exit without draining the queue. To ensure tasks are completed, use {@link #close(boolean, boolean)}.
+     * <p>
+     *     This is not safe to call with {@link #close(boolean, boolean)} if <code>wait = true</code>, in which case
+     *     the waiting thread may block indefinitely.
+     * </p>
+     * <p>
+     *     This function is MT-Safe.
+     * </p>
+     * @param killQueue Whether to shutdown this thread's queue
+     * @see #close(boolean, boolean)
+     */
+    public void halt(final boolean killQueue) {
+        if (killQueue) {
+            this.queue.shutdown();
+        }
+        this.threadShutdown = true;
+        this.halted = true;
+
+        // force thread to respond to the shutdown
+        this.setThreadParkedVolatile(false);
+        LockSupport.unpark(this);
+    }
+
+    protected final boolean getThreadParkedVolatile() {
+        return (boolean)THREAD_PARKED_HANDLE.getVolatile(this);
+    }
+
+    protected final boolean exchangeThreadParkedVolatile(final boolean value) {
+        return (boolean)THREAD_PARKED_HANDLE.getAndSet(this, value);
+    }
+
+    protected final void setThreadParkedVolatile(final boolean value) {
+        THREAD_PARKED_HANDLE.setVolatile(this, value);
+    }
+}
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadPool.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadPool.java
new file mode 100644
index 0000000000000000000000000000000000000000..2ba36e29d0d8693f2f5e6c6d195ca27f2a5099aa
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadPool.java
@@ -0,0 +1,632 @@
+package ca.spottedleaf.concurrentutil.executor.standard;
+
+import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.TreeSet;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.BiConsumer;
+
+public final class PrioritisedThreadPool {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(PrioritisedThreadPool.class);
+
+    private final PrioritisedThread[] threads;
+    private final TreeSet<PrioritisedPoolExecutorImpl> queues = new TreeSet<>(PrioritisedPoolExecutorImpl.comparator());
+    private final String name;
+    private final long queueMaxHoldTime;
+
+    private final ReferenceOpenHashSet<PrioritisedPoolExecutorImpl> nonShutdownQueues = new ReferenceOpenHashSet<>();
+    private final ReferenceOpenHashSet<PrioritisedPoolExecutorImpl> activeQueues = new ReferenceOpenHashSet<>();
+
+    private boolean shutdown;
+
+    private long schedulingIdGenerator;
+
+    private static final long DEFAULT_QUEUE_HOLD_TIME = (long)(5.0e6);
+
+    /**
+     * @param name Specified debug name of this thread pool
+     * @param threads The number of threads to use
+     */
+    public PrioritisedThreadPool(final String name, final int threads) {
+        this(name, threads, null);
+    }
+
+    /**
+     * @param name Specified debug name of this thread pool
+     * @param threads The number of threads to use
+     * @param threadModifier Invoked for each created thread with its incremental id before starting them
+     */
+    public PrioritisedThreadPool(final String name, final int threads, final BiConsumer<Thread, Integer> threadModifier) {
+        this(name, threads, threadModifier, DEFAULT_QUEUE_HOLD_TIME); // 5ms
+    }
+
+    /**
+     * @param name Specified debug name of this thread pool
+     * @param threads The number of threads to use
+     * @param threadModifier Invoked for each created thread with its incremental id before starting them
+     * @param queueHoldTime The maximum amount of time to spend executing tasks in a specific queue before attempting
+     *                      to switch to another queue, per thread
+     */
+    public PrioritisedThreadPool(final String name, final int threads, final BiConsumer<Thread, Integer> threadModifier,
+                                 final long queueHoldTime) { // in ns
+        if (threads <= 0) {
+            throw new IllegalArgumentException("Thread count must be > 0, not " + threads);
+        }
+        if (name == null) {
+            throw new IllegalArgumentException("Name cannot be null");
+        }
+        this.name = name;
+        this.queueMaxHoldTime = queueHoldTime;
+
+        this.threads = new PrioritisedThread[threads];
+        for (int i = 0; i < threads; ++i) {
+            this.threads[i] = new PrioritisedThread(this);
+
+            // set default attributes
+            this.threads[i].setName("Prioritised thread for pool '" + name + "' #" + i);
+            this.threads[i].setUncaughtExceptionHandler((final Thread thread, final Throwable throwable) -> {
+                LOGGER.error("Uncaught exception in thread " + thread.getName(), throwable);
+            });
+
+            // let thread modifier override defaults
+            if (threadModifier != null) {
+                threadModifier.accept(this.threads[i], Integer.valueOf(i));
+            }
+
+            // now the thread can start
+            this.threads[i].start();
+        }
+    }
+
+    /**
+     * Returns an array representing the threads backing this thread pool.
+     */
+    public Thread[] getThreads() {
+        return Arrays.copyOf(this.threads, this.threads.length, Thread[].class);
+    }
+
+    /**
+     * Creates and returns a {@link PrioritisedPoolExecutor} to schedule tasks onto. The returned executor will execute
+     * tasks on this thread pool only.
+     * @param name The debug name of the executor.
+     * @param minParallelism The minimum number of threads to be executing tasks from the returned executor
+     *                       before threads may be allocated to other queues in this thread pool.
+     * @param parallelism The maximum number of threads which may be executing tasks from the returned executor.
+     * @throws IllegalStateException If this thread pool is shut down
+     */
+    public PrioritisedPoolExecutor createExecutor(final String name, final int minParallelism, final int parallelism) {
+        synchronized (this.nonShutdownQueues) {
+            if (this.shutdown) {
+                throw new IllegalStateException("Queue is shutdown: " + this.toString());
+            }
+            final PrioritisedPoolExecutorImpl ret = new PrioritisedPoolExecutorImpl(
+                    this, name,
+                    Math.min(Math.max(1, parallelism), this.threads.length),
+                    Math.min(Math.max(0, minParallelism), this.threads.length)
+            );
+
+            this.nonShutdownQueues.add(ret);
+
+            synchronized (this.activeQueues) {
+                this.activeQueues.add(ret);
+            }
+
+            return ret;
+        }
+    }
+
+    /**
+     * Prevents creation of new queues, shutdowns all non-shutdown queues if specified
+     */
+    public void halt(final boolean shutdownQueues) {
+        synchronized (this.nonShutdownQueues) {
+            this.shutdown = true;
+        }
+        if (shutdownQueues) {
+            final ArrayList<PrioritisedPoolExecutorImpl> queuesToShutdown;
+            synchronized (this.nonShutdownQueues) {
+                this.shutdown = true;
+                queuesToShutdown = new ArrayList<>(this.nonShutdownQueues);
+            }
+
+            for (final PrioritisedPoolExecutorImpl queue : queuesToShutdown) {
+                queue.shutdown();
+            }
+        }
+
+
+        for (final PrioritisedThread thread : this.threads) {
+            // can't kill queue, queue is null
+            thread.halt(false);
+        }
+    }
+
+    /**
+     * Waits until all threads in this pool have shutdown, or until the specified time has passed.
+     * @param msToWait Maximum time to wait.
+     * @return {@code false} if the maximum time passed, {@code true} otherwise.
+     */
+    public boolean join(final long msToWait) {
+        try {
+            return this.join(msToWait, false);
+        } catch (final InterruptedException ex) {
+            throw new IllegalStateException(ex);
+        }
+    }
+
+    /**
+     * Waits until all threads in this pool have shutdown, or until the specified time has passed.
+     * @param msToWait Maximum time to wait.
+     * @return {@code false} if the maximum time passed, {@code true} otherwise.
+     * @throws InterruptedException If this thread is interrupted.
+     */
+    public boolean joinInterruptable(final long msToWait) throws InterruptedException {
+        return this.join(msToWait, true);
+    }
+
+    protected final boolean join(final long msToWait, final boolean interruptable) throws InterruptedException {
+        final long nsToWait = msToWait * (1000 * 1000);
+        final long start = System.nanoTime();
+        final long deadline = start + nsToWait;
+        boolean interrupted = false;
+        try {
+            for (final PrioritisedThread thread : this.threads) {
+                for (;;) {
+                    if (!thread.isAlive()) {
+                        break;
+                    }
+                    final long current = System.nanoTime();
+                    if (current >= deadline) {
+                        return false;
+                    }
+
+                    try {
+                        thread.join(Math.max(1L, (deadline - current) / (1000 * 1000)));
+                    } catch (final InterruptedException ex) {
+                        if (interruptable) {
+                            throw ex;
+                        }
+                        interrupted = true;
+                    }
+                }
+            }
+
+            return true;
+        } finally {
+            if (interrupted) {
+                Thread.currentThread().interrupt();
+            }
+        }
+    }
+
+    /**
+     * Shuts down this thread pool, optionally waiting for all tasks to be executed.
+     * This function will invoke {@link PrioritisedPoolExecutor#shutdown()} on all created executors on this
+     * thread pool.
+     * @param wait Whether to wait for tasks to be executed
+     */
+    public void shutdown(final boolean wait) {
+        final ArrayList<PrioritisedPoolExecutorImpl> queuesToShutdown;
+        synchronized (this.nonShutdownQueues) {
+            this.shutdown = true;
+            queuesToShutdown = new ArrayList<>(this.nonShutdownQueues);
+        }
+
+        for (final PrioritisedPoolExecutorImpl queue : queuesToShutdown) {
+            queue.shutdown();
+        }
+
+        for (final PrioritisedThread thread : this.threads) {
+            // none of these can be true or else NPE
+            thread.close(false, false);
+        }
+
+        if (wait) {
+            final ArrayList<PrioritisedPoolExecutorImpl> queues;
+            synchronized (this.activeQueues) {
+                queues = new ArrayList<>(this.activeQueues);
+            }
+            for (final PrioritisedPoolExecutorImpl queue : queues) {
+                queue.waitUntilAllExecuted();
+            }
+        }
+    }
+
+    protected static final class PrioritisedThread extends PrioritisedQueueExecutorThread {
+
+        protected final PrioritisedThreadPool pool;
+        protected final AtomicBoolean alertedHighPriority = new AtomicBoolean();
+
+        public PrioritisedThread(final PrioritisedThreadPool pool) {
+            super(null);
+            this.pool = pool;
+        }
+
+        public boolean alertHighPriorityExecutor() {
+            if (!this.notifyTasks()) {
+                if (!this.alertedHighPriority.get()) {
+                    this.alertedHighPriority.set(true);
+                }
+                return false;
+            }
+
+            return true;
+        }
+
+        private boolean isAlertedHighPriority() {
+            return this.alertedHighPriority.get() && this.alertedHighPriority.getAndSet(false);
+        }
+
+        @Override
+        protected boolean pollTasks() {
+            final PrioritisedThreadPool pool = this.pool;
+            final TreeSet<PrioritisedPoolExecutorImpl> queues = this.pool.queues;
+
+            boolean ret = false;
+            for (;;) {
+                if (this.halted) {
+                    break;
+                }
+                // try to find a queue
+                // note that if and ONLY IF the queues set is empty, this means there are no tasks for us to execute.
+                // so we can only break when it's empty
+                final PrioritisedPoolExecutorImpl queue;
+                // select queue
+                synchronized (queues) {
+                    queue = queues.pollFirst();
+                    if (queue == null) {
+                        // no tasks to execute
+                        break;
+                    }
+
+                    queue.schedulingId = ++pool.schedulingIdGenerator;
+                    // we own this queue now, so increment the executor count
+                    // do we also need to push this queue up for grabs for another executor?
+                    if (++queue.concurrentExecutors < queue.maximumExecutors) {
+                        // re-add to queues
+                        // it's very important this is done in the same synchronised block for polling, as this prevents
+                        // us from possibly later adding a queue that should not exist in the set
+                        queues.add(queue);
+                        queue.isQueued = true;
+                    } else {
+                        queue.isQueued = false;
+                    }
+                    // note: we cannot drain entries from the queue while holding this lock, as it will cause deadlock
+                    // the queue addition holds the per-queue lock first then acquires the lock we have now, but if we
+                    // try to poll now we don't hold the per queue lock but we do hold the global lock...
+                }
+
+                // parse tasks as long as we are allowed
+                final long start = System.nanoTime();
+                final long deadline = start + pool.queueMaxHoldTime;
+                do {
+                    try {
+                        if (this.halted) {
+                            break;
+                        }
+                        if (!queue.executeTask()) {
+                            // no more tasks, try next queue
+                            break;
+                        }
+                        ret = true;
+                    } catch (final ThreadDeath death) {
+                        throw death; // goodbye world...
+                    } catch (final Throwable throwable) {
+                        LOGGER.error("Exception thrown from thread '" + this.getName() + "' in queue '" + queue.toString() + "'", throwable);
+                    }
+                } while (!this.isAlertedHighPriority() && System.nanoTime() <= deadline);
+
+                synchronized (queues) {
+                    // decrement executors, we are no longer executing
+                    if (queue.isQueued) {
+                        queues.remove(queue);
+                        queue.isQueued = false;
+                    }
+                    if (--queue.concurrentExecutors == 0 && queue.scheduledPriority == null) {
+                        // reset scheduling id once the queue is empty again
+                        // this will ensure empty queues are not prioritised suddenly over active queues once tasks are
+                        // queued
+                        queue.schedulingId = 0L;
+                    }
+
+                    // ensure the executor is queued for execution again
+                    if (!queue.isHalted && queue.scheduledPriority != null) { // make sure it actually has tasks
+                        queues.add(queue);
+                        queue.isQueued = true;
+                    }
+                }
+            }
+
+            return ret;
+        }
+    }
+
+    public interface PrioritisedPoolExecutor extends PrioritisedExecutor {
+
+        /**
+         * Removes this queue from the thread pool without shutting the queue down or waiting for queued tasks to be executed
+         */
+        public void halt();
+
+        /**
+         * Returns whether this executor is scheduled to run tasks or is running tasks, otherwise it returns whether
+         * this queue is not halted and not shutdown.
+         */
+        public boolean isActive();
+    }
+
+    protected static final class PrioritisedPoolExecutorImpl extends PrioritisedThreadedTaskQueue implements PrioritisedPoolExecutor {
+
+        protected final PrioritisedThreadPool pool;
+        protected final long[] priorityCounts = new long[Priority.TOTAL_SCHEDULABLE_PRIORITIES];
+        protected long schedulingId;
+        protected int concurrentExecutors;
+        protected Priority scheduledPriority;
+
+        protected final String name;
+        protected final int maximumExecutors;
+        protected final int minimumExecutors;
+        protected boolean isQueued;
+
+        public PrioritisedPoolExecutorImpl(final PrioritisedThreadPool pool, final String name, final int maximumExecutors, final int minimumExecutors) {
+            this.pool = pool;
+            this.name = name;
+            this.maximumExecutors = maximumExecutors;
+            this.minimumExecutors = minimumExecutors;
+        }
+
+        public static Comparator<PrioritisedPoolExecutorImpl> comparator() {
+            return (final PrioritisedPoolExecutorImpl p1, final PrioritisedPoolExecutorImpl p2) -> {
+                if (p1 == p2) {
+                    return 0;
+                }
+
+                final int belowMin1 = p1.minimumExecutors - p1.concurrentExecutors;
+                final int belowMin2 = p2.minimumExecutors - p2.concurrentExecutors;
+
+                // test minimum executors
+                if (belowMin1 > 0 || belowMin2 > 0) {
+                    // want the largest belowMin to be first
+                    final int minCompare = Integer.compare(belowMin2, belowMin1);
+
+                    if (minCompare != 0) {
+                        return minCompare;
+                    }
+                }
+
+                // prefer higher priority
+                final int priorityCompare = p1.scheduledPriority.ordinal() - p2.scheduledPriority.ordinal();
+                if (priorityCompare != 0) {
+                    return priorityCompare;
+                }
+
+                // try to spread out the executors so that each can have threads executing
+                final int executorCompare = p1.concurrentExecutors - p2.concurrentExecutors;
+                if (executorCompare != 0) {
+                    return executorCompare;
+                }
+
+                // if all else fails here we just choose whichever executor was queued first
+                return Long.compare(p1.schedulingId, p2.schedulingId);
+            };
+        }
+
+        private boolean isHalted;
+
+        @Override
+        public void halt() {
+            final PrioritisedThreadPool pool = this.pool;
+            final TreeSet<PrioritisedPoolExecutorImpl> queues = pool.queues;
+            synchronized (queues) {
+                if (this.isHalted) {
+                    return;
+                }
+                this.isHalted = true;
+                if (this.isQueued) {
+                    queues.remove(this);
+                    this.isQueued = false;
+                }
+            }
+            synchronized (pool.nonShutdownQueues) {
+                pool.nonShutdownQueues.remove(this);
+            }
+            synchronized (pool.activeQueues) {
+                pool.activeQueues.remove(this);
+            }
+        }
+
+        @Override
+        public boolean isActive() {
+            final PrioritisedThreadPool pool = this.pool;
+            final TreeSet<PrioritisedPoolExecutorImpl> queues = pool.queues;
+
+            synchronized (queues) {
+                if (this.concurrentExecutors != 0) {
+                    return true;
+                }
+                synchronized (pool.activeQueues) {
+                    if (pool.activeQueues.contains(this)) {
+                        return true;
+                    }
+                }
+            }
+
+            return false;
+        }
+
+        private long totalQueuedTasks = 0L;
+
+        @Override
+        protected void priorityChange(final PrioritisedThreadedTaskQueue.PrioritisedTask task, final Priority from, final Priority to) {
+            // Note: The superclass' queue lock is ALWAYS held when inside this method. So we do NOT need to do any additional synchronisation
+            // for accessing this queue's state.
+            final long[] priorityCounts = this.priorityCounts;
+            final boolean shutdown = this.isShutdown();
+
+            if (from == null && to == Priority.COMPLETING) {
+                throw new IllegalStateException("Cannot complete task without queueing it first");
+            }
+
+            // we should only notify for queueing of tasks, not changing priorities
+            final boolean shouldNotifyTasks = from == null;
+
+            final Priority scheduledPriority = this.scheduledPriority;
+            if (from != null) {
+                --priorityCounts[from.priority];
+            }
+            if (to != Priority.COMPLETING) {
+                ++priorityCounts[to.priority];
+            }
+            final long totalQueuedTasks;
+            if (to == Priority.COMPLETING) {
+                totalQueuedTasks = --this.totalQueuedTasks;
+            } else if (from == null) {
+                totalQueuedTasks = ++this.totalQueuedTasks;
+            } else {
+                totalQueuedTasks = this.totalQueuedTasks;
+            }
+
+            // find new highest priority
+            int highest = Math.min(to == Priority.COMPLETING ? Priority.IDLE.priority : to.priority, scheduledPriority == null ? Priority.IDLE.priority : scheduledPriority.priority);
+            int lowestPriority = priorityCounts.length; // exclusive
+            for (;highest < lowestPriority; ++highest) {
+                final long count = priorityCounts[highest];
+                if (count < 0) {
+                    throw new IllegalStateException("Priority " + highest + " has " + count + " scheduled tasks");
+                }
+
+                if (count != 0) {
+                    break;
+                }
+            }
+
+            final Priority newPriority;
+            if (highest == lowestPriority) {
+                // no tasks left
+                newPriority = null;
+            } else if (shutdown) {
+                // whichever is lower, the actual greatest priority or simply HIGHEST
+                // this is so shutdown automatically gets priority
+                newPriority = Priority.getPriority(Math.min(highest, Priority.HIGHEST.priority));
+            } else {
+                newPriority = Priority.getPriority(highest);
+            }
+
+            final int executorsWanted;
+            boolean shouldNotifyHighPriority = false;
+
+            final PrioritisedThreadPool pool = this.pool;
+            final TreeSet<PrioritisedPoolExecutorImpl> queues = pool.queues;
+
+            synchronized (queues) {
+                if (!this.isQueued) {
+                    // see if we need to be queued
+                    if (newPriority != null) {
+                        if (this.schedulingId == 0L) {
+                            this.schedulingId = ++pool.schedulingIdGenerator;
+                        }
+                        this.scheduledPriority = newPriority; // must be updated before queue add
+                        if (!this.isHalted && this.concurrentExecutors < this.maximumExecutors) {
+                            shouldNotifyHighPriority = newPriority.isHigherOrEqualPriority(Priority.HIGH);
+                            queues.add(this);
+                            this.isQueued = true;
+                        }
+                    } else {
+                        // do not queue
+                        this.scheduledPriority = null;
+                    }
+                } else {
+                    // see if we need to NOT be queued
+                    if (newPriority == null) {
+                        queues.remove(this);
+                        this.scheduledPriority = null;
+                        this.isQueued = false;
+                    } else if (scheduledPriority != newPriority) {
+                        // if our priority changed, we need to update it - which means removing and re-adding into the queue
+                        queues.remove(this);
+                        // only now can we update scheduledPriority, since we are no longer in queue
+                        this.scheduledPriority = newPriority;
+                        queues.add(this);
+                        shouldNotifyHighPriority = (scheduledPriority == null || scheduledPriority.isLowerPriority(Priority.HIGH)) && newPriority.isHigherOrEqualPriority(Priority.HIGH);
+                    }
+                }
+
+                if (this.isQueued) {
+                    executorsWanted = Math.min(this.maximumExecutors - this.concurrentExecutors, (int)totalQueuedTasks);
+                } else {
+                    executorsWanted = 0;
+                }
+            }
+
+            if (newPriority == null && shutdown) {
+                synchronized (pool.activeQueues) {
+                    pool.activeQueues.remove(this);
+                }
+            }
+
+            // Wake up the number of executors we want
+            if (executorsWanted > 0 || (shouldNotifyTasks | shouldNotifyHighPriority)) {
+                int notified = 0;
+                for (final PrioritisedThread thread : pool.threads) {
+                    if ((shouldNotifyHighPriority ? thread.alertHighPriorityExecutor() : thread.notifyTasks())
+                            && (++notified >= executorsWanted)) {
+                        break;
+                    }
+                }
+            }
+        }
+
+        @Override
+        public boolean shutdown() {
+            final boolean ret = super.shutdown();
+            if (!ret) {
+                return ret;
+            }
+
+            final PrioritisedThreadPool pool = this.pool;
+
+            // remove from active queues
+            synchronized (pool.nonShutdownQueues) {
+                pool.nonShutdownQueues.remove(this);
+            }
+
+            final TreeSet<PrioritisedPoolExecutorImpl> queues = pool.queues;
+
+            // try and shift around our priority
+            synchronized (queues) {
+                if (this.scheduledPriority == null) {
+                    // no tasks are queued, ensure we aren't in activeQueues
+                    synchronized (pool.activeQueues) {
+                        pool.activeQueues.remove(this);
+                    }
+
+                    return ret;
+                }
+
+                // try to set scheduled priority to HIGHEST so it drains faster
+
+                if (this.scheduledPriority.isHigherOrEqualPriority(Priority.HIGHEST)) {
+                    // already at target priority (highest or above)
+                    return ret;
+                }
+
+                // shift priority to HIGHEST
+
+                if (this.isQueued) {
+                    queues.remove(this);
+                    this.scheduledPriority = Priority.HIGHEST;
+                    queues.add(this);
+                } else {
+                    this.scheduledPriority = Priority.HIGHEST;
+                }
+            }
+
+            return ret;
+        }
+    }
+}
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadedTaskQueue.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadedTaskQueue.java
new file mode 100644
index 0000000000000000000000000000000000000000..3e8401b1b1f833c4f01bc87059a2f48d761d989f
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadedTaskQueue.java
@@ -0,0 +1,378 @@
+package ca.spottedleaf.concurrentutil.executor.standard;
+
+import java.util.ArrayDeque;
+import java.util.concurrent.atomic.AtomicLong;
+
+public class PrioritisedThreadedTaskQueue implements PrioritisedExecutor {
+
+    protected final ArrayDeque<PrioritisedTask>[] queues = new ArrayDeque[Priority.TOTAL_SCHEDULABLE_PRIORITIES]; {
+        for (int i = 0; i < Priority.TOTAL_SCHEDULABLE_PRIORITIES; ++i) {
+            this.queues[i] = new ArrayDeque<>();
+        }
+    }
+
+    // Use AtomicLong to separate from the queue field, we don't want false sharing here.
+    protected final AtomicLong totalScheduledTasks = new AtomicLong();
+    protected final AtomicLong totalCompletedTasks = new AtomicLong();
+
+    // this is here to prevent failures to queue stalling flush() calls (as the schedule calls would increment totalScheduledTasks without this check)
+    protected volatile boolean hasShutdown;
+
+    protected long taskIdGenerator = 0;
+
+    @Override
+    public PrioritisedExecutor.PrioritisedTask queueRunnable(final Runnable task, final Priority priority) throws IllegalStateException, IllegalArgumentException {
+        if (!Priority.isValidPriority(priority)) {
+            throw new IllegalArgumentException("Priority " + priority + " is invalid");
+        }
+        if (task == null) {
+            throw new NullPointerException("Task cannot be null");
+        }
+
+        if (this.hasShutdown) {
+            // prevent us from stalling flush() calls by incrementing scheduled tasks when we really didn't schedule something
+            throw new IllegalStateException("Queue has shutdown");
+        }
+
+        final PrioritisedTask ret;
+
+        synchronized (this.queues) {
+            if (this.hasShutdown) {
+                throw new IllegalStateException("Queue has shutdown");
+            }
+            this.getAndAddTotalScheduledTasksVolatile(1L);
+
+            ret = new PrioritisedTask(this.taskIdGenerator++, task, priority, this);
+
+            this.queues[ret.priority.priority].add(ret);
+
+            // call priority change callback (note: only after we successfully queue!)
+            this.priorityChange(ret, null, priority);
+        }
+
+        return ret;
+    }
+
+    @Override
+    public PrioritisedExecutor.PrioritisedTask createTask(final Runnable task, final Priority priority) {
+        if (!Priority.isValidPriority(priority)) {
+            throw new IllegalArgumentException("Priority " + priority + " is invalid");
+        }
+        if (task == null) {
+            throw new NullPointerException("Task cannot be null");
+        }
+
+        return new PrioritisedTask(task, priority, this);
+    }
+
+    @Override
+    public long getTotalTasksScheduled() {
+        return this.totalScheduledTasks.get();
+    }
+
+    @Override
+    public long getTotalTasksExecuted() {
+        return this.totalCompletedTasks.get();
+    }
+
+    // callback method for subclasses to override
+    // from is null when a task is immediately created
+    protected void priorityChange(final PrioritisedTask task, final Priority from, final Priority to) {}
+
+    /**
+     * Polls the highest priority task currently available. {@code null} if none. This will mark the
+     * returned task as completed.
+     */
+    protected PrioritisedTask poll() {
+        return this.poll(Priority.IDLE);
+    }
+
+    protected PrioritisedTask poll(final Priority minPriority) {
+        final ArrayDeque<PrioritisedTask>[] queues = this.queues;
+        synchronized (queues) {
+            final int max = minPriority.priority;
+            for (int i = 0; i <= max; ++i) {
+                final ArrayDeque<PrioritisedTask> queue = queues[i];
+                PrioritisedTask task;
+                while ((task = queue.pollFirst()) != null) {
+                    if (task.trySetCompleting(i)) {
+                        return task;
+                    }
+                }
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Polls and executes the highest priority task currently available. Exceptions thrown during task execution will
+     * be rethrown.
+     * @return {@code true} if a task was executed, {@code false} otherwise.
+     */
+    @Override
+    public boolean executeTask() {
+        final PrioritisedTask task = this.poll();
+
+        if (task != null) {
+            task.executeInternal();
+            return true;
+        }
+
+        return false;
+    }
+
+    @Override
+    public boolean shutdown() {
+        synchronized (this.queues) {
+            if (this.hasShutdown) {
+                return false;
+            }
+            this.hasShutdown = true;
+        }
+        return true;
+    }
+
+    @Override
+    public boolean isShutdown() {
+        return this.hasShutdown;
+    }
+
+    /* totalScheduledTasks */
+
+    protected final long getTotalScheduledTasksVolatile() {
+        return this.totalScheduledTasks.get();
+    }
+
+    protected final long getAndAddTotalScheduledTasksVolatile(final long value) {
+        return this.totalScheduledTasks.getAndAdd(value);
+    }
+
+    /* totalCompletedTasks */
+
+    protected final long getTotalCompletedTasksVolatile() {
+        return this.totalCompletedTasks.get();
+    }
+
+    protected final long getAndAddTotalCompletedTasksVolatile(final long value) {
+        return this.totalCompletedTasks.getAndAdd(value);
+    }
+
+    protected static final class PrioritisedTask implements PrioritisedExecutor.PrioritisedTask {
+        protected final PrioritisedThreadedTaskQueue queue;
+        protected long id;
+        protected static final long NOT_SCHEDULED_ID = -1L;
+
+        protected Runnable runnable;
+        protected volatile Priority priority;
+
+        protected PrioritisedTask(final long id, final Runnable runnable, final Priority priority, final PrioritisedThreadedTaskQueue queue) {
+            if (!Priority.isValidPriority(priority)) {
+                throw new IllegalArgumentException("Invalid priority " + priority);
+            }
+
+            this.priority = priority;
+            this.runnable = runnable;
+            this.queue = queue;
+            this.id = id;
+        }
+
+        protected PrioritisedTask(final Runnable runnable, final Priority priority, final PrioritisedThreadedTaskQueue queue) {
+            if (!Priority.isValidPriority(priority)) {
+                throw new IllegalArgumentException("Invalid priority " + priority);
+            }
+
+            this.priority = priority;
+            this.runnable = runnable;
+            this.queue = queue;
+            this.id = NOT_SCHEDULED_ID;
+        }
+
+        @Override
+        public boolean queue() {
+            if (this.queue.hasShutdown) {
+                throw new IllegalStateException("Queue has shutdown");
+            }
+
+            synchronized (this.queue.queues) {
+                if (this.queue.hasShutdown) {
+                    throw new IllegalStateException("Queue has shutdown");
+                }
+
+                final Priority priority = this.priority;
+                if (priority == Priority.COMPLETING) {
+                    return false;
+                }
+
+                if (this.id != NOT_SCHEDULED_ID) {
+                    return false;
+                }
+
+                this.queue.getAndAddTotalScheduledTasksVolatile(1L);
+                this.id = this.queue.taskIdGenerator++;
+                this.queue.queues[priority.priority].add(this);
+
+                this.queue.priorityChange(this, null, priority);
+
+                return true;
+            }
+        }
+
+        protected boolean trySetCompleting(final int minPriority) {
+            final Priority oldPriority = this.priority;
+            if (oldPriority != Priority.COMPLETING && oldPriority.isHigherOrEqualPriority(minPriority)) {
+                this.priority = Priority.COMPLETING;
+                if (this.id != NOT_SCHEDULED_ID) {
+                    this.queue.priorityChange(this, oldPriority, Priority.COMPLETING);
+                }
+                return true;
+            }
+
+            return false;
+        }
+
+        @Override
+        public Priority getPriority() {
+            return this.priority;
+        }
+
+        @Override
+        public boolean setPriority(final Priority priority) {
+            if (!Priority.isValidPriority(priority)) {
+                throw new IllegalArgumentException("Invalid priority " + priority);
+            }
+            synchronized (this.queue.queues) {
+                final Priority curr = this.priority;
+
+                if (curr == Priority.COMPLETING) {
+                    return false;
+                }
+
+                if (curr == priority) {
+                    return true;
+                }
+
+                this.priority = priority;
+                if (this.id != NOT_SCHEDULED_ID) {
+                    this.queue.queues[priority.priority].add(this);
+
+                    // call priority change callback
+                    this.queue.priorityChange(this, curr, priority);
+                }
+            }
+
+            return true;
+        }
+
+        @Override
+        public boolean raisePriority(final Priority priority) {
+            if (!Priority.isValidPriority(priority)) {
+                throw new IllegalArgumentException("Invalid priority " + priority);
+            }
+
+            synchronized (this.queue.queues) {
+                final Priority curr = this.priority;
+
+                if (curr == Priority.COMPLETING) {
+                    return false;
+                }
+
+                if (curr.isHigherOrEqualPriority(priority)) {
+                    return true;
+                }
+
+                this.priority = priority;
+                if (this.id != NOT_SCHEDULED_ID) {
+                    this.queue.queues[priority.priority].add(this);
+
+                    // call priority change callback
+                    this.queue.priorityChange(this, curr, priority);
+                }
+            }
+
+            return true;
+        }
+
+        @Override
+        public boolean lowerPriority(final Priority priority) {
+            if (!Priority.isValidPriority(priority)) {
+                throw new IllegalArgumentException("Invalid priority " + priority);
+            }
+
+            synchronized (this.queue.queues) {
+                final Priority curr = this.priority;
+
+                if (curr == Priority.COMPLETING) {
+                    return false;
+                }
+
+                if (curr.isLowerOrEqualPriority(priority)) {
+                    return true;
+                }
+
+                this.priority = priority;
+                if (this.id != NOT_SCHEDULED_ID) {
+                    this.queue.queues[priority.priority].add(this);
+
+                    // call priority change callback
+                    this.queue.priorityChange(this, curr, priority);
+                }
+            }
+
+            return true;
+        }
+
+        @Override
+        public boolean cancel() {
+            final long id;
+            synchronized (this.queue.queues) {
+                final Priority oldPriority = this.priority;
+                if (oldPriority == Priority.COMPLETING) {
+                    return false;
+                }
+
+                this.priority = Priority.COMPLETING;
+                // call priority change callback
+                if ((id = this.id) != NOT_SCHEDULED_ID) {
+                    this.queue.priorityChange(this, oldPriority, Priority.COMPLETING);
+                }
+            }
+            this.runnable = null;
+            if (id != NOT_SCHEDULED_ID) {
+                this.queue.getAndAddTotalCompletedTasksVolatile(1L);
+            }
+            return true;
+        }
+
+        protected void executeInternal() {
+            try {
+                final Runnable execute = this.runnable;
+                this.runnable = null;
+                execute.run();
+            } finally {
+                if (this.id != NOT_SCHEDULED_ID) {
+                    this.queue.getAndAddTotalCompletedTasksVolatile(1L);
+                }
+            }
+        }
+
+        @Override
+        public boolean execute() {
+            synchronized (this.queue.queues) {
+                final Priority oldPriority = this.priority;
+                if (oldPriority == Priority.COMPLETING) {
+                    return false;
+                }
+
+                this.priority = Priority.COMPLETING;
+                // call priority change callback
+                if (this.id != NOT_SCHEDULED_ID) {
+                    this.queue.priorityChange(this, oldPriority, Priority.COMPLETING);
+                }
+            }
+
+            this.executeInternal();
+            return true;
+        }
+    }
+}
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/function/BiLong1Function.java b/src/main/java/ca/spottedleaf/concurrentutil/function/BiLong1Function.java
new file mode 100644
index 0000000000000000000000000000000000000000..94bfd7c56ffcea7d6491e94a7804bc3bd60fe9c3
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/concurrentutil/function/BiLong1Function.java
@@ -0,0 +1,8 @@
+package ca.spottedleaf.concurrentutil.function;
+
+@FunctionalInterface
+public interface BiLong1Function<T, R> {
+
+    public R apply(final long t1, final T t2);
+
+}
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/function/BiLongObjectConsumer.java b/src/main/java/ca/spottedleaf/concurrentutil/function/BiLongObjectConsumer.java
new file mode 100644
index 0000000000000000000000000000000000000000..8e7eef07960a18d0593688eba55adfa1c85efadf
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/concurrentutil/function/BiLongObjectConsumer.java
@@ -0,0 +1,8 @@
+package ca.spottedleaf.concurrentutil.function;
+
+@FunctionalInterface
+public interface BiLongObjectConsumer<V> {
+
+    public void accept(final long key, final V value);
+
+}
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/lock/ReentrantAreaLock.java b/src/main/java/ca/spottedleaf/concurrentutil/lock/ReentrantAreaLock.java
new file mode 100644
index 0000000000000000000000000000000000000000..7ffe4379b06c03c56abbcbdee3bb720894a10702
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/concurrentutil/lock/ReentrantAreaLock.java
@@ -0,0 +1,350 @@
+package ca.spottedleaf.concurrentutil.lock;
+
+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
+import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable;
+import ca.spottedleaf.concurrentutil.util.IntPairUtil;
+import java.util.Objects;
+import java.util.concurrent.locks.LockSupport;
+
+public final class ReentrantAreaLock {
+
+    public final int coordinateShift;
+
+    // aggressive load factor to reduce contention
+    private final ConcurrentLong2ReferenceChainedHashTable<Node> nodes = ConcurrentLong2ReferenceChainedHashTable.createWithCapacity(128, 0.2f);
+
+    public ReentrantAreaLock(final int coordinateShift) {
+        this.coordinateShift = coordinateShift;
+    }
+
+    public boolean isHeldByCurrentThread(final int x, final int z) {
+        final Thread currThread = Thread.currentThread();
+        final int shift = this.coordinateShift;
+        final int sectionX = x >> shift;
+        final int sectionZ = z >> shift;
+
+        final long coordinate = IntPairUtil.key(sectionX, sectionZ);
+        final Node node = this.nodes.get(coordinate);
+
+        return node != null && node.thread == currThread;
+    }
+
+    public boolean isHeldByCurrentThread(final int centerX, final int centerZ, final int radius) {
+        return this.isHeldByCurrentThread(centerX - radius, centerZ - radius, centerX + radius, centerZ + radius);
+    }
+
+    public boolean isHeldByCurrentThread(final int fromX, final int fromZ, final int toX, final int toZ) {
+        if (fromX > toX || fromZ > toZ) {
+            throw new IllegalArgumentException();
+        }
+
+        final Thread currThread = Thread.currentThread();
+        final int shift = this.coordinateShift;
+        final int fromSectionX = fromX >> shift;
+        final int fromSectionZ = fromZ >> shift;
+        final int toSectionX = toX >> shift;
+        final int toSectionZ = toZ >> shift;
+
+        for (int currZ = fromSectionZ; currZ <= toSectionZ; ++currZ) {
+            for (int currX = fromSectionX; currX <= toSectionX; ++currX) {
+                final long coordinate = IntPairUtil.key(currX, currZ);
+
+                final Node node = this.nodes.get(coordinate);
+
+                if (node == null || node.thread != currThread) {
+                    return false;
+                }
+            }
+        }
+
+        return true;
+    }
+
+    public Node tryLock(final int x, final int z) {
+        return this.tryLock(x, z, x, z);
+    }
+
+    public Node tryLock(final int centerX, final int centerZ, final int radius) {
+        return this.tryLock(centerX - radius, centerZ - radius, centerX + radius, centerZ + radius);
+    }
+
+    public Node tryLock(final int fromX, final int fromZ, final int toX, final int toZ) {
+        if (fromX > toX || fromZ > toZ) {
+            throw new IllegalArgumentException();
+        }
+
+        final Thread currThread = Thread.currentThread();
+        final int shift = this.coordinateShift;
+        final int fromSectionX = fromX >> shift;
+        final int fromSectionZ = fromZ >> shift;
+        final int toSectionX = toX >> shift;
+        final int toSectionZ = toZ >> shift;
+
+        final long[] areaAffected = new long[(toSectionX - fromSectionX + 1) * (toSectionZ - fromSectionZ + 1)];
+        int areaAffectedLen = 0;
+
+        final Node ret = new Node(this, areaAffected, currThread);
+
+        boolean failed = false;
+
+        // try to fast acquire area
+        for (int currZ = fromSectionZ; currZ <= toSectionZ; ++currZ) {
+            for (int currX = fromSectionX; currX <= toSectionX; ++currX) {
+                final long coordinate = IntPairUtil.key(currX, currZ);
+
+                final Node prev = this.nodes.putIfAbsent(coordinate, ret);
+
+                if (prev == null) {
+                    areaAffected[areaAffectedLen++] = coordinate;
+                    continue;
+                }
+
+                if (prev.thread != currThread) {
+                    failed = true;
+                    break;
+                }
+            }
+        }
+
+        if (!failed) {
+            return ret;
+        }
+
+        // failed, undo logic
+        if (areaAffectedLen != 0) {
+            for (int i = 0; i < areaAffectedLen; ++i) {
+                final long key = areaAffected[i];
+
+                if (this.nodes.remove(key) != ret) {
+                    throw new IllegalStateException();
+                }
+            }
+
+            areaAffectedLen = 0;
+
+            // since we inserted, we need to drain waiters
+            Thread unpark;
+            while ((unpark = ret.pollOrBlockAdds()) != null) {
+                LockSupport.unpark(unpark);
+            }
+        }
+
+        return null;
+    }
+
+    public Node lock(final int x, final int z) {
+        final Thread currThread = Thread.currentThread();
+        final int shift = this.coordinateShift;
+        final int sectionX = x >> shift;
+        final int sectionZ = z >> shift;
+
+        final long coordinate = IntPairUtil.key(sectionX, sectionZ);
+        final long[] areaAffected = new long[1];
+        areaAffected[0] = coordinate;
+
+        final Node ret = new Node(this, areaAffected, currThread);
+
+        for (long failures = 0L;;) {
+            final Node park;
+
+            // try to fast acquire area
+            {
+                final Node prev = this.nodes.putIfAbsent(coordinate, ret);
+
+                if (prev == null) {
+                    ret.areaAffectedLen = 1;
+                    return ret;
+                } else if (prev.thread != currThread) {
+                    park = prev;
+                } else {
+                    // only one node we would want to acquire, and it's owned by this thread already
+                    // areaAffectedLen = 0 already
+                    return ret;
+                }
+            }
+
+            ++failures;
+
+            if (failures > 128L && park.add(currThread)) {
+                LockSupport.park();
+            } else {
+                // high contention, spin wait
+                if (failures < 128L) {
+                    for (long i = 0; i < failures; ++i) {
+                        Thread.onSpinWait();
+                    }
+                    failures = failures << 1;
+                } else if (failures < 1_200L) {
+                    LockSupport.parkNanos(1_000L);
+                    failures = failures + 1L;
+                } else { // scale 0.1ms (100us) per failure
+                    Thread.yield();
+                    LockSupport.parkNanos(100_000L * failures);
+                    failures = failures + 1L;
+                }
+            }
+        }
+    }
+
+    public Node lock(final int centerX, final int centerZ, final int radius) {
+        return this.lock(centerX - radius, centerZ - radius, centerX + radius, centerZ + radius);
+    }
+
+    public Node lock(final int fromX, final int fromZ, final int toX, final int toZ) {
+        if (fromX > toX || fromZ > toZ) {
+            throw new IllegalArgumentException();
+        }
+
+        final Thread currThread = Thread.currentThread();
+        final int shift = this.coordinateShift;
+        final int fromSectionX = fromX >> shift;
+        final int fromSectionZ = fromZ >> shift;
+        final int toSectionX = toX >> shift;
+        final int toSectionZ = toZ >> shift;
+
+        if (((fromSectionX ^ toSectionX) | (fromSectionZ ^ toSectionZ)) == 0) {
+            return this.lock(fromX, fromZ);
+        }
+
+        final long[] areaAffected = new long[(toSectionX - fromSectionX + 1) * (toSectionZ - fromSectionZ + 1)];
+        int areaAffectedLen = 0;
+
+        final Node ret = new Node(this, areaAffected, currThread);
+
+        for (long failures = 0L;;) {
+            Node park = null;
+            boolean addedToArea = false;
+            boolean alreadyOwned = false;
+            boolean allOwned = true;
+
+            // try to fast acquire area
+            for (int currZ = fromSectionZ; currZ <= toSectionZ; ++currZ) {
+                for (int currX = fromSectionX; currX <= toSectionX; ++currX) {
+                    final long coordinate = IntPairUtil.key(currX, currZ);
+
+                    final Node prev = this.nodes.putIfAbsent(coordinate, ret);
+
+                    if (prev == null) {
+                        addedToArea = true;
+                        allOwned = false;
+                        areaAffected[areaAffectedLen++] = coordinate;
+                        continue;
+                    }
+
+                    if (prev.thread != currThread) {
+                        park = prev;
+                        alreadyOwned = true;
+                        break;
+                    }
+                }
+            }
+
+            // check for failure
+            if ((park != null && addedToArea) || (park == null && alreadyOwned && !allOwned)) {
+                // failure to acquire: added and we need to block, or improper lock usage
+                for (int i = 0; i < areaAffectedLen; ++i) {
+                    final long key = areaAffected[i];
+
+                    if (this.nodes.remove(key) != ret) {
+                        throw new IllegalStateException();
+                    }
+                }
+
+                areaAffectedLen = 0;
+
+                // since we inserted, we need to drain waiters
+                Thread unpark;
+                while ((unpark = ret.pollOrBlockAdds()) != null) {
+                    LockSupport.unpark(unpark);
+                }
+            }
+
+            if (park == null) {
+                if (alreadyOwned && !allOwned) {
+                    throw new IllegalStateException("Improper lock usage: Should never acquire intersecting areas");
+                }
+                ret.areaAffectedLen = areaAffectedLen;
+                return ret;
+            }
+
+            // failed
+
+            ++failures;
+
+            if (failures > 128L && park.add(currThread)) {
+                LockSupport.park(park);
+            } else {
+                // high contention, spin wait
+                if (failures < 128L) {
+                    for (long i = 0; i < failures; ++i) {
+                        Thread.onSpinWait();
+                    }
+                    failures = failures << 1;
+                } else if (failures < 1_200L) {
+                    LockSupport.parkNanos(1_000L);
+                    failures = failures + 1L;
+                } else { // scale 0.1ms (100us) per failure
+                    Thread.yield();
+                    LockSupport.parkNanos(100_000L * failures);
+                    failures = failures + 1L;
+                }
+            }
+
+            if (addedToArea) {
+                // try again, so we need to allow adds so that other threads can properly block on us
+                ret.allowAdds();
+            }
+        }
+    }
+
+    public void unlock(final Node node) {
+        if (node.lock != this) {
+            throw new IllegalStateException("Unlock target lock mismatch");
+        }
+
+        final long[] areaAffected = node.areaAffected;
+        final int areaAffectedLen = node.areaAffectedLen;
+
+        if (areaAffectedLen == 0) {
+            // here we are not in the node map, and so do not need to remove from the node map or unblock any waiters
+            return;
+        }
+
+        Objects.checkFromToIndex(0, areaAffectedLen, areaAffected.length);
+
+        // remove from node map; allowing other threads to lock
+        for (int i = 0; i < areaAffectedLen; ++i) {
+            final long coordinate = areaAffected[i];
+            if (this.nodes.remove(coordinate, node) != node) {
+                throw new IllegalStateException();
+            }
+        }
+
+        Thread unpark;
+        while ((unpark = node.pollOrBlockAdds()) != null) {
+            LockSupport.unpark(unpark);
+        }
+    }
+
+    public static final class Node extends MultiThreadedQueue<Thread> {
+
+        private final ReentrantAreaLock lock;
+        private final long[] areaAffected;
+        private int areaAffectedLen;
+        private final Thread thread;
+
+        private Node(final ReentrantAreaLock lock, final long[] areaAffected, final Thread thread) {
+            this.lock = lock;
+            this.areaAffected = areaAffected;
+            this.thread = thread;
+        }
+
+        @Override
+        public String toString() {
+            return "Node{" +
+                "areaAffected=" + IntPairUtil.toString(this.areaAffected, 0, this.areaAffectedLen) +
+                ", thread=" + this.thread +
+                '}';
+        }
+    }
+}
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/map/ConcurrentLong2ReferenceChainedHashTable.java b/src/main/java/ca/spottedleaf/concurrentutil/map/ConcurrentLong2ReferenceChainedHashTable.java
new file mode 100644
index 0000000000000000000000000000000000000000..d701998b376579ec652fb94823befa3cc0bc4090
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/concurrentutil/map/ConcurrentLong2ReferenceChainedHashTable.java
@@ -0,0 +1,1684 @@
+package ca.spottedleaf.concurrentutil.map;
+
+import ca.spottedleaf.concurrentutil.function.BiLong1Function;
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
+import ca.spottedleaf.concurrentutil.util.HashUtil;
+import ca.spottedleaf.concurrentutil.util.IntegerUtil;
+import ca.spottedleaf.concurrentutil.util.ThrowUtil;
+import ca.spottedleaf.concurrentutil.util.Validate;
+import java.lang.invoke.VarHandle;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.PrimitiveIterator;
+import java.util.concurrent.atomic.LongAdder;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.LongConsumer;
+import java.util.function.LongFunction;
+import java.util.function.Predicate;
+
+/**
+ * Concurrent hashtable implementation supporting mapping arbitrary {@code long} values onto non-null {@code Object}
+ * values with support for multiple writer and multiple reader threads.
+ *
+ * <p><h3>Happens-before relationship</h3></p>
+ * <p>
+ * As with {@link java.util.concurrent.ConcurrentMap}, there is a happens-before relationship between actions in one thread
+ * prior to writing to the map and access to the results of those actions in another thread.
+ * </p>
+ *
+ * <p><h3>Atomicity of functional methods</h3></p>
+ * <p>
+ * Functional methods are functions declared in this class which possibly perform a write (remove, replace, or modify)
+ * to an entry in this map as a result of invoking a function on an input parameter. For example, {@link #compute(long, BiLong1Function)},
+ * {@link #merge(long, Object, BiFunction)} and {@link #removeIf(long, Predicate)} are examples of functional methods.
+ * Functional methods will be performed atomically, that is, the input parameter is guaranteed to only be invoked at most
+ * once per function call. The consequence of this behavior however is that a critical lock for a bin entry is held, which
+ * means that if the input parameter invocation makes additional calls to write into this hash table that the result
+ * is undefined and deadlock-prone.
+ * </p>
+ *
+ * @param <V>
+ * @see java.util.concurrent.ConcurrentMap
+ */
+public class ConcurrentLong2ReferenceChainedHashTable<V> {
+
+    protected static final int DEFAULT_CAPACITY = 16;
+    protected static final float DEFAULT_LOAD_FACTOR = 0.75f;
+    protected static final int MAXIMUM_CAPACITY = Integer.MIN_VALUE >>> 1;
+
+    protected final LongAdder size = new LongAdder();
+    protected final float loadFactor;
+
+    protected volatile TableEntry<V>[] table;
+
+    protected static final int THRESHOLD_NO_RESIZE = -1;
+    protected static final int THRESHOLD_RESIZING  = -2;
+    protected volatile int threshold;
+    protected static final VarHandle THRESHOLD_HANDLE = ConcurrentUtil.getVarHandle(ConcurrentLong2ReferenceChainedHashTable.class, "threshold", int.class);
+
+    protected final int getThresholdAcquire() {
+        return (int)THRESHOLD_HANDLE.getAcquire(this);
+    }
+
+    protected final int getThresholdVolatile() {
+        return (int)THRESHOLD_HANDLE.getVolatile(this);
+    }
+
+    protected final void setThresholdPlain(final int threshold) {
+        THRESHOLD_HANDLE.set(this, threshold);
+    }
+
+    protected final void setThresholdRelease(final int threshold) {
+        THRESHOLD_HANDLE.setRelease(this, threshold);
+    }
+
+    protected final void setThresholdVolatile(final int threshold) {
+        THRESHOLD_HANDLE.setVolatile(this, threshold);
+    }
+
+    protected final int compareExchangeThresholdVolatile(final int expect, final int update) {
+        return (int)THRESHOLD_HANDLE.compareAndExchange(this, expect, update);
+    }
+
+    public ConcurrentLong2ReferenceChainedHashTable() {
+        this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR);
+    }
+
+    protected static int getTargetThreshold(final int capacity, final float loadFactor) {
+        final double ret = (double)capacity * (double)loadFactor;
+        if (Double.isInfinite(ret) || ret >= ((double)Integer.MAX_VALUE)) {
+            return THRESHOLD_NO_RESIZE;
+        }
+
+        return (int)Math.ceil(ret);
+    }
+
+    protected static int getCapacityFor(final int capacity) {
+        if (capacity <= 0) {
+            throw new IllegalArgumentException("Invalid capacity: " + capacity);
+        }
+        if (capacity >= MAXIMUM_CAPACITY) {
+            return MAXIMUM_CAPACITY;
+        }
+        return IntegerUtil.roundCeilLog2(capacity);
+    }
+
+    protected ConcurrentLong2ReferenceChainedHashTable(final int capacity, final float loadFactor) {
+        final int tableSize = getCapacityFor(capacity);
+
+        if (loadFactor <= 0.0 || !Float.isFinite(loadFactor)) {
+            throw new IllegalArgumentException("Invalid load factor: " + loadFactor);
+        }
+
+        if (tableSize == MAXIMUM_CAPACITY) {
+            this.setThresholdPlain(THRESHOLD_NO_RESIZE);
+        } else {
+            this.setThresholdPlain(getTargetThreshold(tableSize, loadFactor));
+        }
+
+        this.loadFactor = loadFactor;
+        // noinspection unchecked
+        this.table = (TableEntry<V>[])new TableEntry[tableSize];
+    }
+
+    public static <V> ConcurrentLong2ReferenceChainedHashTable<V> createWithCapacity(final int capacity) {
+        return createWithCapacity(capacity, DEFAULT_LOAD_FACTOR);
+    }
+
+    public static <V> ConcurrentLong2ReferenceChainedHashTable<V> createWithCapacity(final int capacity, final float loadFactor) {
+        return new ConcurrentLong2ReferenceChainedHashTable<>(capacity, loadFactor);
+    }
+
+    public static <V> ConcurrentLong2ReferenceChainedHashTable<V> createWithExpected(final int expected) {
+        return createWithExpected(expected, DEFAULT_LOAD_FACTOR);
+    }
+
+    public static <V> ConcurrentLong2ReferenceChainedHashTable<V> createWithExpected(final int expected, final float loadFactor) {
+        final int capacity = (int)Math.ceil((double)expected / (double)loadFactor);
+
+        return createWithCapacity(capacity, loadFactor);
+    }
+
+    /** must be deterministic given a key */
+    protected static int getHash(final long key) {
+        return (int)HashUtil.mix(key);
+    }
+
+    /**
+     * Returns the load factor associated with this map.
+     */
+    public final float getLoadFactor() {
+        return this.loadFactor;
+    }
+
+    protected static <V> TableEntry<V> getAtIndexVolatile(final TableEntry<V>[] table, final int index) {
+        //noinspection unchecked
+        return (TableEntry<V>)TableEntry.TABLE_ENTRY_ARRAY_HANDLE.getVolatile(table, index);
+    }
+
+    protected static <V> void setAtIndexRelease(final TableEntry<V>[] table, final int index, final TableEntry<V> value) {
+        TableEntry.TABLE_ENTRY_ARRAY_HANDLE.setRelease(table, index, value);
+    }
+
+    protected static <V> void setAtIndexVolatile(final TableEntry<V>[] table, final int index, final TableEntry<V> value) {
+        TableEntry.TABLE_ENTRY_ARRAY_HANDLE.setVolatile(table, index, value);
+    }
+
+    protected static <V> TableEntry<V> compareAndExchangeAtIndexVolatile(final TableEntry<V>[] table, final int index,
+                                                                         final TableEntry<V> expect, final TableEntry<V> update) {
+        //noinspection unchecked
+        return (TableEntry<V>)TableEntry.TABLE_ENTRY_ARRAY_HANDLE.compareAndExchange(table, index, expect, update);
+    }
+
+    /**
+     * Returns the possible node associated with the key, or {@code null} if there is no such node. The node
+     * returned may have a {@code null} {@link TableEntry#value}, in which case the node is a placeholder for
+     * a compute/computeIfAbsent call. The placeholder node should not be considered mapped in order to preserve
+     * happens-before relationships between writes and reads in the map.
+     */
+    protected final TableEntry<V> getNode(final long key) {
+        final int hash = getHash(key);
+
+        TableEntry<V>[] table = this.table;
+        for (;;) {
+            TableEntry<V> node = getAtIndexVolatile(table, hash & (table.length - 1));
+
+            if (node == null) {
+                // node == null
+                return node;
+            }
+
+            if (node.resize) {
+                table = (TableEntry<V>[])node.getValuePlain();
+                continue;
+            }
+
+            for (; node != null; node = node.getNextVolatile()) {
+                if (node.key == key) {
+                    return node;
+                }
+            }
+
+            // node == null
+            return node;
+        }
+    }
+
+    /**
+     * Returns the currently mapped value associated with the specified key, or {@code null} if there is none.
+     *
+     * @param key Specified key
+     */
+    public V get(final long key) {
+        final TableEntry<V> node = this.getNode(key);
+        return node == null ? null : node.getValueVolatile();
+    }
+
+    /**
+     * Returns the currently mapped value associated with the specified key, or the specified default value if there is none.
+     *
+     * @param key Specified key
+     * @param defaultValue Specified default value
+     */
+    public V getOrDefault(final long key, final V defaultValue) {
+        final TableEntry<V> node = this.getNode(key);
+        if (node == null) {
+            return defaultValue;
+        }
+
+        final V ret = node.getValueVolatile();
+        if (ret == null) {
+            // ret == null for nodes pre-allocated to compute() and friends
+            return defaultValue;
+        }
+
+        return ret;
+    }
+
+    /**
+     * Returns whether the specified key is mapped to some value.
+     * @param key Specified key
+     */
+    public boolean containsKey(final long key) {
+        // cannot use getNode, as the node may be a placeholder for compute()
+        return this.get(key) != null;
+    }
+
+    /**
+     * Returns whether the specified value has a key mapped to it.
+     * @param value Specified value
+     * @throws NullPointerException If value is null
+     */
+    public boolean containsValue(final V value) {
+        Validate.notNull(value, "Value cannot be null");
+
+        final NodeIterator<V> iterator = new NodeIterator<>(this.table);
+
+        TableEntry<V> node;
+        while ((node = iterator.findNext()) != null) {
+            // need to use acquire here to ensure the happens-before relationship
+            if (node.getValueAcquire() == value) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns the number of mappings in this map.
+     */
+    public int size() {
+        final long ret = this.size.sum();
+
+        if (ret <= 0L) {
+            return 0;
+        }
+        if (ret >= (long)Integer.MAX_VALUE) {
+            return Integer.MAX_VALUE;
+        }
+
+        return (int)ret;
+    }
+
+    /**
+     * Returns whether this map has no mappings.
+     */
+    public boolean isEmpty() {
+        return this.size.sum() <= 0L;
+    }
+
+    /**
+     * Adds count to size and checks threshold for resizing
+     */
+    protected final void addSize(final long count) {
+        this.size.add(count);
+
+        final int threshold = this.getThresholdAcquire();
+
+        if (threshold < 0L) {
+            // resizing or no resizing allowed, in either cases we do not need to do anything
+            return;
+        }
+
+        final long sum = this.size.sum();
+
+        if (sum < (long)threshold) {
+            return;
+        }
+
+        if (threshold != this.compareExchangeThresholdVolatile(threshold, THRESHOLD_RESIZING)) {
+            // some other thread resized
+            return;
+        }
+
+        // create new table
+        this.resize(sum);
+    }
+
+    /**
+     * Resizes table, only invoke for the thread which has successfully updated threshold to {@link #THRESHOLD_RESIZING}
+     * @param sum Estimate of current mapping count, must be >= old threshold
+     */
+    private void resize(final long sum) {
+        int capacity;
+
+        // add 1.0, as sum may equal threshold (in which case, sum / loadFactor = current capacity)
+        // adding 1.0 should at least raise the size by a factor of two due to usage of roundCeilLog2
+        final double targetD = ((double)sum / (double)this.loadFactor) + 1.0;
+        if (targetD >= (double)MAXIMUM_CAPACITY) {
+            capacity = MAXIMUM_CAPACITY;
+        } else {
+            capacity = (int)Math.ceil(targetD);
+            capacity = IntegerUtil.roundCeilLog2(capacity);
+            if (capacity > MAXIMUM_CAPACITY) {
+                capacity = MAXIMUM_CAPACITY;
+            }
+        }
+
+        // create new table data
+
+        final TableEntry<V>[] newTable = new TableEntry[capacity];
+        // noinspection unchecked
+        final TableEntry<V> resizeNode = new TableEntry<>(0L, (V)newTable, true);
+
+        // transfer nodes from old table
+
+        // does not need to be volatile read, just plain
+        final TableEntry<V>[] oldTable = this.table;
+
+        // when resizing, the old entries at bin i (where i = hash % oldTable.length) are assigned to
+        // bin k in the new table (where k = hash % newTable.length)
+        // since both table lengths are powers of two (specifically, newTable is a multiple of oldTable),
+        // the possible number of locations in the new table to assign any given i is newTable.length/oldTable.length
+
+        // we can build the new linked nodes for the new table by using a work array sized to newTable.length/oldTable.length
+        // which holds the _last_ entry in the chain per bin
+
+        final int capOldShift = IntegerUtil.floorLog2(oldTable.length);
+        final int capDiffShift = IntegerUtil.floorLog2(capacity) - capOldShift;
+
+        if (capDiffShift == 0) {
+            throw new IllegalStateException("Resizing to same size");
+        }
+
+        final TableEntry<V>[] work = new TableEntry[1 << capDiffShift]; // typically, capDiffShift = 1
+
+        for (int i = 0, len = oldTable.length; i < len; ++i) {
+            TableEntry<V> binNode = getAtIndexVolatile(oldTable, i);
+
+            for (;;) {
+                if (binNode == null) {
+                    // just need to replace the bin node, do not need to move anything
+                    if (null == (binNode = compareAndExchangeAtIndexVolatile(oldTable, i, null, resizeNode))) {
+                        break;
+                    } // else: binNode != null, fall through
+                }
+
+                // need write lock to block other writers
+                synchronized (binNode) {
+                    if (binNode != (binNode = getAtIndexVolatile(oldTable, i))) {
+                        continue;
+                    }
+
+                    // an important detail of resizing is that we do not need to be concerned with synchronisation on
+                    // writes to the new table, as no access to any nodes on bin i on oldTable will occur until a thread
+                    // sees the resizeNode
+                    // specifically, as long as the resizeNode is release written there are no cases where another thread
+                    // will see our writes to the new table
+
+                    TableEntry<V> next = binNode.getNextPlain();
+
+                    if (next == null) {
+                        // simple case: do not use work array
+
+                        // do not need to create new node, readers only need to see the state of the map at the
+                        // beginning of a call, so any additions onto _next_ don't really matter
+                        // additionally, the old node is replaced so that writers automatically forward to the new table,
+                        // which resolves any issues
+                        newTable[getHash(binNode.key) & (capacity - 1)] = binNode;
+                    } else {
+                        // reset for next usage
+                        Arrays.fill(work, null);
+
+                        for (TableEntry<V> curr = binNode; curr != null; curr = curr.getNextPlain()) {
+                            final int newTableIdx = getHash(curr.key) & (capacity - 1);
+                            final int workIdx = newTableIdx >>> capOldShift;
+
+                            final TableEntry<V> replace = new TableEntry<>(curr.key, curr.getValuePlain());
+
+                            final TableEntry<V> workNode = work[workIdx];
+                            work[workIdx] = replace;
+
+                            if (workNode == null) {
+                                newTable[newTableIdx] = replace;
+                                continue;
+                            } else {
+                                workNode.setNextPlain(replace);
+                                continue;
+                            }
+                        }
+                    }
+
+                    setAtIndexRelease(oldTable, i, resizeNode);
+                    break;
+                }
+            }
+        }
+
+        // calculate new threshold
+        final int newThreshold;
+        if (capacity == MAXIMUM_CAPACITY) {
+            newThreshold = THRESHOLD_NO_RESIZE;
+        } else {
+            newThreshold = getTargetThreshold(capacity, loadFactor);
+        }
+
+        this.table = newTable;
+        // finish resize operation by releasing hold on threshold
+        this.setThresholdVolatile(newThreshold);
+    }
+
+    /**
+     * Subtracts count from size
+     */
+    protected final void subSize(final long count) {
+        this.size.add(-count);
+    }
+
+    /**
+     * Atomically updates the value associated with {@code key} to {@code value}, or inserts a new mapping with {@code key}
+     * mapped to {@code value}.
+     * @param key Specified key
+     * @param value Specified value
+     * @throws NullPointerException If value is null
+     * @return Old value previously associated with key, or {@code null} if none.
+     */
+    public V put(final long key, final V value) {
+        Validate.notNull(value, "Value may not be null");
+
+        final int hash = getHash(key);
+
+        TableEntry<V>[] table = this.table;
+        table_loop:
+        for (;;) {
+            final int index = hash & (table.length - 1);
+
+            TableEntry<V> node = getAtIndexVolatile(table, index);
+            node_loop:
+            for (;;) {
+                if (node == null) {
+                    if (null == (node = compareAndExchangeAtIndexVolatile(table, index, null, new TableEntry<>(key, value)))) {
+                        // successfully inserted
+                        this.addSize(1L);
+                        return null;
+                    } // else: node != null, fall through
+                }
+
+                if (node.resize) {
+                    table = (TableEntry<V>[])node.getValuePlain();
+                    continue table_loop;
+                }
+
+                synchronized (node) {
+                    if (node != (node = getAtIndexVolatile(table, index))) {
+                        continue node_loop;
+                    }
+                    // plain reads are fine during synchronised access, as we are the only writer
+                    TableEntry<V> prev = null;
+                    for (; node != null; prev = node, node = node.getNextPlain()) {
+                        if (node.key == key) {
+                            final V ret = node.getValuePlain();
+                            node.setValueVolatile(value);
+                            return ret;
+                        }
+                    }
+
+                    // volatile ordering ensured by addSize(), but we need release here
+                    // to ensure proper ordering with reads and other writes
+                    prev.setNextRelease(new TableEntry<>(key, value));
+                }
+
+                this.addSize(1L);
+                return null;
+            }
+        }
+    }
+
+    /**
+     * Atomically inserts a new mapping with {@code key} mapped to {@code value} if and only if {@code key} is not
+     * currently mapped to some value.
+     * @param key Specified key
+     * @param value Specified value
+     * @throws NullPointerException If value is null
+     * @return Value currently associated with key, or {@code null} if none and {@code value} was associated.
+     */
+    public V putIfAbsent(final long key, final V value) {
+        Validate.notNull(value, "Value may not be null");
+
+        final int hash = getHash(key);
+
+        TableEntry<V>[] table = this.table;
+        table_loop:
+        for (;;) {
+            final int index = hash & (table.length - 1);
+
+            TableEntry<V> node = getAtIndexVolatile(table, index);
+            node_loop:
+            for (;;) {
+                if (node == null) {
+                    if (null == (node = compareAndExchangeAtIndexVolatile(table, index, null, new TableEntry<>(key, value)))) {
+                        // successfully inserted
+                        this.addSize(1L);
+                        return null;
+                    } // else: node != null, fall through
+                }
+
+                if (node.resize) {
+                    table = (TableEntry<V>[])node.getValuePlain();
+                    continue table_loop;
+                }
+
+                // optimise ifAbsent calls: check if first node is key before attempting lock acquire
+                if (node.key == key) {
+                    final V ret = node.getValueVolatile();
+                    if (ret != null) {
+                        return ret;
+                    } // else: fall back to lock to read the node
+                }
+
+                synchronized (node) {
+                    if (node != (node = getAtIndexVolatile(table, index))) {
+                        continue node_loop;
+                    }
+                    // plain reads are fine during synchronised access, as we are the only writer
+                    TableEntry<V> prev = null;
+                    for (; node != null; prev = node, node = node.getNextPlain()) {
+                        if (node.key == key) {
+                            return node.getValuePlain();
+                        }
+                    }
+
+                    // volatile ordering ensured by addSize(), but we need release here
+                    // to ensure proper ordering with reads and other writes
+                    prev.setNextRelease(new TableEntry<>(key, value));
+                }
+
+                this.addSize(1L);
+                return null;
+            }
+        }
+    }
+
+    /**
+     * Atomically updates the value associated with {@code key} to {@code value}, or does nothing if {@code key} is not
+     * associated with a value.
+     * @param key Specified key
+     * @param value Specified value
+     * @throws NullPointerException If value is null
+     * @return Old value previously associated with key, or {@code null} if none.
+     */
+    public V replace(final long key, final V value) {
+        Validate.notNull(value, "Value may not be null");
+
+        final int hash = getHash(key);
+
+        TableEntry<V>[] table = this.table;
+        table_loop:
+        for (;;) {
+            final int index = hash & (table.length - 1);
+
+            TableEntry<V> node = getAtIndexVolatile(table, index);
+            node_loop:
+            for (;;) {
+                if (node == null) {
+                    return null;
+                }
+
+                if (node.resize) {
+                    table = (TableEntry<V>[])node.getValuePlain();
+                    continue table_loop;
+                }
+
+                synchronized (node) {
+                    if (node != (node = getAtIndexVolatile(table, index))) {
+                        continue node_loop;
+                    }
+
+                    // plain reads are fine during synchronised access, as we are the only writer
+                    for (; node != null; node = node.getNextPlain()) {
+                        if (node.key == key) {
+                            final V ret = node.getValuePlain();
+                            node.setValueVolatile(value);
+                            return ret;
+                        }
+                    }
+                }
+
+                return null;
+            }
+        }
+    }
+
+    /**
+     * Atomically updates the value associated with {@code key} to {@code update} if the currently associated
+     * value is reference equal to {@code expect}, otherwise does nothing.
+     * @param key Specified key
+     * @param expect Expected value to check current mapped value with
+     * @param update Update value to replace mapped value with
+     * @throws NullPointerException If value is null
+     * @return If the currently mapped value is not reference equal to {@code expect}, then returns the currently mapped
+     *         value. If the key is not mapped to any value, then returns {@code null}. If neither of the two cases are
+     *         true, then returns {@code expect}.
+     */
+    public V replace(final long key, final V expect, final V update) {
+        Validate.notNull(expect, "Expect may not be null");
+        Validate.notNull(update, "Update may not be null");
+
+        final int hash = getHash(key);
+
+        TableEntry<V>[] table = this.table;
+        table_loop:
+        for (;;) {
+            final int index = hash & (table.length - 1);
+
+            TableEntry<V> node = getAtIndexVolatile(table, index);
+            node_loop:
+            for (;;) {
+                if (node == null) {
+                    return null;
+                }
+
+                if (node.resize) {
+                    table = (TableEntry<V>[])node.getValuePlain();
+                    continue table_loop;
+                }
+
+                synchronized (node) {
+                    if (node != (node = getAtIndexVolatile(table, index))) {
+                        continue node_loop;
+                    }
+
+                    // plain reads are fine during synchronised access, as we are the only writer
+                    for (; node != null; node = node.getNextPlain()) {
+                        if (node.key == key) {
+                            final V ret = node.getValuePlain();
+
+                            if (ret != expect) {
+                                return ret;
+                            }
+
+                            node.setValueVolatile(update);
+                            return ret;
+                        }
+                    }
+                }
+
+                return null;
+            }
+        }
+    }
+
+    /**
+     * Atomically removes the mapping for the specified key and returns the value it was associated with. If the key
+     * is not mapped to a value, then does nothing and returns {@code null}.
+     * @param key Specified key
+     * @return Old value previously associated with key, or {@code null} if none.
+     */
+    public V remove(final long key) {
+        final int hash = getHash(key);
+
+        TableEntry<V>[] table = this.table;
+        table_loop:
+        for (;;) {
+            final int index = hash & (table.length - 1);
+
+            TableEntry<V> node = getAtIndexVolatile(table, index);
+            node_loop:
+            for (;;) {
+                if (node == null) {
+                    return null;
+                }
+
+                if (node.resize) {
+                    table = (TableEntry<V>[])node.getValuePlain();
+                    continue table_loop;
+                }
+
+                boolean removed = false;
+                V ret = null;
+
+                synchronized (node) {
+                    if (node != (node = getAtIndexVolatile(table, index))) {
+                        continue node_loop;
+                    }
+
+                    TableEntry<V> prev = null;
+
+                    // plain reads are fine during synchronised access, as we are the only writer
+                    for (; node != null; prev = node, node = node.getNextPlain()) {
+                        if (node.key == key) {
+                            ret = node.getValuePlain();
+                            removed = true;
+
+                            // volatile ordering ensured by addSize(), but we need release here
+                            // to ensure proper ordering with reads and other writes
+                            if (prev == null) {
+                                setAtIndexRelease(table, index, node.getNextPlain());
+                            } else {
+                                prev.setNextRelease(node.getNextPlain());
+                            }
+
+                            break;
+                        }
+                    }
+                }
+
+                if (removed) {
+                    this.subSize(1L);
+                }
+
+                return ret;
+            }
+        }
+    }
+
+    /**
+     * Atomically removes the mapping for the specified key if it is mapped to {@code expect} and returns {@code expect}. If the key
+     * is not mapped to a value, then does nothing and returns {@code null}. If the key is mapped to a value that is not reference
+     * equal to {@code expect}, then returns that value.
+     * @param key Specified key
+     * @param expect Specified expected value
+     * @return The specified expected value if the key was mapped to {@code expect}. If
+     *         the key is not mapped to any value, then returns {@code null}. If neither of those cases are true,
+     *         then returns the current (non-null) mapped value for key.
+     */
+    public V remove(final long key, final V expect) {
+        final int hash = getHash(key);
+
+        TableEntry<V>[] table = this.table;
+        table_loop:
+        for (;;) {
+            final int index = hash & (table.length - 1);
+
+            TableEntry<V> node = getAtIndexVolatile(table, index);
+            node_loop:
+            for (;;) {
+                if (node == null) {
+                    return null;
+                }
+
+                if (node.resize) {
+                    table = (TableEntry<V>[])node.getValuePlain();
+                    continue table_loop;
+                }
+
+                boolean removed = false;
+                V ret = null;
+
+                synchronized (node) {
+                    if (node != (node = getAtIndexVolatile(table, index))) {
+                        continue node_loop;
+                    }
+
+                    TableEntry<V> prev = null;
+
+                    // plain reads are fine during synchronised access, as we are the only writer
+                    for (; node != null; prev = node, node = node.getNextPlain()) {
+                        if (node.key == key) {
+                            ret = node.getValuePlain();
+                            if (ret == expect) {
+                                removed = true;
+
+                                // volatile ordering ensured by addSize(), but we need release here
+                                // to ensure proper ordering with reads and other writes
+                                if (prev == null) {
+                                    setAtIndexRelease(table, index, node.getNextPlain());
+                                } else {
+                                    prev.setNextRelease(node.getNextPlain());
+                                }
+                            }
+                            break;
+                        }
+                    }
+                }
+
+                if (removed) {
+                    this.subSize(1L);
+                }
+
+                return ret;
+            }
+        }
+    }
+
+    /**
+     * Atomically removes the mapping for the specified key the predicate returns true for its currently mapped value. If the key
+     * is not mapped to a value, then does nothing and returns {@code null}.
+     *
+     * <p>
+     * This function is a "functional methods" as defined by {@link ConcurrentLong2ReferenceChainedHashTable}.
+     * </p>
+     *
+     * @param key Specified key
+     * @param predicate Specified predicate
+     * @throws NullPointerException If predicate is null
+     * @return The specified expected value if the key was mapped to {@code expect}. If
+     *         the key is not mapped to any value, then returns {@code null}. If neither of those cases are true,
+     *         then returns the current (non-null) mapped value for key.
+     */
+    public V removeIf(final long key, final Predicate<? super V> predicate) {
+        Validate.notNull(predicate, "Predicate may not be null");
+
+        final int hash = getHash(key);
+
+        TableEntry<V>[] table = this.table;
+        table_loop:
+        for (;;) {
+            final int index = hash & (table.length - 1);
+
+            TableEntry<V> node = getAtIndexVolatile(table, index);
+            node_loop:
+            for (;;) {
+                if (node == null) {
+                    return null;
+                }
+
+                if (node.resize) {
+                    table = (TableEntry<V>[])node.getValuePlain();
+                    continue table_loop;
+                }
+
+                boolean removed = false;
+                V ret = null;
+
+                synchronized (node) {
+                    if (node != (node = getAtIndexVolatile(table, index))) {
+                        continue node_loop;
+                    }
+
+                    TableEntry<V> prev = null;
+
+                    // plain reads are fine during synchronised access, as we are the only writer
+                    for (; node != null; prev = node, node = node.getNextPlain()) {
+                        if (node.key == key) {
+                            ret = node.getValuePlain();
+                            if (predicate.test(ret)) {
+                                removed = true;
+
+                                // volatile ordering ensured by addSize(), but we need release here
+                                // to ensure proper ordering with reads and other writes
+                                if (prev == null) {
+                                    setAtIndexRelease(table, index, node.getNextPlain());
+                                } else {
+                                    prev.setNextRelease(node.getNextPlain());
+                                }
+                            }
+                            break;
+                        }
+                    }
+                }
+
+                if (removed) {
+                    this.subSize(1L);
+                }
+
+                return ret;
+            }
+        }
+    }
+
+    /**
+     * See {@link java.util.concurrent.ConcurrentMap#compute(Object, BiFunction)}
+     * <p>
+     * This function is a "functional methods" as defined by {@link ConcurrentLong2ReferenceChainedHashTable}.
+     * </p>
+     */
+    public V compute(final long key, final BiLong1Function<? super V, ? extends V> function) {
+        final int hash = getHash(key);
+
+        TableEntry<V>[] table = this.table;
+        table_loop:
+        for (;;) {
+            final int index = hash & (table.length - 1);
+
+            TableEntry<V> node = getAtIndexVolatile(table, index);
+            node_loop:
+            for (;;) {
+                V ret = null;
+                if (node == null) {
+                    final TableEntry<V> insert = new TableEntry<>(key, null);
+
+                    boolean added = false;
+
+                    synchronized (insert) {
+                        if (null == (node = compareAndExchangeAtIndexVolatile(table, index, null, insert))) {
+                            try {
+                                ret = function.apply(key, null);
+                            } catch (final Throwable throwable) {
+                                setAtIndexVolatile(table, index, null);
+                                ThrowUtil.throwUnchecked(throwable);
+                                // unreachable
+                                return null;
+                            }
+
+                            if (ret == null) {
+                                setAtIndexVolatile(table, index, null);
+                                return ret;
+                            } else {
+                                // volatile ordering ensured by addSize(), but we need release here
+                                // to ensure proper ordering with reads and other writes
+                                insert.setValueRelease(ret);
+                                added = true;
+                            }
+                        } // else: node != null, fall through
+                    }
+
+                    if (added) {
+                        this.addSize(1L);
+                        return ret;
+                    }
+                }
+
+                if (node.resize) {
+                    table = (TableEntry<V>[])node.getValuePlain();
+                    continue table_loop;
+                }
+
+                boolean removed = false;
+                boolean added = false;
+
+                synchronized (node) {
+                    if (node != (node = getAtIndexVolatile(table, index))) {
+                        continue node_loop;
+                    }
+                    // plain reads are fine during synchronised access, as we are the only writer
+                    TableEntry<V> prev = null;
+                    for (; node != null; prev = node, node = node.getNextPlain()) {
+                        if (node.key == key) {
+                            final V old = node.getValuePlain();
+
+                            final V computed = function.apply(key, old);
+
+                            if (computed != null) {
+                                node.setValueVolatile(computed);
+                                return computed;
+                            }
+
+                            // volatile ordering ensured by addSize(), but we need release here
+                            // to ensure proper ordering with reads and other writes
+                            if (prev == null) {
+                                setAtIndexRelease(table, index, node.getNextPlain());
+                            } else {
+                                prev.setNextRelease(node.getNextPlain());
+                            }
+
+                            removed = true;
+                            break;
+                        }
+                    }
+
+                    if (!removed) {
+                        final V computed = function.apply(key, null);
+                        if (computed != null) {
+                            // volatile ordering ensured by addSize(), but we need release here
+                            // to ensure proper ordering with reads and other writes
+                            prev.setNextRelease(new TableEntry<>(key, computed));
+                            ret = computed;
+                            added = true;
+                        }
+                    }
+                }
+
+                if (removed) {
+                    this.subSize(1L);
+                }
+                if (added) {
+                    this.addSize(1L);
+                }
+
+                return ret;
+            }
+        }
+    }
+
+    /**
+     * See {@link java.util.concurrent.ConcurrentMap#computeIfAbsent(Object, Function)}
+     * <p>
+     * This function is a "functional methods" as defined by {@link ConcurrentLong2ReferenceChainedHashTable}.
+     * </p>
+     */
+    public V computeIfAbsent(final long key, final LongFunction<? extends V> function) {
+        final int hash = getHash(key);
+
+        TableEntry<V>[] table = this.table;
+        table_loop:
+        for (;;) {
+            final int index = hash & (table.length - 1);
+
+            TableEntry<V> node = getAtIndexVolatile(table, index);
+            node_loop:
+            for (;;) {
+                V ret = null;
+                if (node == null) {
+                    final TableEntry<V> insert = new TableEntry<>(key, null);
+
+                    boolean added = false;
+
+                    synchronized (insert) {
+                        if (null == (node = compareAndExchangeAtIndexVolatile(table, index, null, insert))) {
+                            try {
+                                ret = function.apply(key);
+                            } catch (final Throwable throwable) {
+                                setAtIndexVolatile(table, index, null);
+                                ThrowUtil.throwUnchecked(throwable);
+                                // unreachable
+                                return null;
+                            }
+
+                            if (ret == null) {
+                                setAtIndexVolatile(table, index, null);
+                                return null;
+                            } else {
+                                // volatile ordering ensured by addSize(), but we need release here
+                                // to ensure proper ordering with reads and other writes
+                                insert.setValueRelease(ret);
+                                added = true;
+                            }
+                        } // else: node != null, fall through
+                    }
+
+                    if (added) {
+                        this.addSize(1L);
+                        return ret;
+                    }
+                }
+
+                if (node.resize) {
+                    table = (TableEntry<V>[])node.getValuePlain();
+                    continue table_loop;
+                }
+
+                // optimise ifAbsent calls: check if first node is key before attempting lock acquire
+                if (node.key == key) {
+                    ret = node.getValueVolatile();
+                    if (ret != null) {
+                        return ret;
+                    } // else: fall back to lock to read the node
+                }
+
+                boolean added = false;
+
+                synchronized (node) {
+                    if (node != (node = getAtIndexVolatile(table, index))) {
+                        continue node_loop;
+                    }
+                    // plain reads are fine during synchronised access, as we are the only writer
+                    TableEntry<V> prev = null;
+                    for (; node != null; prev = node, node = node.getNextPlain()) {
+                        if (node.key == key) {
+                            ret = node.getValuePlain();
+                            return ret;
+                        }
+                    }
+
+                    final V computed = function.apply(key);
+                    if (computed != null) {
+                        // volatile ordering ensured by addSize(), but we need release here
+                        // to ensure proper ordering with reads and other writes
+                        prev.setNextRelease(new TableEntry<>(key, computed));
+                        ret = computed;
+                        added = true;
+                    }
+                }
+
+                if (added) {
+                    this.addSize(1L);
+                }
+
+                return ret;
+            }
+        }
+    }
+
+    /**
+     * See {@link java.util.concurrent.ConcurrentMap#computeIfPresent(Object, BiFunction)}
+     * <p>
+     * This function is a "functional methods" as defined by {@link ConcurrentLong2ReferenceChainedHashTable}.
+     * </p>
+     */
+    public V computeIfPresent(final long key, final BiLong1Function<? super V, ? extends V> function) {
+        final int hash = getHash(key);
+
+        TableEntry<V>[] table = this.table;
+        table_loop:
+        for (;;) {
+            final int index = hash & (table.length - 1);
+
+            TableEntry<V> node = getAtIndexVolatile(table, index);
+            node_loop:
+            for (;;) {
+                if (node == null) {
+                    return null;
+                }
+
+                if (node.resize) {
+                    table = (TableEntry<V>[])node.getValuePlain();
+                    continue table_loop;
+                }
+
+                boolean removed = false;
+
+                synchronized (node) {
+                    if (node != (node = getAtIndexVolatile(table, index))) {
+                        continue node_loop;
+                    }
+                    // plain reads are fine during synchronised access, as we are the only writer
+                    TableEntry<V> prev = null;
+                    for (; node != null; prev = node, node = node.getNextPlain()) {
+                        if (node.key == key) {
+                            final V old = node.getValuePlain();
+
+                            final V computed = function.apply(key, old);
+
+                            if (computed != null) {
+                                node.setValueVolatile(computed);
+                                return computed;
+                            }
+
+                            // volatile ordering ensured by addSize(), but we need release here
+                            // to ensure proper ordering with reads and other writes
+                            if (prev == null) {
+                                setAtIndexRelease(table, index, node.getNextPlain());
+                            } else {
+                                prev.setNextRelease(node.getNextPlain());
+                            }
+
+                            removed = true;
+                            break;
+                        }
+                    }
+                }
+
+                if (removed) {
+                    this.subSize(1L);
+                }
+
+                return null;
+            }
+        }
+    }
+
+    /**
+     * See {@link java.util.concurrent.ConcurrentMap#merge(Object, Object, BiFunction)}
+     * <p>
+     * This function is a "functional methods" as defined by {@link ConcurrentLong2ReferenceChainedHashTable}.
+     * </p>
+     */
+    public V merge(final long key, final V def, final BiFunction<? super V, ? super V, ? extends V> function) {
+        Validate.notNull(def, "Default value may not be null");
+
+        final int hash = getHash(key);
+
+        TableEntry<V>[] table = this.table;
+        table_loop:
+        for (;;) {
+            final int index = hash & (table.length - 1);
+
+            TableEntry<V> node = getAtIndexVolatile(table, index);
+            node_loop:
+            for (;;) {
+                if (node == null) {
+                    if (null == (node = compareAndExchangeAtIndexVolatile(table, index, null, new TableEntry<>(key, def)))) {
+                        // successfully inserted
+                        this.addSize(1L);
+                        return def;
+                    } // else: node != null, fall through
+                }
+
+                if (node.resize) {
+                    table = (TableEntry<V>[])node.getValuePlain();
+                    continue table_loop;
+                }
+
+                boolean removed = false;
+                boolean added = false;
+                V ret = null;
+
+                synchronized (node) {
+                    if (node != (node = getAtIndexVolatile(table, index))) {
+                        continue node_loop;
+                    }
+                    // plain reads are fine during synchronised access, as we are the only writer
+                    TableEntry<V> prev = null;
+                    for (; node != null; prev = node, node = node.getNextPlain()) {
+                        if (node.key == key) {
+                            final V old = node.getValuePlain();
+
+                            final V computed = function.apply(old, def);
+
+                            if (computed != null) {
+                                node.setValueVolatile(computed);
+                                return computed;
+                            }
+
+                            // volatile ordering ensured by addSize(), but we need release here
+                            // to ensure proper ordering with reads and other writes
+                            if (prev == null) {
+                                setAtIndexRelease(table, index, node.getNextPlain());
+                            } else {
+                                prev.setNextRelease(node.getNextPlain());
+                            }
+
+                            removed = true;
+                            break;
+                        }
+                    }
+
+                    if (!removed) {
+                        // volatile ordering ensured by addSize(), but we need release here
+                        // to ensure proper ordering with reads and other writes
+                        prev.setNextRelease(new TableEntry<>(key, def));
+                        ret = def;
+                        added = true;
+                    }
+                }
+
+                if (removed) {
+                    this.subSize(1L);
+                }
+                if (added) {
+                    this.addSize(1L);
+                }
+
+                return ret;
+            }
+        }
+    }
+
+    /**
+     * Removes at least all entries currently mapped at the beginning of this call. May not remove entries added during
+     * this call. As a result, only if this map is not modified during the call, that all entries will be removed by
+     * the end of the call.
+     *
+     * <p>
+     * This function is not atomic.
+     * </p>
+     */
+    public void clear() {
+        // it is possible to optimise this to directly interact with the table,
+        // but we do need to be careful when interacting with resized tables,
+        // and the NodeIterator already does this logic
+        final NodeIterator<V> nodeIterator = new NodeIterator<>(this.table);
+
+        TableEntry<V> node;
+        while ((node = nodeIterator.findNext()) != null) {
+            this.remove(node.key);
+        }
+    }
+
+    /**
+     * Returns an iterator over the entries in this map. The iterator is only guaranteed to see entries that were
+     * added before the beginning of this call, but it may see entries added during.
+     */
+    public Iterator<TableEntry<V>> entryIterator() {
+        return new EntryIterator<>(this);
+    }
+
+    /**
+     * Returns an iterator over the keys in this map. The iterator is only guaranteed to see keys that were
+     * added before the beginning of this call, but it may see keys added during.
+     */
+    public PrimitiveIterator.OfLong keyIterator() {
+        return new KeyIterator<>(this);
+    }
+
+    /**
+     * Returns an iterator over the values in this map. The iterator is only guaranteed to see values that were
+     * added before the beginning of this call, but it may see values added during.
+     */
+    public Iterator<V> valueIterator() {
+        return new ValueIterator<>(this);
+    }
+
+    protected static final class EntryIterator<V> extends BaseIteratorImpl<V, TableEntry<V>> {
+
+        protected EntryIterator(final ConcurrentLong2ReferenceChainedHashTable<V> map) {
+            super(map);
+        }
+
+        @Override
+        public TableEntry<V> next() throws NoSuchElementException {
+            return this.nextNode();
+        }
+
+        @Override
+        public void forEachRemaining(final Consumer<? super TableEntry<V>> action) {
+            Validate.notNull(action, "Action may not be null");
+            while (this.hasNext()) {
+                action.accept(this.next());
+            }
+        }
+    }
+
+    protected static final class KeyIterator<V> extends BaseIteratorImpl<V, Long> implements PrimitiveIterator.OfLong {
+
+        protected KeyIterator(final ConcurrentLong2ReferenceChainedHashTable<V> map) {
+            super(map);
+        }
+
+        @Override
+        public Long next() throws NoSuchElementException {
+            return Long.valueOf(this.nextNode().key);
+        }
+
+        @Override
+        public long nextLong() {
+            return this.nextNode().key;
+        }
+
+        @Override
+        public void forEachRemaining(final Consumer<? super Long> action) {
+            Validate.notNull(action, "Action may not be null");
+
+            if (action instanceof LongConsumer longConsumer) {
+                this.forEachRemaining(longConsumer);
+                return;
+            }
+
+            while (this.hasNext()) {
+                action.accept(this.next());
+            }
+        }
+
+        @Override
+        public void forEachRemaining(final LongConsumer action) {
+            Validate.notNull(action, "Action may not be null");
+            while (this.hasNext()) {
+                action.accept(this.nextLong());
+            }
+        }
+    }
+
+    protected static final class ValueIterator<V> extends BaseIteratorImpl<V, V> {
+
+        protected ValueIterator(final ConcurrentLong2ReferenceChainedHashTable<V> map) {
+            super(map);
+        }
+
+        @Override
+        public V next() throws NoSuchElementException {
+            return this.nextNode().getValueVolatile();
+        }
+
+        @Override
+        public void forEachRemaining(final Consumer<? super V> action) {
+            Validate.notNull(action, "Action may not be null");
+            while (this.hasNext()) {
+                action.accept(this.next());
+            }
+        }
+    }
+
+    protected static abstract class BaseIteratorImpl<V, T> extends NodeIterator<V> implements Iterator<T> {
+
+        protected final ConcurrentLong2ReferenceChainedHashTable<V> map;
+        protected TableEntry<V> lastReturned;
+        protected TableEntry<V> nextToReturn;
+
+        protected BaseIteratorImpl(final ConcurrentLong2ReferenceChainedHashTable<V> map) {
+            super(map.table);
+            this.map = map;
+        }
+
+        @Override
+        public final boolean hasNext() {
+            if (this.nextToReturn != null) {
+                return true;
+            }
+
+            return (this.nextToReturn = this.findNext()) != null;
+        }
+
+        protected final TableEntry<V> nextNode() throws NoSuchElementException {
+            TableEntry<V> ret = this.nextToReturn;
+            if (ret != null) {
+                this.lastReturned = ret;
+                this.nextToReturn = null;
+                return ret;
+            }
+            ret = this.findNext();
+            if (ret != null) {
+                this.lastReturned = ret;
+                return ret;
+            }
+            throw new NoSuchElementException();
+        }
+
+        @Override
+        public final void remove() {
+            final TableEntry<V> lastReturned = this.nextToReturn;
+            if (lastReturned == null) {
+                throw new NoSuchElementException();
+            }
+            this.lastReturned = null;
+            this.map.remove(lastReturned.key);
+        }
+
+        @Override
+        public abstract T next() throws NoSuchElementException;
+
+        // overwritten by subclasses to avoid indirection on hasNext() and next()
+        @Override
+        public abstract void forEachRemaining(final Consumer<? super T> action);
+    }
+
+    protected static class NodeIterator<V> {
+
+        protected TableEntry<V>[] currentTable;
+        protected ResizeChain<V> resizeChain;
+        protected TableEntry<V> last;
+        protected int nextBin;
+        protected int increment;
+
+        protected NodeIterator(final TableEntry<V>[] baseTable) {
+            this.currentTable = baseTable;
+            this.increment = 1;
+        }
+
+        private TableEntry<V>[] pullResizeChain(final int index) {
+            final ResizeChain<V> resizeChain = this.resizeChain;
+            if (resizeChain == null) {
+                this.currentTable = null;
+                return null;
+            }
+
+            final ResizeChain<V> prevChain = resizeChain.prev;
+            this.resizeChain = prevChain;
+            if (prevChain == null) {
+                this.currentTable = null;
+                return null;
+            }
+
+            final TableEntry<V>[] newTable = prevChain.table;
+
+            // we recover the original index by modding by the new table length, as the increments applied to the index
+            // are a multiple of the new table's length
+            int newIdx = index & (newTable.length - 1);
+
+            // the increment is always the previous table's length
+            final ResizeChain<V> nextPrevChain = prevChain.prev;
+            final int increment;
+            if (nextPrevChain == null) {
+                increment = 1;
+            } else {
+                increment = nextPrevChain.table.length;
+            }
+
+            // done with the upper table, so we can skip the resize node
+            newIdx += increment;
+
+            this.increment = increment;
+            this.nextBin = newIdx;
+            this.currentTable = newTable;
+
+            return newTable;
+        }
+
+        private TableEntry<V>[] pushResizeChain(final TableEntry<V>[] table, final TableEntry<V> entry) {
+            final ResizeChain<V> chain = this.resizeChain;
+
+            if (chain == null) {
+                final TableEntry<V>[] nextTable = (TableEntry<V>[])entry.getValuePlain();
+
+                final ResizeChain<V> oldChain = new ResizeChain<>(table, null, null);
+                final ResizeChain<V> currChain = new ResizeChain<>(nextTable, oldChain, null);
+                oldChain.next = currChain;
+
+                this.increment = table.length;
+                this.resizeChain = currChain;
+                this.currentTable = nextTable;
+
+                return nextTable;
+            } else {
+                ResizeChain<V> currChain = chain.next;
+                if (currChain == null) {
+                    final TableEntry<V>[] ret = (TableEntry<V>[])entry.getValuePlain();
+                    currChain = new ResizeChain<>(ret, chain, null);
+                    chain.next = currChain;
+
+                    this.increment = table.length;
+                    this.resizeChain = currChain;
+                    this.currentTable = ret;
+
+                    return ret;
+                } else {
+                    this.increment = table.length;
+                    this.resizeChain = currChain;
+                    return this.currentTable = currChain.table;
+                }
+            }
+        }
+
+        protected final TableEntry<V> findNext() {
+            for (;;) {
+                final TableEntry<V> last = this.last;
+                if (last != null) {
+                    final TableEntry<V> next = last.getNextVolatile();
+                    if (next != null) {
+                        this.last = next;
+                        if (next.getValuePlain() == null) {
+                            // compute() node not yet available
+                            continue;
+                        }
+                        return next;
+                    }
+                }
+
+                TableEntry<V>[] table = this.currentTable;
+
+                if (table == null) {
+                    return null;
+                }
+
+                int idx = this.nextBin;
+                int increment = this.increment;
+                for (;;) {
+                    if (idx >= table.length) {
+                        table = this.pullResizeChain(idx);
+                        idx = this.nextBin;
+                        increment = this.increment;
+                        if (table != null) {
+                            continue;
+                        } else {
+                            this.last = null;
+                            return null;
+                        }
+                    }
+
+                    final TableEntry<V> entry = getAtIndexVolatile(table, idx);
+                    if (entry == null) {
+                        idx += increment;
+                        continue;
+                    }
+
+                    if (entry.resize) {
+                        // push onto resize chain
+                        table = this.pushResizeChain(table, entry);
+                        increment = this.increment;
+                        continue;
+                    }
+
+                    this.last = entry;
+                    this.nextBin = idx + increment;
+                    if (entry.getValuePlain() != null) {
+                        return entry;
+                    } else {
+                        // compute() node not yet available
+                        break;
+                    }
+                }
+            }
+        }
+
+        protected static final class ResizeChain<V> {
+
+            protected final TableEntry<V>[] table;
+            protected final ResizeChain<V> prev;
+            protected ResizeChain<V> next;
+
+            protected ResizeChain(final TableEntry<V>[] table, final ResizeChain<V> prev, final ResizeChain<V> next) {
+                this.table = table;
+                this.prev = prev;
+                this.next = next;
+            }
+        }
+    }
+
+    public static final class TableEntry<V> {
+
+        protected static final VarHandle TABLE_ENTRY_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(TableEntry[].class);
+
+        protected final boolean resize;
+
+        protected final long key;
+
+        protected volatile V value;
+        protected static final VarHandle VALUE_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "value", Object.class);
+
+        protected final V getValuePlain() {
+            //noinspection unchecked
+            return (V)VALUE_HANDLE.get(this);
+        }
+
+        protected final V getValueAcquire() {
+            //noinspection unchecked
+            return (V)VALUE_HANDLE.getAcquire(this);
+        }
+
+        protected final V getValueVolatile() {
+            //noinspection unchecked
+            return (V)VALUE_HANDLE.getVolatile(this);
+        }
+
+        protected final void setValuePlain(final V value) {
+            VALUE_HANDLE.set(this, (Object)value);
+        }
+
+        protected final void setValueRelease(final V value) {
+            VALUE_HANDLE.setRelease(this, (Object)value);
+        }
+
+        protected final void setValueVolatile(final V value) {
+            VALUE_HANDLE.setVolatile(this, (Object)value);
+        }
+
+        protected volatile TableEntry<V> next;
+        protected static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "next", TableEntry.class);
+
+        protected final TableEntry<V> getNextPlain() {
+            //noinspection unchecked
+            return (TableEntry<V>)NEXT_HANDLE.get(this);
+        }
+
+        protected final TableEntry<V> getNextVolatile() {
+            //noinspection unchecked
+            return (TableEntry<V>)NEXT_HANDLE.getVolatile(this);
+        }
+
+        protected final void setNextPlain(final TableEntry<V> next) {
+            NEXT_HANDLE.set(this, next);
+        }
+
+        protected final void setNextRelease(final TableEntry<V> next) {
+            NEXT_HANDLE.setRelease(this, next);
+        }
+
+        protected final void setNextVolatile(final TableEntry<V> next) {
+            NEXT_HANDLE.setVolatile(this, next);
+        }
+
+        public TableEntry(final long key, final V value) {
+            this.resize = false;
+            this.key = key;
+            this.setValuePlain(value);
+        }
+
+        public TableEntry(final long key, final V value, final boolean resize) {
+            this.resize = resize;
+            this.key = key;
+            this.setValuePlain(value);
+        }
+
+        public long getKey() {
+            return this.key;
+        }
+
+        public V getValue() {
+            return this.getValueVolatile();
+        }
+    }
+}
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRHashTable.java b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRHashTable.java
new file mode 100644
index 0000000000000000000000000000000000000000..83965350d292ccf42a34520d84dcda3f88146cff
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRHashTable.java
@@ -0,0 +1,1656 @@
+package ca.spottedleaf.concurrentutil.map;
+
+import ca.spottedleaf.concurrentutil.util.CollectionUtil;
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
+import ca.spottedleaf.concurrentutil.util.HashUtil;
+import ca.spottedleaf.concurrentutil.util.IntegerUtil;
+import ca.spottedleaf.concurrentutil.util.Validate;
+import java.lang.invoke.VarHandle;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Set;
+import java.util.Spliterator;
+import java.util.Spliterators;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+import java.util.function.BiPredicate;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.IntFunction;
+import java.util.function.Predicate;
+
+/**
+ * <p>
+ * Note: Not really tested, use at your own risk.
+ * </p>
+ * This map is safe for reading from multiple threads, however it is only safe to write from a single thread.
+ * {@code null} keys or values are not permitted. Writes to values in this map are guaranteed to be ordered by release semantics,
+ * however immediate visibility to other threads is not guaranteed. However, writes are guaranteed to be made visible eventually.
+ * Reads are ordered by acquire semantics.
+ * <p>
+ * Iterators cannot be modified concurrently, and its backing map cannot be modified concurrently. There is no
+ * fast-fail attempt made by iterators, thus modifying the iterator's backing map while iterating will have undefined
+ * behaviour.
+ * </p>
+ * <p>
+ * Subclasses should override {@link #clone()} to return correct instances of this class.
+ * </p>
+ * @param <K> {@inheritDoc}
+ * @param <V> {@inheritDoc}
+ */
+public class SWMRHashTable<K, V> implements Map<K, V>, Iterable<Map.Entry<K, V>> {
+
+    protected int size;
+
+    protected TableEntry<K, V>[] table;
+
+    protected final float loadFactor;
+
+    protected static final VarHandle SIZE_HANDLE = ConcurrentUtil.getVarHandle(SWMRHashTable.class, "size", int.class);
+    protected static final VarHandle TABLE_HANDLE = ConcurrentUtil.getVarHandle(SWMRHashTable.class, "table", TableEntry[].class);
+
+    /* size */
+
+    protected final int getSizePlain() {
+        return (int)SIZE_HANDLE.get(this);
+    }
+
+    protected final int getSizeOpaque() {
+        return (int)SIZE_HANDLE.getOpaque(this);
+    }
+
+    protected final int getSizeAcquire() {
+        return (int)SIZE_HANDLE.getAcquire(this);
+    }
+
+    protected final void setSizePlain(final int value) {
+        SIZE_HANDLE.set(this, value);
+    }
+
+    protected final void setSizeOpaque(final int value) {
+        SIZE_HANDLE.setOpaque(this, value);
+    }
+
+    protected final void setSizeRelease(final int value) {
+        SIZE_HANDLE.setRelease(this, value);
+    }
+
+    /* table */
+
+    protected final TableEntry<K, V>[] getTablePlain() {
+        //noinspection unchecked
+        return (TableEntry<K, V>[])TABLE_HANDLE.get(this);
+    }
+
+    protected final TableEntry<K, V>[] getTableAcquire() {
+        //noinspection unchecked
+        return (TableEntry<K, V>[])TABLE_HANDLE.getAcquire(this);
+    }
+
+    protected final void setTablePlain(final TableEntry<K, V>[] table) {
+        TABLE_HANDLE.set(this, table);
+    }
+
+    protected final void setTableRelease(final TableEntry<K, V>[] table) {
+        TABLE_HANDLE.setRelease(this, table);
+    }
+
+    protected static final int DEFAULT_CAPACITY = 16;
+    protected static final float DEFAULT_LOAD_FACTOR = 0.75f;
+    protected static final int MAXIMUM_CAPACITY = Integer.MIN_VALUE >>> 1;
+
+    /**
+     * Constructs this map with a capacity of {@code 16} and load factor of {@code 0.75f}.
+     */
+    public SWMRHashTable() {
+        this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR);
+    }
+
+    /**
+     * Constructs this map with the specified capacity and load factor of {@code 0.75f}.
+     * @param capacity specified initial capacity, > 0
+     */
+    public SWMRHashTable(final int capacity) {
+        this(capacity, DEFAULT_LOAD_FACTOR);
+    }
+
+    /**
+     * Constructs this map with the specified capacity and load factor.
+     * @param capacity specified capacity, > 0
+     * @param loadFactor specified load factor, > 0 && finite
+     */
+    public SWMRHashTable(final int capacity, final float loadFactor) {
+        final int tableSize = getCapacityFor(capacity);
+
+        if (loadFactor <= 0.0 || !Float.isFinite(loadFactor)) {
+            throw new IllegalArgumentException("Invalid load factor: " + loadFactor);
+        }
+
+        //noinspection unchecked
+        final TableEntry<K, V>[] table = new TableEntry[tableSize];
+        this.setTablePlain(table);
+
+        if (tableSize == MAXIMUM_CAPACITY) {
+            this.threshold = -1;
+        } else {
+            this.threshold = getTargetCapacity(tableSize, loadFactor);
+        }
+
+        this.loadFactor = loadFactor;
+    }
+
+    /**
+     * Constructs this map with a capacity of {@code 16} or the specified map's size, whichever is larger, and
+     * with a load factor of {@code 0.75f}.
+     * All of the specified map's entries are copied into this map.
+     * @param other The specified map.
+     */
+    public SWMRHashTable(final Map<K, V> other) {
+        this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR, other);
+    }
+
+    /**
+     * Constructs this map with a minimum capacity of the specified capacity or the specified map's size, whichever is larger, and
+     * with a load factor of {@code 0.75f}.
+     * All of the specified map's entries are copied into this map.
+     * @param capacity specified capacity, > 0
+     * @param other The specified map.
+     */
+    public SWMRHashTable(final int capacity, final Map<K, V> other) {
+        this(capacity, DEFAULT_LOAD_FACTOR, other);
+    }
+
+    /**
+     * Constructs this map with a min capacity of the specified capacity or the specified map's size, whichever is larger, and
+     * with the specified load factor.
+     * All of the specified map's entries are copied into this map.
+     * @param capacity specified capacity, > 0
+     * @param loadFactor specified load factor, > 0 && finite
+     * @param other The specified map.
+     */
+    public SWMRHashTable(final int capacity, final float loadFactor, final Map<K, V> other) {
+        this(Math.max(Validate.notNull(other, "Null map").size(), capacity), loadFactor);
+        this.putAll(other);
+    }
+
+    protected static <K, V> TableEntry<K, V> getAtIndexOpaque(final TableEntry<K, V>[] table, final int index) {
+        // noinspection unchecked
+        return (TableEntry<K, V>)TableEntry.TABLE_ENTRY_ARRAY_HANDLE.getOpaque(table, index);
+    }
+
+    protected static <K, V> void setAtIndexRelease(final TableEntry<K, V>[] table, final int index, final TableEntry<K, V> value) {
+        TableEntry.TABLE_ENTRY_ARRAY_HANDLE.setRelease(table, index, value);
+    }
+
+    public final float getLoadFactor() {
+        return this.loadFactor;
+    }
+
+    protected static int getCapacityFor(final int capacity) {
+        if (capacity <= 0) {
+            throw new IllegalArgumentException("Invalid capacity: " + capacity);
+        }
+        if (capacity >= MAXIMUM_CAPACITY) {
+            return MAXIMUM_CAPACITY;
+        }
+        return IntegerUtil.roundCeilLog2(capacity);
+    }
+
+    /** Callers must still use acquire when reading the value of the entry. */
+    protected final TableEntry<K, V> getEntryForOpaque(final K key) {
+        final int hash = SWMRHashTable.getHash(key);
+        final TableEntry<K, V>[] table = this.getTableAcquire();
+
+        for (TableEntry<K, V> curr = getAtIndexOpaque(table, hash & (table.length - 1)); curr != null; curr = curr.getNextOpaque()) {
+            if (hash == curr.hash && (key == curr.key || curr.key.equals(key))) {
+                return curr;
+            }
+        }
+
+        return null;
+    }
+
+    protected final TableEntry<K, V> getEntryForPlain(final K key) {
+        final int hash = SWMRHashTable.getHash(key);
+        final TableEntry<K, V>[] table = this.getTablePlain();
+
+        for (TableEntry<K, V> curr = table[hash & (table.length - 1)]; curr != null; curr = curr.getNextPlain()) {
+            if (hash == curr.hash && (key == curr.key || curr.key.equals(key))) {
+                return curr;
+            }
+        }
+
+        return null;
+    }
+
+    /* MT-Safe */
+
+    /** must be deterministic given a key */
+    private static int getHash(final Object key) {
+        int hash = key == null ? 0 : key.hashCode();
+        return HashUtil.mix(hash);
+    }
+
+    // rets -1 if capacity*loadFactor is too large
+    protected static int getTargetCapacity(final int capacity, final float loadFactor) {
+        final double ret = (double)capacity * (double)loadFactor;
+        if (Double.isInfinite(ret) || ret >= ((double)Integer.MAX_VALUE)) {
+            return -1;
+        }
+
+        return (int)ret;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        /* Make no attempt to deal with concurrent modifications */
+        if (!(obj instanceof Map<?, ?> other)) {
+            return false;
+        }
+
+        if (this.size() != other.size()) {
+            return false;
+        }
+
+        final TableEntry<K, V>[] table = this.getTableAcquire();
+
+        for (int i = 0, len = table.length; i < len; ++i) {
+            for (TableEntry<K, V> curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
+                final V value = curr.getValueAcquire();
+
+                final Object otherValue = other.get(curr.key);
+                if (otherValue == null || (value != otherValue && value.equals(otherValue))) {
+                    return false;
+                }
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int hashCode() {
+        /* Make no attempt to deal with concurrent modifications */
+        int hash = 0;
+        final TableEntry<K, V>[] table = this.getTableAcquire();
+
+        for (int i = 0, len = table.length; i < len; ++i) {
+            for (TableEntry<K, V> curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
+                hash += curr.hashCode();
+            }
+        }
+
+        return hash;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder(64);
+        builder.append("SWMRHashTable:{");
+
+        this.forEach((final K key, final V value) -> {
+            builder.append("{key: \"").append(key).append("\", value: \"").append(value).append("\"}");
+        });
+
+        return builder.append('}').toString();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public SWMRHashTable<K, V> clone() {
+        return new SWMRHashTable<>(this.getTableAcquire().length, this.loadFactor, this);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public Iterator<Map.Entry<K, V>> iterator() {
+        return new EntryIterator<>(this.getTableAcquire(), this);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void forEach(final Consumer<? super Map.Entry<K, V>> action) {
+        Validate.notNull(action, "Null action");
+
+        final TableEntry<K, V>[] table = this.getTableAcquire();
+        for (int i = 0, len = table.length; i < len; ++i) {
+            for (TableEntry<K, V> curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
+                action.accept(curr);
+            }
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void forEach(final BiConsumer<? super K, ? super V> action) {
+        Validate.notNull(action, "Null action");
+
+        final TableEntry<K, V>[] table = this.getTableAcquire();
+        for (int i = 0, len = table.length; i < len; ++i) {
+            for (TableEntry<K, V> curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
+                final V value = curr.getValueAcquire();
+
+                action.accept(curr.key, value);
+            }
+        }
+    }
+
+    /**
+     * Provides the specified consumer with all keys contained within this map.
+     * @param action The specified consumer.
+     */
+    public void forEachKey(final Consumer<? super K> action) {
+        Validate.notNull(action, "Null action");
+
+        final TableEntry<K, V>[] table = this.getTableAcquire();
+        for (int i = 0, len = table.length; i < len; ++i) {
+            for (TableEntry<K, V> curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
+                action.accept(curr.key);
+            }
+        }
+    }
+
+    /**
+     * Provides the specified consumer with all values contained within this map. Equivalent to {@code map.values().forEach(Consumer)}.
+     * @param action The specified consumer.
+     */
+    public void forEachValue(final Consumer<? super V> action) {
+        Validate.notNull(action, "Null action");
+
+        final TableEntry<K, V>[] table = this.getTableAcquire();
+        for (int i = 0, len = table.length; i < len; ++i) {
+            for (TableEntry<K, V> curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
+                final V value = curr.getValueAcquire();
+
+                action.accept(value);
+            }
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public V get(final Object key) {
+        Validate.notNull(key, "Null key");
+
+        //noinspection unchecked
+        final TableEntry<K, V> entry = this.getEntryForOpaque((K)key);
+        return entry == null ? null : entry.getValueAcquire();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean containsKey(final Object key) {
+        Validate.notNull(key, "Null key");
+
+        // note: we need to use getValueAcquire, so that the reads from this map are ordered by acquire semantics
+        return this.get(key) != null;
+    }
+
+    /**
+     * Returns {@code true} if this map contains an entry with the specified key and value at some point during this call.
+     * @param key The specified key.
+     * @param value The specified value.
+     * @return {@code true} if this map contains an entry with the specified key and value.
+     */
+    public boolean contains(final Object key, final Object value) {
+        Validate.notNull(key, "Null key");
+
+        //noinspection unchecked
+        final TableEntry<K, V> entry = this.getEntryForOpaque((K)key);
+
+        if (entry == null) {
+            return false;
+        }
+
+        final V entryVal = entry.getValueAcquire();
+        return entryVal == value || entryVal.equals(value);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean containsValue(final Object value) {
+        Validate.notNull(value, "Null value");
+
+        final TableEntry<K, V>[] table = this.getTableAcquire();
+        for (int i = 0, len = table.length; i < len; ++i) {
+            for (TableEntry<K, V> curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
+                final V currVal = curr.getValueAcquire();
+                if (currVal == value || currVal.equals(value)) {
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public V getOrDefault(final Object key, final V defaultValue) {
+        Validate.notNull(key, "Null key");
+
+        //noinspection unchecked
+        final TableEntry<K, V> entry = this.getEntryForOpaque((K)key);
+
+        return entry == null ? defaultValue : entry.getValueAcquire();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int size() {
+        return this.getSizeAcquire();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean isEmpty() {
+        return this.getSizeAcquire() == 0;
+    }
+
+    protected KeySet<K, V> keyset;
+    protected ValueCollection<K, V> values;
+    protected EntrySet<K, V> entrySet;
+
+    @Override
+    public Set<K> keySet() {
+        return this.keyset == null ? this.keyset = new KeySet<>(this) : this.keyset;
+    }
+
+    @Override
+    public Collection<V> values() {
+        return this.values == null ? this.values = new ValueCollection<>(this) : this.values;
+    }
+
+    @Override
+    public Set<Map.Entry<K, V>> entrySet() {
+        return this.entrySet == null ? this.entrySet = new EntrySet<>(this) : this.entrySet;
+    }
+
+    /* Non-MT-Safe */
+
+    protected int threshold;
+
+    protected final void checkResize(final int minCapacity) {
+        if (minCapacity <= this.threshold || this.threshold < 0) {
+            return;
+        }
+
+        final TableEntry<K, V>[] table = this.getTablePlain();
+        int newCapacity = minCapacity >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY : IntegerUtil.roundCeilLog2(minCapacity);
+        if (newCapacity < 0) {
+            newCapacity = MAXIMUM_CAPACITY;
+        }
+        if (newCapacity <= table.length) {
+            if (newCapacity == MAXIMUM_CAPACITY) {
+                return;
+            }
+            newCapacity = table.length << 1;
+        }
+
+        //noinspection unchecked
+        final TableEntry<K, V>[] newTable = new TableEntry[newCapacity];
+        final int indexMask = newCapacity - 1;
+
+        for (int i = 0, len = table.length; i < len; ++i) {
+            for (TableEntry<K, V> entry = table[i]; entry != null; entry = entry.getNextPlain()) {
+                final int hash = entry.hash;
+                final int index = hash & indexMask;
+
+                /* we need to create a new entry since there could be reading threads */
+                final TableEntry<K, V> insert = new TableEntry<>(hash, entry.key, entry.getValuePlain());
+
+                final TableEntry<K, V> prev = newTable[index];
+
+                newTable[index] = insert;
+                insert.setNextPlain(prev);
+            }
+        }
+
+        if (newCapacity == MAXIMUM_CAPACITY) {
+            this.threshold = -1; /* No more resizing */
+        } else {
+            this.threshold = getTargetCapacity(newCapacity, this.loadFactor);
+        }
+        this.setTableRelease(newTable); /* use release to publish entries in table */
+    }
+
+    protected final int addToSize(final int num) {
+        final int newSize = this.getSizePlain() + num;
+
+        this.setSizeOpaque(newSize);
+        this.checkResize(newSize);
+
+        return newSize;
+    }
+
+    protected final int removeFromSize(final int num) {
+        final int newSize = this.getSizePlain() - num;
+
+        this.setSizeOpaque(newSize);
+
+        return newSize;
+    }
+
+    /* Cannot be used to perform downsizing */
+    protected final int removeFromSizePlain(final int num) {
+        final int newSize = this.getSizePlain() - num;
+
+        this.setSizePlain(newSize);
+
+        return newSize;
+    }
+
+    protected final V put(final K key, final V value, final boolean onlyIfAbsent) {
+        final TableEntry<K, V>[] table = this.getTablePlain();
+        final int hash = SWMRHashTable.getHash(key);
+        final int index = hash & (table.length - 1);
+
+        final TableEntry<K, V> head = table[index];
+        if (head == null) {
+            final TableEntry<K, V> insert = new TableEntry<>(hash, key, value);
+            setAtIndexRelease(table, index, insert);
+            this.addToSize(1);
+            return null;
+        }
+
+        for (TableEntry<K, V> curr = head;;) {
+            if (curr.hash == hash && (key == curr.key || curr.key.equals(key))) {
+                if (onlyIfAbsent) {
+                    return curr.getValuePlain();
+                }
+
+                final V currVal = curr.getValuePlain();
+                curr.setValueRelease(value);
+                return currVal;
+            }
+
+            final TableEntry<K, V> next = curr.getNextPlain();
+            if (next != null) {
+                curr = next;
+                continue;
+            }
+
+            final TableEntry<K, V> insert = new TableEntry<>(hash, key, value);
+
+            curr.setNextRelease(insert);
+            this.addToSize(1);
+            return null;
+        }
+    }
+
+    /**
+     * Removes a key-value pair from this map if the specified predicate returns true. The specified predicate is
+     * tested with every entry in this map. Returns the number of key-value pairs removed.
+     * @param predicate The predicate to test key-value pairs against.
+     * @return The total number of key-value pairs removed from this map.
+     */
+    public int removeIf(final BiPredicate<K, V> predicate) {
+        Validate.notNull(predicate, "Null predicate");
+
+        int removed = 0;
+
+        final TableEntry<K, V>[] table = this.getTablePlain();
+
+        bin_iteration_loop:
+        for (int i = 0, len = table.length; i < len; ++i) {
+            TableEntry<K, V> curr = table[i];
+            if (curr == null) {
+                continue;
+            }
+
+            /* Handle bin nodes first */
+            while (predicate.test(curr.key, curr.getValuePlain())) {
+                ++removed;
+                this.removeFromSizePlain(1); /* required in case predicate throws an exception */
+
+                setAtIndexRelease(table, i, curr = curr.getNextPlain());
+
+                if (curr == null) {
+                    continue bin_iteration_loop;
+                }
+            }
+
+            TableEntry<K, V> prev;
+
+            /* curr at this point is the bin node */
+
+            for (prev = curr, curr = curr.getNextPlain(); curr != null;) {
+                /* If we want to remove, then we should hold prev, as it will be a valid entry to link on */
+                if (predicate.test(curr.key, curr.getValuePlain())) {
+                    ++removed;
+                    this.removeFromSizePlain(1); /* required in case predicate throws an exception */
+
+                    prev.setNextRelease(curr = curr.getNextPlain());
+                } else {
+                    prev = curr;
+                    curr = curr.getNextPlain();
+                }
+            }
+        }
+
+        return removed;
+    }
+
+    /**
+     * Removes a key-value pair from this map if the specified predicate returns true. The specified predicate is
+     * tested with every entry in this map. Returns the number of key-value pairs removed.
+     * @param predicate The predicate to test key-value pairs against.
+     * @return The total number of key-value pairs removed from this map.
+     */
+    public int removeEntryIf(final Predicate<? super Map.Entry<K, V>> predicate) {
+        Validate.notNull(predicate, "Null predicate");
+
+        int removed = 0;
+
+        final TableEntry<K, V>[] table = this.getTablePlain();
+
+        bin_iteration_loop:
+        for (int i = 0, len = table.length; i < len; ++i) {
+            TableEntry<K, V> curr = table[i];
+            if (curr == null) {
+                continue;
+            }
+
+            /* Handle bin nodes first */
+            while (predicate.test(curr)) {
+                ++removed;
+                this.removeFromSizePlain(1); /* required in case predicate throws an exception */
+
+                setAtIndexRelease(table, i, curr = curr.getNextPlain());
+
+                if (curr == null) {
+                    continue bin_iteration_loop;
+                }
+            }
+
+            TableEntry<K, V> prev;
+
+            /* curr at this point is the bin node */
+
+            for (prev = curr, curr = curr.getNextPlain(); curr != null;) {
+                /* If we want to remove, then we should hold prev, as it will be a valid entry to link on */
+                if (predicate.test(curr)) {
+                    ++removed;
+                    this.removeFromSizePlain(1); /* required in case predicate throws an exception */
+
+                    prev.setNextRelease(curr = curr.getNextPlain());
+                } else {
+                    prev = curr;
+                    curr = curr.getNextPlain();
+                }
+            }
+        }
+
+        return removed;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public V put(final K key, final V value) {
+        Validate.notNull(key, "Null key");
+        Validate.notNull(value, "Null value");
+
+        return this.put(key, value, false);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public V putIfAbsent(final K key, final V value) {
+        Validate.notNull(key, "Null key");
+        Validate.notNull(value, "Null value");
+
+        return this.put(key, value, true);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean remove(final Object key, final Object value) {
+        Validate.notNull(key, "Null key");
+        Validate.notNull(value, "Null value");
+
+        final TableEntry<K, V>[] table = this.getTablePlain();
+        final int hash = SWMRHashTable.getHash(key);
+        final int index = hash & (table.length - 1);
+
+        final TableEntry<K, V> head = table[index];
+        if (head == null) {
+            return false;
+        }
+
+        if (head.hash == hash && (head.key == key || head.key.equals(key))) {
+            final V currVal = head.getValuePlain();
+
+            if (currVal != value && !currVal.equals(value)) {
+                return false;
+            }
+
+            setAtIndexRelease(table, index, head.getNextPlain());
+            this.removeFromSize(1);
+
+            return true;
+        }
+
+        for (TableEntry<K, V> curr = head.getNextPlain(), prev = head; curr != null; prev = curr, curr = curr.getNextPlain()) {
+            if (curr.hash == hash && (curr.key == key || curr.key.equals(key))) {
+                final V currVal = curr.getValuePlain();
+
+                if (currVal != value && !currVal.equals(value)) {
+                    return false;
+                }
+
+                prev.setNextRelease(curr.getNextPlain());
+                this.removeFromSize(1);
+
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    protected final V remove(final Object key, final int hash) {
+        final TableEntry<K, V>[] table = this.getTablePlain();
+        final int index = (table.length - 1) & hash;
+
+        final TableEntry<K, V> head = table[index];
+        if (head == null) {
+            return null;
+        }
+
+        if (hash == head.hash && (head.key == key || head.key.equals(key))) {
+            setAtIndexRelease(table, index, head.getNextPlain());
+            this.removeFromSize(1);
+
+            return head.getValuePlain();
+        }
+
+        for (TableEntry<K, V> curr = head.getNextPlain(), prev = head; curr != null; prev = curr, curr = curr.getNextPlain()) {
+            if (curr.hash == hash && (key == curr.key || curr.key.equals(key))) {
+                prev.setNextRelease(curr.getNextPlain());
+                this.removeFromSize(1);
+
+                return curr.getValuePlain();
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public V remove(final Object key) {
+        Validate.notNull(key, "Null key");
+
+        return this.remove(key, SWMRHashTable.getHash(key));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean replace(final K key, final V oldValue, final V newValue) {
+        Validate.notNull(key, "Null key");
+        Validate.notNull(oldValue, "Null oldValue");
+        Validate.notNull(newValue, "Null newValue");
+
+        final TableEntry<K, V> entry = this.getEntryForPlain(key);
+        if (entry == null) {
+            return false;
+        }
+
+        final V currValue = entry.getValuePlain();
+        if (currValue == oldValue || currValue.equals(oldValue)) {
+            entry.setValueRelease(newValue);
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public V replace(final K key, final V value) {
+        Validate.notNull(key, "Null key");
+        Validate.notNull(value, "Null value");
+
+        final TableEntry<K, V> entry = this.getEntryForPlain(key);
+        if (entry == null) {
+            return null;
+        }
+
+        final V prev = entry.getValuePlain();
+        entry.setValueRelease(value);
+        return prev;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void replaceAll(final BiFunction<? super K, ? super V, ? extends V> function) {
+        Validate.notNull(function, "Null function");
+
+        final TableEntry<K, V>[] table = this.getTablePlain();
+        for (int i = 0, len = table.length; i < len; ++i) {
+            for (TableEntry<K, V> curr = table[i]; curr != null; curr = curr.getNextPlain()) {
+                final V value = curr.getValuePlain();
+
+                final V newValue = function.apply(curr.key, value);
+                if (newValue == null) {
+                    throw new NullPointerException();
+                }
+
+                curr.setValueRelease(newValue);
+            }
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void putAll(final Map<? extends K, ? extends V> map) {
+        Validate.notNull(map, "Null map");
+
+        final int size = map.size();
+        this.checkResize(Math.max(this.getSizePlain() + size/2, size)); /* preemptively resize */
+        map.forEach(this::put);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * This call is non-atomic and the order that which entries are removed is undefined. The clear operation itself
+     * is release ordered, that is, after the clear operation is performed a release fence is performed.
+     * </p>
+     */
+    @Override
+    public void clear() {
+        Arrays.fill(this.getTablePlain(), null);
+        this.setSizeRelease(0);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public V compute(final K key, final BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
+        Validate.notNull(key, "Null key");
+        Validate.notNull(remappingFunction, "Null remappingFunction");
+
+        final int hash = SWMRHashTable.getHash(key);
+        final TableEntry<K, V>[] table = this.getTablePlain();
+        final int index = hash & (table.length - 1);
+
+        for (TableEntry<K, V> curr = table[index], prev = null;;prev = curr, curr = curr.getNextPlain()) {
+            if (curr == null) {
+                final V newVal = remappingFunction.apply(key ,null);
+
+                if (newVal == null) {
+                    return null;
+                }
+
+                final TableEntry<K, V> insert = new TableEntry<>(hash, key, newVal);
+                if (prev == null) {
+                    setAtIndexRelease(table, index, insert);
+                } else {
+                    prev.setNextRelease(insert);
+                }
+
+                this.addToSize(1);
+
+                return newVal;
+            }
+
+            if (curr.hash == hash && (curr.key == key || curr.key.equals(key))) {
+                final V newVal = remappingFunction.apply(key, curr.getValuePlain());
+
+                if (newVal != null) {
+                    curr.setValueRelease(newVal);
+                    return newVal;
+                }
+
+                if (prev == null) {
+                    setAtIndexRelease(table, index, curr.getNextPlain());
+                } else {
+                    prev.setNextRelease(curr.getNextPlain());
+                }
+
+                this.removeFromSize(1);
+
+                return null;
+            }
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public V computeIfPresent(final K key, final BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
+        Validate.notNull(key, "Null key");
+        Validate.notNull(remappingFunction, "Null remappingFunction");
+
+        final int hash = SWMRHashTable.getHash(key);
+        final TableEntry<K, V>[] table = this.getTablePlain();
+        final int index = hash & (table.length - 1);
+
+        for (TableEntry<K, V> curr = table[index], prev = null; curr != null; prev = curr, curr = curr.getNextPlain()) {
+            if (curr.hash != hash || (curr.key != key && !curr.key.equals(key))) {
+                continue;
+            }
+
+            final V newVal = remappingFunction.apply(key, curr.getValuePlain());
+            if (newVal != null) {
+                curr.setValueRelease(newVal);
+                return newVal;
+            }
+
+            if (prev == null) {
+                setAtIndexRelease(table, index, curr.getNextPlain());
+            } else {
+                prev.setNextRelease(curr.getNextPlain());
+            }
+
+            this.removeFromSize(1);
+
+            return null;
+        }
+
+        return null;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public V computeIfAbsent(final K key, final Function<? super K, ? extends V> mappingFunction) {
+        Validate.notNull(key, "Null key");
+        Validate.notNull(mappingFunction, "Null mappingFunction");
+
+        final int hash = SWMRHashTable.getHash(key);
+        final TableEntry<K, V>[] table = this.getTablePlain();
+        final int index = hash & (table.length - 1);
+
+        for (TableEntry<K, V> curr = table[index], prev = null;;prev = curr, curr = curr.getNextPlain()) {
+            if (curr != null) {
+                if (curr.hash == hash && (curr.key == key || curr.key.equals(key))) {
+                    return curr.getValuePlain();
+                }
+                continue;
+            }
+
+            final V newVal = mappingFunction.apply(key);
+
+            if (newVal == null) {
+                return null;
+            }
+
+            final TableEntry<K, V> insert = new TableEntry<>(hash, key, newVal);
+            if (prev == null) {
+                setAtIndexRelease(table, index, insert);
+            } else {
+                prev.setNextRelease(insert);
+            }
+
+            this.addToSize(1);
+
+            return newVal;
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public V merge(final K key, final V value, final BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
+        Validate.notNull(key, "Null key");
+        Validate.notNull(value, "Null value");
+        Validate.notNull(remappingFunction, "Null remappingFunction");
+
+        final int hash = SWMRHashTable.getHash(key);
+        final TableEntry<K, V>[] table = this.getTablePlain();
+        final int index = hash & (table.length - 1);
+
+        for (TableEntry<K, V> curr = table[index], prev = null;;prev = curr, curr = curr.getNextPlain()) {
+            if (curr == null) {
+                final TableEntry<K, V> insert = new TableEntry<>(hash, key, value);
+                if (prev == null) {
+                    setAtIndexRelease(table, index, insert);
+                } else {
+                    prev.setNextRelease(insert);
+                }
+
+                this.addToSize(1);
+
+                return value;
+            }
+
+            if (curr.hash == hash && (curr.key == key || curr.key.equals(key))) {
+                final V newVal = remappingFunction.apply(curr.getValuePlain(), value);
+
+                if (newVal != null) {
+                    curr.setValueRelease(newVal);
+                    return newVal;
+                }
+
+                if (prev == null) {
+                    setAtIndexRelease(table, index, curr.getNextPlain());
+                } else {
+                    prev.setNextRelease(curr.getNextPlain());
+                }
+
+                this.removeFromSize(1);
+
+                return null;
+            }
+        }
+    }
+
+    protected static final class TableEntry<K, V> implements Map.Entry<K, V> {
+
+        protected static final VarHandle TABLE_ENTRY_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(TableEntry[].class);
+
+        protected final int hash;
+        protected final K key;
+        protected V value;
+
+        protected TableEntry<K, V> next;
+
+        protected static final VarHandle VALUE_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "value", Object.class);
+        protected static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "next", TableEntry.class);
+
+        /* value */
+
+        protected final V getValuePlain() {
+            //noinspection unchecked
+            return (V)VALUE_HANDLE.get(this);
+        }
+
+        protected final V getValueAcquire() {
+            //noinspection unchecked
+            return (V)VALUE_HANDLE.getAcquire(this);
+        }
+
+        protected final void setValueRelease(final V to) {
+            VALUE_HANDLE.setRelease(this, to);
+        }
+
+        /* next */
+
+        protected final TableEntry<K, V> getNextPlain() {
+            //noinspection unchecked
+            return (TableEntry<K, V>)NEXT_HANDLE.get(this);
+        }
+
+        protected final TableEntry<K, V> getNextOpaque() {
+            //noinspection unchecked
+            return (TableEntry<K, V>)NEXT_HANDLE.getOpaque(this);
+        }
+
+        protected final void setNextPlain(final TableEntry<K, V> next) {
+            NEXT_HANDLE.set(this, next);
+        }
+
+        protected final void setNextRelease(final TableEntry<K, V> next) {
+            NEXT_HANDLE.setRelease(this, next);
+        }
+
+        protected TableEntry(final int hash, final K key, final V value) {
+            this.hash = hash;
+            this.key = key;
+            this.value = value;
+        }
+
+        @Override
+        public K getKey() {
+            return this.key;
+        }
+
+        @Override
+        public V getValue() {
+            return this.getValueAcquire();
+        }
+
+        @Override
+        public V setValue(final V value) {
+            throw new UnsupportedOperationException();
+        }
+
+        protected static int hash(final Object key, final Object value) {
+            return key.hashCode() ^ (value == null ? 0 : value.hashCode());
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public int hashCode() {
+            return hash(this.key, this.getValueAcquire());
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public boolean equals(final Object obj) {
+            if (this == obj) {
+                return true;
+            }
+
+            if (!(obj instanceof Map.Entry<?, ?> other)) {
+                return false;
+            }
+            final Object otherKey = other.getKey();
+            final Object otherValue = other.getValue();
+
+            final K thisKey = this.getKey();
+            final V thisVal = this.getValueAcquire();
+            return (thisKey == otherKey || thisKey.equals(otherKey)) &&
+                (thisVal == otherValue || thisVal.equals(otherValue));
+        }
+    }
+
+
+    protected static abstract class TableEntryIterator<K, V, T> implements Iterator<T> {
+
+        protected final TableEntry<K, V>[] table;
+        protected final SWMRHashTable<K, V> map;
+
+        /* bin which our current element resides on */
+        protected int tableIndex;
+
+        protected TableEntry<K, V> currEntry; /* curr entry, null if no more to iterate or if curr was removed or if we've just init'd */
+        protected TableEntry<K, V> nextEntry; /* may not be on the same bin as currEntry */
+
+        protected TableEntryIterator(final TableEntry<K, V>[] table, final SWMRHashTable<K, V> map) {
+            this.table = table;
+            this.map = map;
+            int tableIndex = 0;
+            for (int len = table.length; tableIndex < len; ++tableIndex) {
+                final TableEntry<K, V> entry = getAtIndexOpaque(table, tableIndex);
+                if (entry != null) {
+                    this.nextEntry = entry;
+                    this.tableIndex = tableIndex + 1;
+                    return;
+                }
+            }
+            this.tableIndex = tableIndex;
+        }
+
+        @Override
+        public boolean hasNext() {
+            return this.nextEntry != null;
+        }
+
+        protected final TableEntry<K, V> advanceEntry() {
+            final TableEntry<K, V>[] table = this.table;
+            final int tableLength = table.length;
+            int tableIndex = this.tableIndex;
+            final TableEntry<K, V> curr = this.nextEntry;
+            if (curr == null) {
+                return null;
+            }
+
+            this.currEntry = curr;
+
+            // set up nextEntry
+
+            // find next in chain
+            TableEntry<K, V> next = curr.getNextOpaque();
+
+            if (next != null) {
+                this.nextEntry = next;
+                return curr;
+            }
+
+            // nothing in chain, so find next available bin
+            for (;tableIndex < tableLength; ++tableIndex) {
+                next = getAtIndexOpaque(table, tableIndex);
+                if (next != null) {
+                    this.nextEntry = next;
+                    this.tableIndex = tableIndex + 1;
+                    return curr;
+                }
+            }
+
+            this.nextEntry = null;
+            this.tableIndex = tableIndex;
+            return curr;
+        }
+
+        @Override
+        public void remove() {
+            final TableEntry<K, V> curr = this.currEntry;
+            if (curr == null) {
+                throw new IllegalStateException();
+            }
+
+            this.map.remove(curr.key, curr.hash);
+
+            this.currEntry = null;
+        }
+    }
+
+    protected static final class ValueIterator<K, V> extends TableEntryIterator<K, V, V> {
+
+        protected ValueIterator(final TableEntry<K, V>[] table, final SWMRHashTable<K, V> map) {
+            super(table, map);
+        }
+
+        @Override
+        public V next() {
+            final TableEntry<K, V> entry = this.advanceEntry();
+
+            if (entry == null) {
+                throw new NoSuchElementException();
+            }
+
+            return entry.getValueAcquire();
+        }
+    }
+
+    protected static final class KeyIterator<K, V> extends TableEntryIterator<K, V, K> {
+
+        protected KeyIterator(final TableEntry<K, V>[] table, final SWMRHashTable<K, V> map) {
+            super(table, map);
+        }
+
+        @Override
+        public K next() {
+            final TableEntry<K, V> curr = this.advanceEntry();
+
+            if (curr == null) {
+                throw new NoSuchElementException();
+            }
+
+            return curr.key;
+        }
+    }
+
+    protected static final class EntryIterator<K, V> extends TableEntryIterator<K, V, Map.Entry<K, V>> {
+
+        protected EntryIterator(final TableEntry<K, V>[] table, final SWMRHashTable<K, V> map) {
+            super(table, map);
+        }
+
+        @Override
+        public Map.Entry<K, V> next() {
+            final TableEntry<K, V> curr = this.advanceEntry();
+
+            if (curr == null) {
+                throw new NoSuchElementException();
+            }
+
+            return curr;
+        }
+    }
+
+    protected static abstract class ViewCollection<K, V, T> implements Collection<T> {
+
+        protected final SWMRHashTable<K, V> map;
+
+        protected ViewCollection(final SWMRHashTable<K, V> map) {
+            this.map = map;
+        }
+
+        @Override
+        public boolean add(final T element) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public boolean addAll(final Collection<? extends T> collections) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public boolean removeAll(final Collection<?> collection) {
+            Validate.notNull(collection, "Null collection");
+
+            boolean modified = false;
+            for (final Object element : collection) {
+                modified |= this.remove(element);
+            }
+            return modified;
+        }
+
+        @Override
+        public int size() {
+            return this.map.size();
+        }
+
+        @Override
+        public boolean isEmpty() {
+            return this.size() == 0;
+        }
+
+        @Override
+        public void clear() {
+            this.map.clear();
+        }
+
+        @Override
+        public boolean containsAll(final Collection<?> collection) {
+            Validate.notNull(collection, "Null collection");
+
+            for (final Object element : collection) {
+                if (!this.contains(element)) {
+                    return false;
+                }
+            }
+
+            return true;
+        }
+
+        @Override
+        public Object[] toArray() {
+            final List<T> list = new ArrayList<>(this.size());
+
+            this.forEach(list::add);
+
+            return list.toArray();
+        }
+
+        @Override
+        public <E> E[] toArray(final E[] array) {
+            final List<T> list = new ArrayList<>(this.size());
+
+            this.forEach(list::add);
+
+            return list.toArray(array);
+        }
+
+        @Override
+        public <E> E[] toArray(final IntFunction<E[]> generator) {
+            final List<T> list = new ArrayList<>(this.size());
+
+            this.forEach(list::add);
+
+            return list.toArray(generator);
+        }
+
+        @Override
+        public int hashCode() {
+            int hash = 0;
+            for (final T element : this) {
+                hash += element == null ? 0 : element.hashCode();
+            }
+            return hash;
+        }
+
+        @Override
+        public Spliterator<T> spliterator() { // TODO implement
+            return Spliterators.spliterator(this, Spliterator.NONNULL);
+        }
+    }
+
+    protected static abstract class ViewSet<K, V, T> extends ViewCollection<K, V, T> implements Set<T> {
+
+        protected ViewSet(final SWMRHashTable<K, V> map) {
+            super(map);
+        }
+
+        @Override
+        public boolean equals(final Object obj) {
+            if (this == obj) {
+                return true;
+            }
+
+            if (!(obj instanceof Set)) {
+                return false;
+            }
+
+            final Set<?> other = (Set<?>)obj;
+            if (other.size() != this.size()) {
+                return false;
+            }
+
+            return this.containsAll(other);
+        }
+    }
+
+    protected static final class EntrySet<K, V> extends ViewSet<K, V, Map.Entry<K, V>> implements Set<Map.Entry<K, V>> {
+
+        protected EntrySet(final SWMRHashTable<K, V> map) {
+            super(map);
+        }
+
+        @Override
+        public boolean remove(final Object object) {
+            if (!(object instanceof Map.Entry<?, ?> entry)) {
+                return false;
+            }
+
+            final Object key;
+            final Object value;
+
+            try {
+                key = entry.getKey();
+                value = entry.getValue();
+            } catch (final IllegalStateException ex) {
+                return false;
+            }
+
+            return this.map.remove(key, value);
+        }
+
+        @Override
+        public boolean removeIf(final Predicate<? super Map.Entry<K, V>> filter) {
+            Validate.notNull(filter, "Null filter");
+
+            return this.map.removeEntryIf(filter) != 0;
+        }
+
+        @Override
+        public boolean retainAll(final Collection<?> collection) {
+            Validate.notNull(collection, "Null collection");
+
+            return this.map.removeEntryIf((final Map.Entry<K, V> entry) -> {
+                return !collection.contains(entry);
+            }) != 0;
+        }
+
+        @Override
+        public Iterator<Map.Entry<K, V>> iterator() {
+            return new EntryIterator<>(this.map.getTableAcquire(), this.map);
+        }
+
+        @Override
+        public void forEach(final Consumer<? super Map.Entry<K, V>> action) {
+            this.map.forEach(action);
+        }
+
+        @Override
+        public boolean contains(final Object object) {
+            if (!(object instanceof Map.Entry<?, ?> entry)) {
+                return false;
+            }
+
+            final Object key;
+            final Object value;
+
+            try {
+                key = entry.getKey();
+                value = entry.getValue();
+            } catch (final IllegalStateException ex) {
+                return false;
+            }
+
+            return this.map.contains(key, value);
+        }
+
+        @Override
+        public String toString() {
+            return CollectionUtil.toString(this, "SWMRHashTableEntrySet");
+        }
+    }
+
+    protected static final class KeySet<K, V> extends ViewSet<K, V, K> {
+
+        protected KeySet(final SWMRHashTable<K, V> map) {
+            super(map);
+        }
+
+        @Override
+        public Iterator<K> iterator() {
+            return new KeyIterator<>(this.map.getTableAcquire(), this.map);
+        }
+
+        @Override
+        public void forEach(final Consumer<? super K> action) {
+            Validate.notNull(action, "Null action");
+
+            this.map.forEachKey(action);
+        }
+
+        @Override
+        public boolean contains(final Object key) {
+            Validate.notNull(key, "Null key");
+
+            return this.map.containsKey(key);
+        }
+
+        @Override
+        public boolean remove(final Object key) {
+            Validate.notNull(key, "Null key");
+
+            return this.map.remove(key) != null;
+        }
+
+        @Override
+        public boolean retainAll(final Collection<?> collection) {
+            Validate.notNull(collection, "Null collection");
+
+            return this.map.removeIf((final K key, final V value) -> {
+                return !collection.contains(key);
+            }) != 0;
+        }
+
+        @Override
+        public boolean removeIf(final Predicate<? super K> filter) {
+            Validate.notNull(filter, "Null filter");
+
+            return this.map.removeIf((final K key, final V value) -> {
+                return filter.test(key);
+            }) != 0;
+        }
+
+        @Override
+        public String toString() {
+            return CollectionUtil.toString(this, "SWMRHashTableKeySet");
+        }
+    }
+
+    protected static final class ValueCollection<K, V> extends ViewSet<K, V, V> implements Collection<V> {
+
+        protected ValueCollection(final SWMRHashTable<K, V> map) {
+            super(map);
+        }
+
+        @Override
+        public Iterator<V> iterator() {
+            return new ValueIterator<>(this.map.getTableAcquire(), this.map);
+        }
+
+        @Override
+        public void forEach(final Consumer<? super V> action) {
+            Validate.notNull(action, "Null action");
+
+            this.map.forEachValue(action);
+        }
+
+        @Override
+        public boolean contains(final Object object) {
+            Validate.notNull(object, "Null object");
+
+            return this.map.containsValue(object);
+        }
+
+        @Override
+        public boolean remove(final Object object) {
+            Validate.notNull(object, "Null object");
+
+            final Iterator<V> itr = this.iterator();
+            while (itr.hasNext()) {
+                final V val = itr.next();
+                if (val == object || val.equals(object)) {
+                    itr.remove();
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
+        @Override
+        public boolean removeIf(final Predicate<? super V> filter) {
+            Validate.notNull(filter, "Null filter");
+
+            return this.map.removeIf((final K key, final V value) -> {
+                return filter.test(value);
+            }) != 0;
+        }
+
+        @Override
+        public boolean retainAll(final Collection<?> collection) {
+            Validate.notNull(collection, "Null collection");
+
+            return this.map.removeIf((final K key, final V value) -> {
+                return !collection.contains(value);
+            }) != 0;
+        }
+
+        @Override
+        public String toString() {
+            return CollectionUtil.toString(this, "SWMRHashTableValues");
+        }
+    }
+}
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRLong2ObjectHashTable.java b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRLong2ObjectHashTable.java
new file mode 100644
index 0000000000000000000000000000000000000000..bb301a9f4e3ac919552eef68afc73569d50674db
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRLong2ObjectHashTable.java
@@ -0,0 +1,674 @@
+package ca.spottedleaf.concurrentutil.map;
+
+import ca.spottedleaf.concurrentutil.function.BiLongObjectConsumer;
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
+import ca.spottedleaf.concurrentutil.util.HashUtil;
+import ca.spottedleaf.concurrentutil.util.IntegerUtil;
+import ca.spottedleaf.concurrentutil.util.Validate;
+import java.lang.invoke.VarHandle;
+import java.util.Arrays;
+import java.util.function.Consumer;
+import java.util.function.LongConsumer;
+
+// trimmed down version of SWMRHashTable
+public class SWMRLong2ObjectHashTable<V> {
+
+    protected int size;
+
+    protected TableEntry<V>[] table;
+
+    protected final float loadFactor;
+
+    protected static final VarHandle SIZE_HANDLE = ConcurrentUtil.getVarHandle(SWMRLong2ObjectHashTable.class, "size", int.class);
+    protected static final VarHandle TABLE_HANDLE = ConcurrentUtil.getVarHandle(SWMRLong2ObjectHashTable.class, "table", TableEntry[].class);
+
+    /* size */
+
+    protected final int getSizePlain() {
+        return (int)SIZE_HANDLE.get(this);
+    }
+
+    protected final int getSizeOpaque() {
+        return (int)SIZE_HANDLE.getOpaque(this);
+    }
+
+    protected final int getSizeAcquire() {
+        return (int)SIZE_HANDLE.getAcquire(this);
+    }
+
+    protected final void setSizePlain(final int value) {
+        SIZE_HANDLE.set(this, value);
+    }
+
+    protected final void setSizeOpaque(final int value) {
+        SIZE_HANDLE.setOpaque(this, value);
+    }
+
+    protected final void setSizeRelease(final int value) {
+        SIZE_HANDLE.setRelease(this, value);
+    }
+
+    /* table */
+
+    protected final TableEntry<V>[] getTablePlain() {
+        //noinspection unchecked
+        return (TableEntry<V>[])TABLE_HANDLE.get(this);
+    }
+
+    protected final TableEntry<V>[] getTableAcquire() {
+        //noinspection unchecked
+        return (TableEntry<V>[])TABLE_HANDLE.getAcquire(this);
+    }
+
+    protected final void setTablePlain(final TableEntry<V>[] table) {
+        TABLE_HANDLE.set(this, table);
+    }
+
+    protected final void setTableRelease(final TableEntry<V>[] table) {
+        TABLE_HANDLE.setRelease(this, table);
+    }
+
+    protected static final int DEFAULT_CAPACITY = 16;
+    protected static final float DEFAULT_LOAD_FACTOR = 0.75f;
+    protected static final int MAXIMUM_CAPACITY = Integer.MIN_VALUE >>> 1;
+
+    /**
+     * Constructs this map with a capacity of {@code 16} and load factor of {@code 0.75f}.
+     */
+    public SWMRLong2ObjectHashTable() {
+        this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR);
+    }
+
+    /**
+     * Constructs this map with the specified capacity and load factor of {@code 0.75f}.
+     * @param capacity specified initial capacity, > 0
+     */
+    public SWMRLong2ObjectHashTable(final int capacity) {
+        this(capacity, DEFAULT_LOAD_FACTOR);
+    }
+
+    /**
+     * Constructs this map with the specified capacity and load factor.
+     * @param capacity specified capacity, > 0
+     * @param loadFactor specified load factor, > 0 && finite
+     */
+    public SWMRLong2ObjectHashTable(final int capacity, final float loadFactor) {
+        final int tableSize = getCapacityFor(capacity);
+
+        if (loadFactor <= 0.0 || !Float.isFinite(loadFactor)) {
+            throw new IllegalArgumentException("Invalid load factor: " + loadFactor);
+        }
+
+        //noinspection unchecked
+        final TableEntry<V>[] table = new TableEntry[tableSize];
+        this.setTablePlain(table);
+
+        if (tableSize == MAXIMUM_CAPACITY) {
+            this.threshold = -1;
+        } else {
+            this.threshold = getTargetCapacity(tableSize, loadFactor);
+        }
+
+        this.loadFactor = loadFactor;
+    }
+
+    /**
+     * Constructs this map with a capacity of {@code 16} or the specified map's size, whichever is larger, and
+     * with a load factor of {@code 0.75f}.
+     * All of the specified map's entries are copied into this map.
+     * @param other The specified map.
+     */
+    public SWMRLong2ObjectHashTable(final SWMRLong2ObjectHashTable<V> other) {
+        this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR, other);
+    }
+
+    /**
+     * Constructs this map with a minimum capacity of the specified capacity or the specified map's size, whichever is larger, and
+     * with a load factor of {@code 0.75f}.
+     * All of the specified map's entries are copied into this map.
+     * @param capacity specified capacity, > 0
+     * @param other The specified map.
+     */
+    public SWMRLong2ObjectHashTable(final int capacity, final SWMRLong2ObjectHashTable<V> other) {
+        this(capacity, DEFAULT_LOAD_FACTOR, other);
+    }
+
+    /**
+     * Constructs this map with a min capacity of the specified capacity or the specified map's size, whichever is larger, and
+     * with the specified load factor.
+     * All of the specified map's entries are copied into this map.
+     * @param capacity specified capacity, > 0
+     * @param loadFactor specified load factor, > 0 && finite
+     * @param other The specified map.
+     */
+    public SWMRLong2ObjectHashTable(final int capacity, final float loadFactor, final SWMRLong2ObjectHashTable<V> other) {
+        this(Math.max(Validate.notNull(other, "Null map").size(), capacity), loadFactor);
+        this.putAll(other);
+    }
+
+    protected static <V> TableEntry<V> getAtIndexOpaque(final TableEntry<V>[] table, final int index) {
+        // noinspection unchecked
+        return (TableEntry<V>)TableEntry.TABLE_ENTRY_ARRAY_HANDLE.getOpaque(table, index);
+    }
+
+    protected static <V> void setAtIndexRelease(final TableEntry<V>[] table, final int index, final TableEntry<V> value) {
+        TableEntry.TABLE_ENTRY_ARRAY_HANDLE.setRelease(table, index, value);
+    }
+
+    public final float getLoadFactor() {
+        return this.loadFactor;
+    }
+
+    protected static int getCapacityFor(final int capacity) {
+        if (capacity <= 0) {
+            throw new IllegalArgumentException("Invalid capacity: " + capacity);
+        }
+        if (capacity >= MAXIMUM_CAPACITY) {
+            return MAXIMUM_CAPACITY;
+        }
+        return IntegerUtil.roundCeilLog2(capacity);
+    }
+
+    /** Callers must still use acquire when reading the value of the entry. */
+    protected final TableEntry<V> getEntryForOpaque(final long key) {
+        final int hash = SWMRLong2ObjectHashTable.getHash(key);
+        final TableEntry<V>[] table = this.getTableAcquire();
+
+        for (TableEntry<V> curr = getAtIndexOpaque(table, hash & (table.length - 1)); curr != null; curr = curr.getNextOpaque()) {
+            if (key == curr.key) {
+                return curr;
+            }
+        }
+
+        return null;
+    }
+
+    protected final TableEntry<V> getEntryForPlain(final long key) {
+        final int hash = SWMRLong2ObjectHashTable.getHash(key);
+        final TableEntry<V>[] table = this.getTablePlain();
+
+        for (TableEntry<V> curr = table[hash & (table.length - 1)]; curr != null; curr = curr.getNextPlain()) {
+            if (key == curr.key) {
+                return curr;
+            }
+        }
+
+        return null;
+    }
+
+    /* MT-Safe */
+
+    /** must be deterministic given a key */
+    protected static int getHash(final long key) {
+        return (int)HashUtil.mix(key);
+    }
+
+    // rets -1 if capacity*loadFactor is too large
+    protected static int getTargetCapacity(final int capacity, final float loadFactor) {
+        final double ret = (double)capacity * (double)loadFactor;
+        if (Double.isInfinite(ret) || ret >= ((double)Integer.MAX_VALUE)) {
+            return -1;
+        }
+
+        return (int)ret;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        /* Make no attempt to deal with concurrent modifications */
+        if (!(obj instanceof SWMRLong2ObjectHashTable<?> other)) {
+            return false;
+        }
+
+        if (this.size() != other.size()) {
+            return false;
+        }
+
+        final TableEntry<V>[] table = this.getTableAcquire();
+
+        for (int i = 0, len = table.length; i < len; ++i) {
+            for (TableEntry<V> curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
+                final V value = curr.getValueAcquire();
+
+                final Object otherValue = other.get(curr.key);
+                if (otherValue == null || (value != otherValue && value.equals(otherValue))) {
+                    return false;
+                }
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int hashCode() {
+        /* Make no attempt to deal with concurrent modifications */
+        int hash = 0;
+        final TableEntry<V>[] table = this.getTableAcquire();
+
+        for (int i = 0, len = table.length; i < len; ++i) {
+            for (TableEntry<V> curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
+                hash += curr.hashCode();
+            }
+        }
+
+        return hash;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder(64);
+        builder.append("SingleWriterMultiReaderHashMap:{");
+
+        this.forEach((final long key, final V value) -> {
+            builder.append("{key: \"").append(key).append("\", value: \"").append(value).append("\"}");
+        });
+
+        return builder.append('}').toString();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public SWMRLong2ObjectHashTable<V> clone() {
+        return new SWMRLong2ObjectHashTable<>(this.getTableAcquire().length, this.loadFactor, this);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void forEach(final Consumer<? super TableEntry<V>> action) {
+        Validate.notNull(action, "Null action");
+
+        final TableEntry<V>[] table = this.getTableAcquire();
+        for (int i = 0, len = table.length; i < len; ++i) {
+            for (TableEntry<V> curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
+                action.accept(curr);
+            }
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void forEach(final BiLongObjectConsumer<? super V> action) {
+        Validate.notNull(action, "Null action");
+
+        final TableEntry<V>[] table = this.getTableAcquire();
+        for (int i = 0, len = table.length; i < len; ++i) {
+            for (TableEntry<V> curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
+                final V value = curr.getValueAcquire();
+
+                action.accept(curr.key, value);
+            }
+        }
+    }
+
+    /**
+     * Provides the specified consumer with all keys contained within this map.
+     * @param action The specified consumer.
+     */
+    public void forEachKey(final LongConsumer action) {
+        Validate.notNull(action, "Null action");
+
+        final TableEntry<V>[] table = this.getTableAcquire();
+        for (int i = 0, len = table.length; i < len; ++i) {
+            for (TableEntry<V> curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
+                action.accept(curr.key);
+            }
+        }
+    }
+
+    /**
+     * Provides the specified consumer with all values contained within this map. Equivalent to {@code map.values().forEach(Consumer)}.
+     * @param action The specified consumer.
+     */
+    public void forEachValue(final Consumer<? super V> action) {
+        Validate.notNull(action, "Null action");
+
+        final TableEntry<V>[] table = this.getTableAcquire();
+        for (int i = 0, len = table.length; i < len; ++i) {
+            for (TableEntry<V> curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
+                final V value = curr.getValueAcquire();
+
+                action.accept(value);
+            }
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public V get(final long key) {
+        final TableEntry<V> entry = this.getEntryForOpaque(key);
+        return entry == null ? null : entry.getValueAcquire();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public boolean containsKey(final long key) {
+        // note: we need to use getValueAcquire, so that the reads from this map are ordered by acquire semantics
+        return this.get(key) != null;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public V getOrDefault(final long key, final V defaultValue) {
+        final TableEntry<V> entry = this.getEntryForOpaque(key);
+
+        return entry == null ? defaultValue : entry.getValueAcquire();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public int size() {
+        return this.getSizeAcquire();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public boolean isEmpty() {
+        return this.getSizeAcquire() == 0;
+    }
+
+    /* Non-MT-Safe */
+
+    protected int threshold;
+
+    protected final void checkResize(final int minCapacity) {
+        if (minCapacity <= this.threshold || this.threshold < 0) {
+            return;
+        }
+
+        final TableEntry<V>[] table = this.getTablePlain();
+        int newCapacity = minCapacity >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY : IntegerUtil.roundCeilLog2(minCapacity);
+        if (newCapacity < 0) {
+            newCapacity = MAXIMUM_CAPACITY;
+        }
+        if (newCapacity <= table.length) {
+            if (newCapacity == MAXIMUM_CAPACITY) {
+                return;
+            }
+            newCapacity = table.length << 1;
+        }
+
+        //noinspection unchecked
+        final TableEntry<V>[] newTable = new TableEntry[newCapacity];
+        final int indexMask = newCapacity - 1;
+
+        for (int i = 0, len = table.length; i < len; ++i) {
+            for (TableEntry<V> entry = table[i]; entry != null; entry = entry.getNextPlain()) {
+                final long key = entry.key;
+                final int hash = SWMRLong2ObjectHashTable.getHash(key);
+                final int index = hash & indexMask;
+
+                /* we need to create a new entry since there could be reading threads */
+                final TableEntry<V> insert = new TableEntry<>(key, entry.getValuePlain());
+
+                final TableEntry<V> prev = newTable[index];
+
+                newTable[index] = insert;
+                insert.setNextPlain(prev);
+            }
+        }
+
+        if (newCapacity == MAXIMUM_CAPACITY) {
+            this.threshold = -1; /* No more resizing */
+        } else {
+            this.threshold = getTargetCapacity(newCapacity, this.loadFactor);
+        }
+        this.setTableRelease(newTable); /* use release to publish entries in table */
+    }
+
+    protected final int addToSize(final int num) {
+        final int newSize = this.getSizePlain() + num;
+
+        this.setSizeOpaque(newSize);
+        this.checkResize(newSize);
+
+        return newSize;
+    }
+
+    protected final int removeFromSize(final int num) {
+        final int newSize = this.getSizePlain() - num;
+
+        this.setSizeOpaque(newSize);
+
+        return newSize;
+    }
+
+    protected final V put(final long key, final V value, final boolean onlyIfAbsent) {
+        final TableEntry<V>[] table = this.getTablePlain();
+        final int hash = SWMRLong2ObjectHashTable.getHash(key);
+        final int index = hash & (table.length - 1);
+
+        final TableEntry<V> head = table[index];
+        if (head == null) {
+            final TableEntry<V> insert = new TableEntry<>(key, value);
+            setAtIndexRelease(table, index, insert);
+            this.addToSize(1);
+            return null;
+        }
+
+        for (TableEntry<V> curr = head;;) {
+            if (key == curr.key) {
+                if (onlyIfAbsent) {
+                    return curr.getValuePlain();
+                }
+
+                final V currVal = curr.getValuePlain();
+                curr.setValueRelease(value);
+                return currVal;
+            }
+
+            final TableEntry<V> next = curr.getNextPlain();
+            if (next != null) {
+                curr = next;
+                continue;
+            }
+
+            final TableEntry<V> insert = new TableEntry<>(key, value);
+
+            curr.setNextRelease(insert);
+            this.addToSize(1);
+            return null;
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public V put(final long key, final V value) {
+        Validate.notNull(value, "Null value");
+
+        return this.put(key, value, false);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public V putIfAbsent(final long key, final V value) {
+        Validate.notNull(value, "Null value");
+
+        return this.put(key, value, true);
+    }
+
+    protected final V remove(final long key, final int hash) {
+        final TableEntry<V>[] table = this.getTablePlain();
+        final int index = (table.length - 1) & hash;
+
+        final TableEntry<V> head = table[index];
+        if (head == null) {
+            return null;
+        }
+
+        if (head.key == key) {
+            setAtIndexRelease(table, index, head.getNextPlain());
+            this.removeFromSize(1);
+
+            return head.getValuePlain();
+        }
+
+        for (TableEntry<V> curr = head.getNextPlain(), prev = head; curr != null; prev = curr, curr = curr.getNextPlain()) {
+            if (key == curr.key) {
+                prev.setNextRelease(curr.getNextPlain());
+                this.removeFromSize(1);
+
+                return curr.getValuePlain();
+            }
+        }
+
+        return null;
+    }
+
+    protected final V remove(final long key, final int hash, final V expect) {
+        final TableEntry<V>[] table = this.getTablePlain();
+        final int index = (table.length - 1) & hash;
+
+        final TableEntry<V> head = table[index];
+        if (head == null) {
+            return null;
+        }
+
+        if (head.key == key) {
+            final V val = head.value;
+            if (val == expect || val.equals(expect)) {
+                setAtIndexRelease(table, index, head.getNextPlain());
+                this.removeFromSize(1);
+
+                return head.getValuePlain();
+            } else {
+                return null;
+            }
+        }
+
+        for (TableEntry<V> curr = head.getNextPlain(), prev = head; curr != null; prev = curr, curr = curr.getNextPlain()) {
+            if (key == curr.key) {
+                final V val = curr.value;
+                if (val == expect || val.equals(expect)) {
+                    prev.setNextRelease(curr.getNextPlain());
+                    this.removeFromSize(1);
+
+                    return curr.getValuePlain();
+                } else {
+                    return null;
+                }
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public V remove(final long key) {
+        return this.remove(key, SWMRLong2ObjectHashTable.getHash(key));
+    }
+
+    public boolean remove(final long key, final V expect) {
+        return this.remove(key, SWMRLong2ObjectHashTable.getHash(key), expect) != null;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void putAll(final SWMRLong2ObjectHashTable<? extends V> map) {
+        Validate.notNull(map, "Null map");
+
+        final int size = map.size();
+        this.checkResize(Math.max(this.getSizePlain() + size/2, size)); /* preemptively resize */
+        map.forEach(this::put);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * This call is non-atomic and the order that which entries are removed is undefined. The clear operation itself
+     * is release ordered, that is, after the clear operation is performed a release fence is performed.
+     * </p>
+     */
+    public void clear() {
+        Arrays.fill(this.getTablePlain(), null);
+        this.setSizeRelease(0);
+    }
+
+    public static final class TableEntry<V> {
+
+        protected static final VarHandle TABLE_ENTRY_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(TableEntry[].class);
+
+        protected final long key;
+        protected V value;
+
+        protected TableEntry<V> next;
+
+        protected static final VarHandle VALUE_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "value", Object.class);
+        protected static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "next", TableEntry.class);
+
+        /* value */
+
+        protected final V getValuePlain() {
+            //noinspection unchecked
+            return (V)VALUE_HANDLE.get(this);
+        }
+
+        protected final V getValueAcquire() {
+            //noinspection unchecked
+            return (V)VALUE_HANDLE.getAcquire(this);
+        }
+
+        protected final void setValueRelease(final V to) {
+            VALUE_HANDLE.setRelease(this, to);
+        }
+
+        /* next */
+
+        protected final TableEntry<V> getNextPlain() {
+            //noinspection unchecked
+            return (TableEntry<V>)NEXT_HANDLE.get(this);
+        }
+
+        protected final TableEntry<V> getNextOpaque() {
+            //noinspection unchecked
+            return (TableEntry<V>)NEXT_HANDLE.getOpaque(this);
+        }
+
+        protected final void setNextPlain(final TableEntry<V> next) {
+            NEXT_HANDLE.set(this, next);
+        }
+
+        protected final void setNextRelease(final TableEntry<V> next) {
+            NEXT_HANDLE.setRelease(this, next);
+        }
+
+        protected TableEntry(final long key, final V value) {
+            this.key = key;
+            this.value = value;
+        }
+
+        public long getKey() {
+            return this.key;
+        }
+
+        public V getValue() {
+            return this.getValueAcquire();
+        }
+    }
+}
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/scheduler/SchedulerThreadPool.java b/src/main/java/ca/spottedleaf/concurrentutil/scheduler/SchedulerThreadPool.java
new file mode 100644
index 0000000000000000000000000000000000000000..8197ccb1c4e5878dbd8007b5fb514640765ec8e4
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/concurrentutil/scheduler/SchedulerThreadPool.java
@@ -0,0 +1,558 @@
+package ca.spottedleaf.concurrentutil.scheduler;
+
+import ca.spottedleaf.concurrentutil.set.LinkedSortedSet;
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
+import ca.spottedleaf.concurrentutil.util.TimeUtil;
+import java.lang.invoke.VarHandle;
+import java.util.BitSet;
+import java.util.Comparator;
+import java.util.PriorityQueue;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.LockSupport;
+import java.util.function.BooleanSupplier;
+
+public class SchedulerThreadPool {
+
+    public static final long DEADLINE_NOT_SET = Long.MIN_VALUE;
+
+    private static final Comparator<SchedulableTick> TICK_COMPARATOR_BY_TIME = (final SchedulableTick t1, final SchedulableTick t2) -> {
+        final int timeCompare = TimeUtil.compareTimes(t1.scheduledStart, t2.scheduledStart);
+        if (timeCompare != 0) {
+            return timeCompare;
+        }
+
+        return Long.compare(t1.id, t2.id);
+    };
+
+    private final TickThreadRunner[] runners;
+    private final Thread[] threads;
+    private final LinkedSortedSet<SchedulableTick> awaiting = new LinkedSortedSet<>(TICK_COMPARATOR_BY_TIME);
+    private final PriorityQueue<SchedulableTick> queued = new PriorityQueue<>(TICK_COMPARATOR_BY_TIME);
+    private final BitSet idleThreads;
+
+    private final Object scheduleLock = new Object();
+
+    private volatile boolean halted;
+
+    /**
+     * Creates, but does not start, a scheduler thread pool with the specified number of threads
+     * created using the specified thread factory.
+     * @param threads Specified number of threads
+     * @param threadFactory Specified thread factory
+     * @see #start()
+     */
+    public SchedulerThreadPool(final int threads, final ThreadFactory threadFactory) {
+        final BitSet idleThreads = new BitSet(threads);
+        for (int i = 0; i < threads; ++i) {
+            idleThreads.set(i);
+        }
+        this.idleThreads = idleThreads;
+
+        final TickThreadRunner[] runners = new TickThreadRunner[threads];
+        final Thread[] t = new Thread[threads];
+        for (int i = 0; i < threads; ++i) {
+            runners[i] = new TickThreadRunner(i, this);
+            t[i] = threadFactory.newThread(runners[i]);
+        }
+
+        this.threads = t;
+        this.runners = runners;
+    }
+
+    /**
+     * Starts the threads in this pool.
+     */
+    public void start() {
+        for (final Thread thread : this.threads) {
+            thread.start();
+        }
+    }
+
+    /**
+     * Attempts to prevent further execution of tasks, optionally waiting for the scheduler threads to die.
+     *
+     * @param sync Whether to wait for the scheduler threads to die.
+     * @param maxWaitNS The maximum time, in ns, to wait for the scheduler threads to die.
+     * @return {@code true} if sync was false, or if sync was true and the scheduler threads died before the timeout.
+     *          Otherwise, returns {@code false} if the time elapsed exceeded the maximum wait time.
+     */
+    public boolean halt(final boolean sync, final long maxWaitNS) {
+        this.halted = true;
+        for (final Thread thread : this.threads) {
+            // force response to halt
+            LockSupport.unpark(thread);
+        }
+        final long time = System.nanoTime();
+        if (sync) {
+            // start at 10 * 0.5ms -> 5ms
+            for (long failures = 9L;; failures = ConcurrentUtil.linearLongBackoff(failures, 500_000L, 50_000_000L)) {
+                boolean allDead = true;
+                for (final Thread thread : this.threads) {
+                    if (thread.isAlive()) {
+                        allDead = false;
+                        break;
+                    }
+                }
+                if (allDead) {
+                    return true;
+                }
+                if ((System.nanoTime() - time) >= maxWaitNS) {
+                    return false;
+                }
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns an array of the underlying scheduling threads.
+     */
+    public Thread[] getThreads() {
+        return this.threads.clone();
+    }
+
+    private void insertFresh(final SchedulableTick task) {
+        final TickThreadRunner[] runners = this.runners;
+
+        final int firstIdleThread = this.idleThreads.nextSetBit(0);
+
+        if (firstIdleThread != -1) {
+            // push to idle thread
+            this.idleThreads.clear(firstIdleThread);
+            final TickThreadRunner runner = runners[firstIdleThread];
+            task.awaitingLink = this.awaiting.addLast(task);
+            runner.acceptTask(task);
+            return;
+        }
+
+        // try to replace the last awaiting task
+        final SchedulableTick last = this.awaiting.last();
+
+        if (last != null && TICK_COMPARATOR_BY_TIME.compare(task, last) < 0) {
+            // need to replace the last task
+            this.awaiting.pollLast();
+            last.awaitingLink = null;
+            task.awaitingLink = this.awaiting.addLast(task);
+            // need to add task to queue to be picked up later
+            this.queued.add(last);
+
+            final TickThreadRunner runner = last.ownedBy;
+            runner.replaceTask(task);
+
+            return;
+        }
+
+        // add to queue, will be picked up later
+        this.queued.add(task);
+    }
+
+    private void takeTask(final TickThreadRunner runner, final SchedulableTick tick) {
+        if (!this.awaiting.remove(tick.awaitingLink)) {
+            throw new IllegalStateException("Task is not in awaiting");
+        }
+        tick.awaitingLink = null;
+    }
+
+    private SchedulableTick returnTask(final TickThreadRunner runner, final SchedulableTick reschedule) {
+        if (reschedule != null) {
+            this.queued.add(reschedule);
+        }
+        final SchedulableTick ret = this.queued.poll();
+        if (ret == null) {
+            this.idleThreads.set(runner.id);
+        } else {
+            ret.awaitingLink = this.awaiting.addLast(ret);
+        }
+
+        return ret;
+    }
+
+    /**
+     * Schedules the specified task to be executed on this thread pool.
+     * @param task Specified task
+     * @throws IllegalStateException If the task is already scheduled
+     * @see SchedulableTick
+     */
+    public void schedule(final SchedulableTick task) {
+        synchronized (this.scheduleLock) {
+            if (!task.tryMarkScheduled()) {
+                throw new IllegalStateException("Task " + task + " is already scheduled or cancelled");
+            }
+
+            task.schedulerOwnedBy = this;
+
+            this.insertFresh(task);
+        }
+    }
+
+    /**
+     * Updates the tasks scheduled start to the maximum of its current scheduled start and the specified
+     * new start. If the task is not scheduled, returns {@code false}. Otherwise, returns whether the
+     * scheduled start was updated. Undefined behavior of the specified task is scheduled in another executor.
+     * @param task Specified task
+     * @param newStart Specified new start
+     */
+    public boolean updateTickStartToMax(final SchedulableTick task, final long newStart) {
+        synchronized (this.scheduleLock) {
+            if (TimeUtil.compareTimes(newStart, task.getScheduledStart()) <= 0) {
+                return false;
+            }
+            if (this.queued.remove(task)) {
+                task.setScheduledStart(newStart);
+                this.queued.add(task);
+                return true;
+            }
+            if (task.awaitingLink != null) {
+                this.awaiting.remove(task.awaitingLink);
+                task.awaitingLink = null;
+
+                // re-queue task
+                task.setScheduledStart(newStart);
+                this.queued.add(task);
+
+                // now we need to replace the task the runner was waiting for
+                final TickThreadRunner runner = task.ownedBy;
+                final SchedulableTick replace = this.queued.poll();
+
+                // replace cannot be null, since we have added a task to queued
+                if (replace != task) {
+                    runner.replaceTask(replace);
+                }
+
+                return true;
+            }
+
+            return false;
+        }
+    }
+
+    /**
+     * Returns {@code null} if the task is not scheduled, returns {@code TRUE} if the task was cancelled
+     * and was queued to execute, returns {@code FALSE} if the task was cancelled but was executing.
+     */
+    public Boolean tryRetire(final SchedulableTick task) {
+        if (task.schedulerOwnedBy != this) {
+            return null;
+        }
+
+        synchronized (this.scheduleLock) {
+            if (this.queued.remove(task)) {
+                // cancelled, and no runner owns it - so return
+                return Boolean.TRUE;
+            }
+            if (task.awaitingLink != null) {
+                this.awaiting.remove(task.awaitingLink);
+                task.awaitingLink = null;
+                // here we need to replace the task the runner was waiting for
+                final TickThreadRunner runner = task.ownedBy;
+                final SchedulableTick replace = this.queued.poll();
+
+                if (replace == null) {
+                    // nothing to replace with, set to idle
+                    this.idleThreads.set(runner.id);
+                    runner.forceIdle();
+                } else {
+                    runner.replaceTask(replace);
+                }
+
+                return Boolean.TRUE;
+            }
+
+            // could not find it in queue
+            return task.tryMarkCancelled() ? Boolean.FALSE : null;
+        }
+    }
+
+    /**
+     * Indicates that intermediate tasks are available to be executed by the task.
+     * <p>
+     * Note: currently a no-op
+     * </p>
+     * @param task The specified task
+     * @see SchedulableTick
+     */
+    public void notifyTasks(final SchedulableTick task) {
+        // Not implemented
+    }
+
+    /**
+     * Represents a tickable task that can be scheduled into a {@link SchedulerThreadPool}.
+     * <p>
+     * A tickable task is expected to run on a fixed interval, which is determined by
+     * the {@link SchedulerThreadPool}.
+     * </p>
+     * <p>
+     * A tickable task can have intermediate tasks that can be executed before its tick method is ran. Instead of
+     * the {@link SchedulerThreadPool} parking in-between ticks, the scheduler will instead drain
+     * intermediate tasks from scheduled tasks. The parsing of intermediate tasks allows the scheduler to take
+     * advantage of downtime to reduce the intermediate task load from tasks once they begin ticking.
+     * </p>
+     * <p>
+     * It is guaranteed that {@link #runTick()} and {@link #runTasks(BooleanSupplier)} are never
+     * invoked in parallel.
+     * It is required that when intermediate tasks are scheduled, that {@link SchedulerThreadPool#notifyTasks(SchedulableTick)}
+     * is invoked for any scheduled task - otherwise, {@link #runTasks(BooleanSupplier)} may not be invoked to
+     * parse intermediate tasks.
+     * </p>
+     */
+    public static abstract class SchedulableTick {
+        private static final AtomicLong ID_GENERATOR = new AtomicLong();
+        public final long id = ID_GENERATOR.getAndIncrement();
+
+        private static final int SCHEDULE_STATE_NOT_SCHEDULED = 0;
+        private static final int SCHEDULE_STATE_SCHEDULED = 1;
+        private static final int SCHEDULE_STATE_CANCELLED = 2;
+
+        private final AtomicInteger scheduled = new AtomicInteger();
+        private SchedulerThreadPool schedulerOwnedBy;
+        private long scheduledStart = DEADLINE_NOT_SET;
+        private TickThreadRunner ownedBy;
+
+        private LinkedSortedSet.Link<SchedulableTick> awaitingLink;
+
+        private boolean tryMarkScheduled() {
+            return this.scheduled.compareAndSet(SCHEDULE_STATE_NOT_SCHEDULED, SCHEDULE_STATE_SCHEDULED);
+        }
+
+        private boolean tryMarkCancelled() {
+            return this.scheduled.compareAndSet(SCHEDULE_STATE_SCHEDULED, SCHEDULE_STATE_CANCELLED);
+        }
+
+        private boolean isScheduled() {
+            return this.scheduled.get() == SCHEDULE_STATE_SCHEDULED;
+        }
+
+        protected final long getScheduledStart() {
+            return this.scheduledStart;
+        }
+
+        /**
+         * If this task is scheduled, then this may only be invoked during {@link #runTick()},
+         * and {@link #runTasks(BooleanSupplier)}
+         */
+        protected final void setScheduledStart(final long value) {
+            this.scheduledStart = value;
+        }
+
+        /**
+         * Executes the tick.
+         * <p>
+         * It is the callee's responsibility to invoke {@link #setScheduledStart(long)} to adjust the start of
+         * the next tick.
+         * </p>
+         * @return {@code true} if the task should continue to be scheduled, {@code false} otherwise.
+         */
+        public abstract boolean runTick();
+
+        /**
+         * Returns whether this task has any intermediate tasks that can be executed.
+         */
+        public abstract boolean hasTasks();
+
+        /**
+         * Returns {@code null} if this task should not be scheduled, otherwise returns
+         * {@code Boolean.TRUE} if there are more intermediate tasks to execute and
+         * {@code Boolean.FALSE} if there are no more intermediate tasks to execute.
+         */
+        public abstract Boolean runTasks(final BooleanSupplier canContinue);
+
+        @Override
+        public String toString() {
+            return "SchedulableTick:{" +
+                    "class=" + this.getClass().getName() + "," +
+                    "scheduled_state=" + this.scheduled.get() + ","
+                    + "}";
+        }
+    }
+
+    private static final class TickThreadRunner implements Runnable {
+
+        /**
+         * There are no tasks in this thread's runqueue, so it is parked.
+         * <p>
+         * stateTarget = null
+         * </p>
+         */
+        private static final int STATE_IDLE = 0;
+
+        /**
+         * The runner is waiting to tick a task, as it has no intermediate tasks to execute.
+         * <p>
+         * stateTarget = the task awaiting tick
+         * </p>
+         */
+        private static final int STATE_AWAITING_TICK = 1;
+
+        /**
+         * The runner is executing a tick for one of the tasks that was in its runqueue.
+         * <p>
+         * stateTarget = the task being ticked
+         * </p>
+         */
+        private static final int STATE_EXECUTING_TICK = 2;
+
+        public final int id;
+        public final SchedulerThreadPool scheduler;
+
+        private volatile Thread thread;
+        private volatile TickThreadRunnerState state = new TickThreadRunnerState(null, STATE_IDLE);
+        private static final VarHandle STATE_HANDLE = ConcurrentUtil.getVarHandle(TickThreadRunner.class, "state", TickThreadRunnerState.class);
+
+        private void setStatePlain(final TickThreadRunnerState state) {
+            STATE_HANDLE.set(this, state);
+        }
+
+        private void setStateOpaque(final TickThreadRunnerState state) {
+            STATE_HANDLE.setOpaque(this, state);
+        }
+
+        private void setStateVolatile(final TickThreadRunnerState state) {
+            STATE_HANDLE.setVolatile(this, state);
+        }
+
+        private static record TickThreadRunnerState(SchedulableTick stateTarget, int state) {}
+
+        public TickThreadRunner(final int id, final SchedulerThreadPool scheduler) {
+            this.id = id;
+            this.scheduler = scheduler;
+        }
+
+        private Thread getRunnerThread() {
+            return this.thread;
+        }
+
+        private void acceptTask(final SchedulableTick task) {
+            if (task.ownedBy != null) {
+                throw new IllegalStateException("Already owned by another runner");
+            }
+            task.ownedBy = this;
+            final TickThreadRunnerState state = this.state;
+            if (state.state != STATE_IDLE) {
+                throw new IllegalStateException("Cannot accept task in state " + state);
+            }
+            this.setStateVolatile(new TickThreadRunnerState(task, STATE_AWAITING_TICK));
+            LockSupport.unpark(this.getRunnerThread());
+        }
+
+        private void replaceTask(final SchedulableTick task) {
+            final TickThreadRunnerState state = this.state;
+            if (state.state != STATE_AWAITING_TICK) {
+                throw new IllegalStateException("Cannot replace task in state " + state);
+            }
+            if (task.ownedBy != null) {
+                throw new IllegalStateException("Already owned by another runner");
+            }
+            task.ownedBy = this;
+
+            state.stateTarget.ownedBy = null;
+
+            this.setStateVolatile(new TickThreadRunnerState(task, STATE_AWAITING_TICK));
+            LockSupport.unpark(this.getRunnerThread());
+        }
+
+        private void forceIdle() {
+            final TickThreadRunnerState state = this.state;
+            if (state.state != STATE_AWAITING_TICK) {
+                throw new IllegalStateException("Cannot replace task in state " + state);
+            }
+            state.stateTarget.ownedBy = null;
+            this.setStateOpaque(new TickThreadRunnerState(null, STATE_IDLE));
+            // no need to unpark
+        }
+
+        private boolean takeTask(final TickThreadRunnerState state, final SchedulableTick task) {
+            synchronized (this.scheduler.scheduleLock) {
+                if (this.state != state) {
+                    return false;
+                }
+                this.setStatePlain(new TickThreadRunnerState(task, STATE_EXECUTING_TICK));
+                this.scheduler.takeTask(this, task);
+                return true;
+            }
+        }
+
+        private void returnTask(final SchedulableTick task, final boolean reschedule) {
+            synchronized (this.scheduler.scheduleLock) {
+                task.ownedBy = null;
+
+                final SchedulableTick newWait = this.scheduler.returnTask(this, reschedule && task.isScheduled() ? task : null);
+                if (newWait == null) {
+                    this.setStatePlain(new TickThreadRunnerState(null, STATE_IDLE));
+                } else {
+                    if (newWait.ownedBy != null) {
+                        throw new IllegalStateException("Already owned by another runner");
+                    }
+                    newWait.ownedBy = this;
+                    this.setStatePlain(new TickThreadRunnerState(newWait, STATE_AWAITING_TICK));
+                }
+            }
+        }
+
+        @Override
+        public void run() {
+            this.thread = Thread.currentThread();
+
+            main_state_loop:
+            for (;;) {
+                final TickThreadRunnerState startState = this.state;
+                final int startStateType = startState.state;
+                final SchedulableTick startStateTask =  startState.stateTarget;
+
+                if (this.scheduler.halted) {
+                    return;
+                }
+
+                switch (startStateType) {
+                    case STATE_IDLE: {
+                        while (this.state.state == STATE_IDLE) {
+                            LockSupport.park();
+                            if (this.scheduler.halted) {
+                                return;
+                            }
+                        }
+                        continue main_state_loop;
+                    }
+
+                    case STATE_AWAITING_TICK: {
+                        final long deadline = startStateTask.getScheduledStart();
+                        for (;;) {
+                            if (this.state != startState) {
+                                continue main_state_loop;
+                            }
+                            final long diff = deadline - System.nanoTime();
+                            if (diff <= 0L) {
+                                break;
+                            }
+                            LockSupport.parkNanos(startState, diff);
+                            if (this.scheduler.halted) {
+                                return;
+                            }
+                        }
+
+                        if (!this.takeTask(startState, startStateTask)) {
+                            continue main_state_loop;
+                        }
+
+                        // TODO exception handling
+                        final boolean reschedule = startStateTask.runTick();
+
+                        this.returnTask(startStateTask, reschedule);
+
+                        continue main_state_loop;
+                    }
+
+                    case STATE_EXECUTING_TICK: {
+                        throw new IllegalStateException("Tick execution must be set by runner thread, not by any other thread");
+                    }
+
+                    default: {
+                        throw new IllegalStateException("Unknown state: " + startState);
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/set/LinkedSortedSet.java b/src/main/java/ca/spottedleaf/concurrentutil/set/LinkedSortedSet.java
new file mode 100644
index 0000000000000000000000000000000000000000..212bc9ae2fc7d37d4a089a2921b00de1e97f7cc1
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/concurrentutil/set/LinkedSortedSet.java
@@ -0,0 +1,272 @@
+package ca.spottedleaf.concurrentutil.set;
+
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+public final class LinkedSortedSet<E> implements Iterable<E> {
+
+    public final Comparator<? super E> comparator;
+
+    protected Link<E> head;
+    protected Link<E> tail;
+
+    public LinkedSortedSet() {
+        this((Comparator)Comparator.naturalOrder());
+    }
+
+    public LinkedSortedSet(final Comparator<? super E> comparator) {
+        this.comparator = comparator;
+    }
+
+    public void clear() {
+        this.head = this.tail = null;
+    }
+
+    public boolean isEmpty() {
+        return this.head == null;
+    }
+
+    public E first() {
+        final Link<E> head = this.head;
+        return head == null ? null : head.element;
+    }
+
+    public E last() {
+        final Link<E> tail = this.tail;
+        return tail == null ? null : tail.element;
+    }
+
+    public boolean containsFirst(final E element) {
+        final Comparator<? super E> comparator = this.comparator;
+        for (Link<E> curr = this.head; curr != null; curr = curr.next) {
+            if (comparator.compare(element, curr.element) == 0) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public boolean containsLast(final E element) {
+        final Comparator<? super E> comparator = this.comparator;
+        for (Link<E> curr = this.tail; curr != null; curr = curr.prev) {
+            if (comparator.compare(element, curr.element) == 0) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private void removeNode(final Link<E> node) {
+        final Link<E> prev = node.prev;
+        final Link<E> next = node.next;
+
+        // help GC
+        node.element = null;
+        node.prev = null;
+        node.next = null;
+
+        if (prev == null) {
+            this.head = next;
+        } else {
+            prev.next = next;
+        }
+
+        if (next == null) {
+            this.tail = prev;
+        } else {
+            next.prev = prev;
+        }
+    }
+
+    public boolean remove(final Link<E> link) {
+        if (link.element == null) {
+            return false;
+        }
+
+        this.removeNode(link);
+        return true;
+    }
+
+    public boolean removeFirst(final E element) {
+        final Comparator<? super E> comparator = this.comparator;
+        for (Link<E> curr = this.head; curr != null; curr = curr.next) {
+            if (comparator.compare(element, curr.element) == 0) {
+                this.removeNode(curr);
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public boolean removeLast(final E element) {
+        final Comparator<? super E> comparator = this.comparator;
+        for (Link<E> curr = this.tail; curr != null; curr = curr.prev) {
+            if (comparator.compare(element, curr.element) == 0) {
+                this.removeNode(curr);
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public Iterator<E> iterator() {
+        return new Iterator<>() {
+            private Link<E> next = LinkedSortedSet.this.head;
+
+            @Override
+            public boolean hasNext() {
+                return this.next != null;
+            }
+
+            @Override
+            public E next() {
+                final Link<E> next = this.next;
+                if (next == null) {
+                    throw new NoSuchElementException();
+                }
+                this.next = next.next;
+                return next.element;
+            }
+        };
+    }
+
+    public E pollFirst() {
+        final Link<E> head = this.head;
+        if (head == null) {
+            return null;
+        }
+
+        final E ret = head.element;
+        final Link<E> next = head.next;
+
+        // unlink head
+        this.head = next;
+        if (next == null) {
+            this.tail = null;
+        } else {
+            next.prev = null;
+        }
+
+        // help GC
+        head.element = null;
+        head.next = null;
+
+        return ret;
+    }
+
+    public E pollLast() {
+        final Link<E> tail = this.tail;
+        if (tail == null) {
+            return null;
+        }
+
+        final E ret = tail.element;
+        final Link<E> prev = tail.prev;
+
+        // unlink tail
+        this.tail = prev;
+        if (prev == null) {
+            this.head = null;
+        } else {
+            prev.next = null;
+        }
+
+        // help GC
+        tail.element = null;
+        tail.prev = null;
+
+        return ret;
+    }
+
+    public Link<E> addLast(final E element) {
+        final Comparator<? super E> comparator = this.comparator;
+
+        Link<E> curr = this.tail;
+        if (curr != null) {
+            int compare;
+
+            while ((compare = comparator.compare(element, curr.element)) < 0) {
+                Link<E> prev = curr;
+                curr = curr.prev;
+                if (curr != null) {
+                    continue;
+                }
+                return this.head = prev.prev = new Link<>(element, null, prev);
+            }
+
+            if (compare != 0) {
+                // insert after curr
+                final Link<E> next = curr.next;
+                final Link<E> insert = new Link<>(element, curr, next);
+                curr.next = insert;
+
+                if (next == null) {
+                    this.tail = insert;
+                } else {
+                    next.prev = insert;
+                }
+                return insert;
+            }
+
+            return null;
+        } else {
+            return this.head = this.tail = new Link<>(element);
+        }
+    }
+
+    public Link<E> addFirst(final E element) {
+        final Comparator<? super E> comparator = this.comparator;
+
+        Link<E> curr = this.head;
+        if (curr != null) {
+            int compare;
+
+            while ((compare = comparator.compare(element, curr.element)) > 0) {
+                Link<E> prev = curr;
+                curr = curr.next;
+                if (curr != null) {
+                    continue;
+                }
+                return this.tail = prev.next = new Link<>(element, prev, null);
+            }
+
+            if (compare != 0) {
+                // insert before curr
+                final Link<E> prev = curr.prev;
+                final Link<E> insert = new Link<>(element, prev, curr);
+                curr.prev = insert;
+
+                if (prev == null) {
+                    this.head = insert;
+                } else {
+                    prev.next = insert;
+                }
+                return insert;
+            }
+
+            return null;
+        } else {
+            return this.head = this.tail = new Link<>(element);
+        }
+    }
+
+    public static final class Link<E> {
+        private E element;
+        private Link<E> prev;
+        private Link<E> next;
+
+        private Link() {}
+
+        private Link(final E element) {
+            this.element = element;
+        }
+
+        private Link(final E element, final Link<E> prev, final Link<E> next) {
+            this.element = element;
+            this.prev = prev;
+            this.next = next;
+        }
+    }
+}
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/ArrayUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/ArrayUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..ebb1ab06165addb173fea4d295001fe37f4e79d3
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/concurrentutil/util/ArrayUtil.java
@@ -0,0 +1,816 @@
+package ca.spottedleaf.concurrentutil.util;
+
+import java.lang.invoke.VarHandle;
+
+public final class ArrayUtil {
+
+    public static final VarHandle BOOLEAN_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(boolean[].class);
+
+    public static final VarHandle BYTE_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(byte[].class);
+
+    public static final VarHandle SHORT_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(short[].class);
+
+    public static final VarHandle INT_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(int[].class);
+
+    public static final VarHandle LONG_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(long[].class);
+
+    public static final VarHandle OBJECT_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(Object[].class);
+
+    private ArrayUtil() {
+        throw new RuntimeException();
+    }
+
+    /* byte array */
+
+    public static byte getPlain(final byte[] array, final int index) {
+        return (byte)BYTE_ARRAY_HANDLE.get(array, index);
+    }
+
+    public static byte getOpaque(final byte[] array, final int index) {
+        return (byte)BYTE_ARRAY_HANDLE.getOpaque(array, index);
+    }
+
+    public static byte getAcquire(final byte[] array, final int index) {
+        return (byte)BYTE_ARRAY_HANDLE.getAcquire(array, index);
+    }
+
+    public static byte getVolatile(final byte[] array, final int index) {
+        return (byte)BYTE_ARRAY_HANDLE.getVolatile(array, index);
+    }
+
+    public static void setPlain(final byte[] array, final int index, final byte value) {
+        BYTE_ARRAY_HANDLE.set(array, index, value);
+    }
+
+    public static void setOpaque(final byte[] array, final int index, final byte value) {
+        BYTE_ARRAY_HANDLE.setOpaque(array, index, value);
+    }
+
+    public static void setRelease(final byte[] array, final int index, final byte value) {
+        BYTE_ARRAY_HANDLE.setRelease(array, index, value);
+    }
+
+    public static void setVolatile(final byte[] array, final int index, final byte value) {
+        BYTE_ARRAY_HANDLE.setVolatile(array, index, value);
+    }
+
+    public static void setVolatileContended(final byte[] array, final int index, final byte param) {
+        int failures = 0;
+
+        for (byte curr = getVolatile(array, index);;++failures) {
+            for (int i = 0; i < failures; ++i) {
+                ConcurrentUtil.backoff();
+            }
+
+            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
+                return;
+            }
+        }
+    }
+
+    public static byte compareAndExchangeVolatile(final byte[] array, final int index, final byte expect, final byte update) {
+        return (byte)BYTE_ARRAY_HANDLE.compareAndExchange(array, index, expect, update);
+    }
+
+    public static byte getAndAddVolatile(final byte[] array, final int index, final byte param) {
+        return (byte)BYTE_ARRAY_HANDLE.getAndAdd(array, index, param);
+    }
+
+    public static byte getAndAndVolatile(final byte[] array, final int index, final byte param) {
+        return (byte)BYTE_ARRAY_HANDLE.getAndBitwiseAnd(array, index, param);
+    }
+
+    public static byte getAndOrVolatile(final byte[] array, final int index, final byte param) {
+        return (byte)BYTE_ARRAY_HANDLE.getAndBitwiseOr(array, index, param);
+    }
+
+    public static byte getAndXorVolatile(final byte[] array, final int index, final byte param) {
+        return (byte)BYTE_ARRAY_HANDLE.getAndBitwiseXor(array, index, param);
+    }
+
+    public static byte getAndSetVolatile(final byte[] array, final int index, final byte param) {
+        return (byte)BYTE_ARRAY_HANDLE.getAndSet(array, index, param);
+    }
+
+    public static byte compareAndExchangeVolatileContended(final byte[] array, final int index, final byte expect, final byte update) {
+        return (byte)BYTE_ARRAY_HANDLE.compareAndExchange(array, index, expect, update);
+    }
+
+    public static byte getAndAddVolatileContended(final byte[] array, final int index, final byte param) {
+        int failures = 0;
+
+        for (byte curr = getVolatile(array, index);;++failures) {
+            for (int i = 0; i < failures; ++i) {
+                ConcurrentUtil.backoff();
+            }
+
+            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (byte) (curr + param)))) {
+                return curr;
+            }
+        }
+    }
+
+    public static byte getAndAndVolatileContended(final byte[] array, final int index, final byte param) {
+        int failures = 0;
+
+        for (byte curr = getVolatile(array, index);;++failures) {
+            for (int i = 0; i < failures; ++i) {
+                ConcurrentUtil.backoff();
+            }
+
+            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (byte) (curr & param)))) {
+                return curr;
+            }
+        }
+    }
+
+    public static byte getAndOrVolatileContended(final byte[] array, final int index, final byte param) {
+        int failures = 0;
+
+        for (byte curr = getVolatile(array, index);;++failures) {
+            for (int i = 0; i < failures; ++i) {
+                ConcurrentUtil.backoff();
+            }
+
+            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (byte) (curr | param)))) {
+                return curr;
+            }
+        }
+    }
+
+    public static byte getAndXorVolatileContended(final byte[] array, final int index, final byte param) {
+        int failures = 0;
+
+        for (byte curr = getVolatile(array, index);;++failures) {
+            for (int i = 0; i < failures; ++i) {
+                ConcurrentUtil.backoff();
+            }
+
+            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (byte) (curr ^ param)))) {
+                return curr;
+            }
+        }
+    }
+
+    public static byte getAndSetVolatileContended(final byte[] array, final int index, final byte param) {
+        int failures = 0;
+
+        for (byte curr = getVolatile(array, index);;++failures) {
+            for (int i = 0; i < failures; ++i) {
+                ConcurrentUtil.backoff();
+            }
+
+            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
+                return curr;
+            }
+        }
+    }
+
+    /* short array */
+
+    public static short getPlain(final short[] array, final int index) {
+        return (short)SHORT_ARRAY_HANDLE.get(array, index);
+    }
+
+    public static short getOpaque(final short[] array, final int index) {
+        return (short)SHORT_ARRAY_HANDLE.getOpaque(array, index);
+    }
+
+    public static short getAcquire(final short[] array, final int index) {
+        return (short)SHORT_ARRAY_HANDLE.getAcquire(array, index);
+    }
+
+    public static short getVolatile(final short[] array, final int index) {
+        return (short)SHORT_ARRAY_HANDLE.getVolatile(array, index);
+    }
+
+    public static void setPlain(final short[] array, final int index, final short value) {
+        SHORT_ARRAY_HANDLE.set(array, index, value);
+    }
+
+    public static void setOpaque(final short[] array, final int index, final short value) {
+        SHORT_ARRAY_HANDLE.setOpaque(array, index, value);
+    }
+
+    public static void setRelease(final short[] array, final int index, final short value) {
+        SHORT_ARRAY_HANDLE.setRelease(array, index, value);
+    }
+
+    public static void setVolatile(final short[] array, final int index, final short value) {
+        SHORT_ARRAY_HANDLE.setVolatile(array, index, value);
+    }
+
+    public static void setVolatileContended(final short[] array, final int index, final short param) {
+        int failures = 0;
+
+        for (short curr = getVolatile(array, index);;++failures) {
+            for (int i = 0; i < failures; ++i) {
+                ConcurrentUtil.backoff();
+            }
+
+            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
+                return;
+            }
+        }
+    }
+
+    public static short compareAndExchangeVolatile(final short[] array, final int index, final short expect, final short update) {
+        return (short)SHORT_ARRAY_HANDLE.compareAndExchange(array, index, expect, update);
+    }
+
+    public static short getAndAddVolatile(final short[] array, final int index, final short param) {
+        return (short)SHORT_ARRAY_HANDLE.getAndAdd(array, index, param);
+    }
+
+    public static short getAndAndVolatile(final short[] array, final int index, final short param) {
+        return (short)SHORT_ARRAY_HANDLE.getAndBitwiseAnd(array, index, param);
+    }
+
+    public static short getAndOrVolatile(final short[] array, final int index, final short param) {
+        return (short)SHORT_ARRAY_HANDLE.getAndBitwiseOr(array, index, param);
+    }
+
+    public static short getAndXorVolatile(final short[] array, final int index, final short param) {
+        return (short)SHORT_ARRAY_HANDLE.getAndBitwiseXor(array, index, param);
+    }
+
+    public static short getAndSetVolatile(final short[] array, final int index, final short param) {
+        return (short)SHORT_ARRAY_HANDLE.getAndSet(array, index, param);
+    }
+
+    public static short compareAndExchangeVolatileContended(final short[] array, final int index, final short expect, final short update) {
+        return (short)SHORT_ARRAY_HANDLE.compareAndExchange(array, index, expect, update);
+    }
+
+    public static short getAndAddVolatileContended(final short[] array, final int index, final short param) {
+        int failures = 0;
+
+        for (short curr = getVolatile(array, index);;++failures) {
+            for (int i = 0; i < failures; ++i) {
+                ConcurrentUtil.backoff();
+            }
+
+            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (short) (curr + param)))) {
+                return curr;
+            }
+        }
+    }
+
+    public static short getAndAndVolatileContended(final short[] array, final int index, final short param) {
+        int failures = 0;
+
+        for (short curr = getVolatile(array, index);;++failures) {
+            for (int i = 0; i < failures; ++i) {
+                ConcurrentUtil.backoff();
+            }
+
+            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (short) (curr & param)))) {
+                return curr;
+            }
+        }
+    }
+
+    public static short getAndOrVolatileContended(final short[] array, final int index, final short param) {
+        int failures = 0;
+
+        for (short curr = getVolatile(array, index);;++failures) {
+            for (int i = 0; i < failures; ++i) {
+                ConcurrentUtil.backoff();
+            }
+
+            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (short) (curr | param)))) {
+                return curr;
+            }
+        }
+    }
+
+    public static short getAndXorVolatileContended(final short[] array, final int index, final short param) {
+        int failures = 0;
+
+        for (short curr = getVolatile(array, index);;++failures) {
+            for (int i = 0; i < failures; ++i) {
+                ConcurrentUtil.backoff();
+            }
+
+            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (short) (curr ^ param)))) {
+                return curr;
+            }
+        }
+    }
+
+    public static short getAndSetVolatileContended(final short[] array, final int index, final short param) {
+        int failures = 0;
+
+        for (short curr = getVolatile(array, index);;++failures) {
+            for (int i = 0; i < failures; ++i) {
+                ConcurrentUtil.backoff();
+            }
+
+            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
+                return curr;
+            }
+        }
+    }
+
+    /* int array */
+
+    public static int getPlain(final int[] array, final int index) {
+        return (int)INT_ARRAY_HANDLE.get(array, index);
+    }
+
+    public static int getOpaque(final int[] array, final int index) {
+        return (int)INT_ARRAY_HANDLE.getOpaque(array, index);
+    }
+
+    public static int getAcquire(final int[] array, final int index) {
+        return (int)INT_ARRAY_HANDLE.getAcquire(array, index);
+    }
+
+    public static int getVolatile(final int[] array, final int index) {
+        return (int)INT_ARRAY_HANDLE.getVolatile(array, index);
+    }
+
+    public static void setPlain(final int[] array, final int index, final int value) {
+        INT_ARRAY_HANDLE.set(array, index, value);
+    }
+
+    public static void setOpaque(final int[] array, final int index, final int value) {
+        INT_ARRAY_HANDLE.setOpaque(array, index, value);
+    }
+
+    public static void setRelease(final int[] array, final int index, final int value) {
+        INT_ARRAY_HANDLE.setRelease(array, index, value);
+    }
+
+    public static void setVolatile(final int[] array, final int index, final int value) {
+        INT_ARRAY_HANDLE.setVolatile(array, index, value);
+    }
+
+    public static void setVolatileContended(final int[] array, final int index, final int param) {
+        int failures = 0;
+
+        for (int curr = getVolatile(array, index);;++failures) {
+            for (int i = 0; i < failures; ++i) {
+                ConcurrentUtil.backoff();
+            }
+
+            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
+                return;
+            }
+        }
+    }
+
+    public static int compareAndExchangeVolatile(final int[] array, final int index, final int expect, final int update) {
+        return (int)INT_ARRAY_HANDLE.compareAndExchange(array, index, expect, update);
+    }
+
+    public static int getAndAddVolatile(final int[] array, final int index, final int param) {
+        return (int)INT_ARRAY_HANDLE.getAndAdd(array, index, param);
+    }
+
+    public static int getAndAndVolatile(final int[] array, final int index, final int param) {
+        return (int)INT_ARRAY_HANDLE.getAndBitwiseAnd(array, index, param);
+    }
+
+    public static int getAndOrVolatile(final int[] array, final int index, final int param) {
+        return (int)INT_ARRAY_HANDLE.getAndBitwiseOr(array, index, param);
+    }
+
+    public static int getAndXorVolatile(final int[] array, final int index, final int param) {
+        return (int)INT_ARRAY_HANDLE.getAndBitwiseXor(array, index, param);
+    }
+
+    public static int getAndSetVolatile(final int[] array, final int index, final int param) {
+        return (int)INT_ARRAY_HANDLE.getAndSet(array, index, param);
+    }
+
+    public static int compareAndExchangeVolatileContended(final int[] array, final int index, final int expect, final int update) {
+        return (int)INT_ARRAY_HANDLE.compareAndExchange(array, index, expect, update);
+    }
+
+    public static int getAndAddVolatileContended(final int[] array, final int index, final int param) {
+        int failures = 0;
+
+        for (int curr = getVolatile(array, index);;++failures) {
+            for (int i = 0; i < failures; ++i) {
+                ConcurrentUtil.backoff();
+            }
+
+            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (int) (curr + param)))) {
+                return curr;
+            }
+        }
+    }
+
+    public static int getAndAndVolatileContended(final int[] array, final int index, final int param) {
+        int failures = 0;
+
+        for (int curr = getVolatile(array, index);;++failures) {
+            for (int i = 0; i < failures; ++i) {
+                ConcurrentUtil.backoff();
+            }
+
+            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (int) (curr & param)))) {
+                return curr;
+            }
+        }
+    }
+
+    public static int getAndOrVolatileContended(final int[] array, final int index, final int param) {
+        int failures = 0;
+
+        for (int curr = getVolatile(array, index);;++failures) {
+            for (int i = 0; i < failures; ++i) {
+                ConcurrentUtil.backoff();
+            }
+
+            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (int) (curr | param)))) {
+                return curr;
+            }
+        }
+    }
+
+    public static int getAndXorVolatileContended(final int[] array, final int index, final int param) {
+        int failures = 0;
+
+        for (int curr = getVolatile(array, index);;++failures) {
+            for (int i = 0; i < failures; ++i) {
+                ConcurrentUtil.backoff();
+            }
+
+            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (int) (curr ^ param)))) {
+                return curr;
+            }
+        }
+    }
+
+    public static int getAndSetVolatileContended(final int[] array, final int index, final int param) {
+        int failures = 0;
+
+        for (int curr = getVolatile(array, index);;++failures) {
+            for (int i = 0; i < failures; ++i) {
+                ConcurrentUtil.backoff();
+            }
+
+            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
+                return curr;
+            }
+        }
+    }
+
+    /* long array */
+
+    public static long getPlain(final long[] array, final int index) {
+        return (long)LONG_ARRAY_HANDLE.get(array, index);
+    }
+
+    public static long getOpaque(final long[] array, final int index) {
+        return (long)LONG_ARRAY_HANDLE.getOpaque(array, index);
+    }
+
+    public static long getAcquire(final long[] array, final int index) {
+        return (long)LONG_ARRAY_HANDLE.getAcquire(array, index);
+    }
+
+    public static long getVolatile(final long[] array, final int index) {
+        return (long)LONG_ARRAY_HANDLE.getVolatile(array, index);
+    }
+
+    public static void setPlain(final long[] array, final int index, final long value) {
+        LONG_ARRAY_HANDLE.set(array, index, value);
+    }
+
+    public static void setOpaque(final long[] array, final int index, final long value) {
+        LONG_ARRAY_HANDLE.setOpaque(array, index, value);
+    }
+
+    public static void setRelease(final long[] array, final int index, final long value) {
+        LONG_ARRAY_HANDLE.setRelease(array, index, value);
+    }
+
+    public static void setVolatile(final long[] array, final int index, final long value) {
+        LONG_ARRAY_HANDLE.setVolatile(array, index, value);
+    }
+
+    public static void setVolatileContended(final long[] array, final int index, final long param) {
+        int failures = 0;
+
+        for (long curr = getVolatile(array, index);;++failures) {
+            for (int i = 0; i < failures; ++i) {
+                ConcurrentUtil.backoff();
+            }
+
+            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
+                return;
+            }
+        }
+    }
+
+    public static long compareAndExchangeVolatile(final long[] array, final int index, final long expect, final long update) {
+        return (long)LONG_ARRAY_HANDLE.compareAndExchange(array, index, expect, update);
+    }
+
+    public static long getAndAddVolatile(final long[] array, final int index, final long param) {
+        return (long)LONG_ARRAY_HANDLE.getAndAdd(array, index, param);
+    }
+
+    public static long getAndAndVolatile(final long[] array, final int index, final long param) {
+        return (long)LONG_ARRAY_HANDLE.getAndBitwiseAnd(array, index, param);
+    }
+
+    public static long getAndOrVolatile(final long[] array, final int index, final long param) {
+        return (long)LONG_ARRAY_HANDLE.getAndBitwiseOr(array, index, param);
+    }
+
+    public static long getAndXorVolatile(final long[] array, final int index, final long param) {
+        return (long)LONG_ARRAY_HANDLE.getAndBitwiseXor(array, index, param);
+    }
+
+    public static long getAndSetVolatile(final long[] array, final int index, final long param) {
+        return (long)LONG_ARRAY_HANDLE.getAndSet(array, index, param);
+    }
+
+    public static long compareAndExchangeVolatileContended(final long[] array, final int index, final long expect, final long update) {
+        return (long)LONG_ARRAY_HANDLE.compareAndExchange(array, index, expect, update);
+    }
+
+    public static long getAndAddVolatileContended(final long[] array, final int index, final long param) {
+        int failures = 0;
+
+        for (long curr = getVolatile(array, index);;++failures) {
+            for (int i = 0; i < failures; ++i) {
+                ConcurrentUtil.backoff();
+            }
+
+            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (long) (curr + param)))) {
+                return curr;
+            }
+        }
+    }
+
+    public static long getAndAndVolatileContended(final long[] array, final int index, final long param) {
+        int failures = 0;
+
+        for (long curr = getVolatile(array, index);;++failures) {
+            for (int i = 0; i < failures; ++i) {
+                ConcurrentUtil.backoff();
+            }
+
+            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (long) (curr & param)))) {
+                return curr;
+            }
+        }
+    }
+
+    public static long getAndOrVolatileContended(final long[] array, final int index, final long param) {
+        int failures = 0;
+
+        for (long curr = getVolatile(array, index);;++failures) {
+            for (int i = 0; i < failures; ++i) {
+                ConcurrentUtil.backoff();
+            }
+
+            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (long) (curr | param)))) {
+                return curr;
+            }
+        }
+    }
+
+    public static long getAndXorVolatileContended(final long[] array, final int index, final long param) {
+        int failures = 0;
+
+        for (long curr = getVolatile(array, index);;++failures) {
+            for (int i = 0; i < failures; ++i) {
+                ConcurrentUtil.backoff();
+            }
+
+            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (long) (curr ^ param)))) {
+                return curr;
+            }
+        }
+    }
+
+    public static long getAndSetVolatileContended(final long[] array, final int index, final long param) {
+        int failures = 0;
+
+        for (long curr = getVolatile(array, index);;++failures) {
+            for (int i = 0; i < failures; ++i) {
+                ConcurrentUtil.backoff();
+            }
+
+            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
+                return curr;
+            }
+        }
+    }
+
+    /* boolean array */
+
+    public static boolean getPlain(final boolean[] array, final int index) {
+        return (boolean)BOOLEAN_ARRAY_HANDLE.get(array, index);
+    }
+
+    public static boolean getOpaque(final boolean[] array, final int index) {
+        return (boolean)BOOLEAN_ARRAY_HANDLE.getOpaque(array, index);
+    }
+
+    public static boolean getAcquire(final boolean[] array, final int index) {
+        return (boolean)BOOLEAN_ARRAY_HANDLE.getAcquire(array, index);
+    }
+
+    public static boolean getVolatile(final boolean[] array, final int index) {
+        return (boolean)BOOLEAN_ARRAY_HANDLE.getVolatile(array, index);
+    }
+
+    public static void setPlain(final boolean[] array, final int index, final boolean value) {
+        BOOLEAN_ARRAY_HANDLE.set(array, index, value);
+    }
+
+    public static void setOpaque(final boolean[] array, final int index, final boolean value) {
+        BOOLEAN_ARRAY_HANDLE.setOpaque(array, index, value);
+    }
+
+    public static void setRelease(final boolean[] array, final int index, final boolean value) {
+        BOOLEAN_ARRAY_HANDLE.setRelease(array, index, value);
+    }
+
+    public static void setVolatile(final boolean[] array, final int index, final boolean value) {
+        BOOLEAN_ARRAY_HANDLE.setVolatile(array, index, value);
+    }
+
+    public static void setVolatileContended(final boolean[] array, final int index, final boolean param) {
+        int failures = 0;
+
+        for (boolean curr = getVolatile(array, index);;++failures) {
+            for (int i = 0; i < failures; ++i) {
+                ConcurrentUtil.backoff();
+            }
+
+            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
+                return;
+            }
+        }
+    }
+
+    public static boolean compareAndExchangeVolatile(final boolean[] array, final int index, final boolean expect, final boolean update) {
+        return (boolean)BOOLEAN_ARRAY_HANDLE.compareAndExchange(array, index, expect, update);
+    }
+
+    public static boolean getAndOrVolatile(final boolean[] array, final int index, final boolean param) {
+        return (boolean)BOOLEAN_ARRAY_HANDLE.getAndBitwiseOr(array, index, param);
+    }
+
+    public static boolean getAndXorVolatile(final boolean[] array, final int index, final boolean param) {
+        return (boolean)BOOLEAN_ARRAY_HANDLE.getAndBitwiseXor(array, index, param);
+    }
+
+    public static boolean getAndSetVolatile(final boolean[] array, final int index, final boolean param) {
+        return (boolean)BOOLEAN_ARRAY_HANDLE.getAndSet(array, index, param);
+    }
+
+    public static boolean compareAndExchangeVolatileContended(final boolean[] array, final int index, final boolean expect, final boolean update) {
+        return (boolean)BOOLEAN_ARRAY_HANDLE.compareAndExchange(array, index, expect, update);
+    }
+
+    public static boolean getAndAndVolatileContended(final boolean[] array, final int index, final boolean param) {
+        int failures = 0;
+
+        for (boolean curr = getVolatile(array, index);;++failures) {
+            for (int i = 0; i < failures; ++i) {
+                ConcurrentUtil.backoff();
+            }
+
+            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (boolean) (curr & param)))) {
+                return curr;
+            }
+        }
+    }
+
+    public static boolean getAndOrVolatileContended(final boolean[] array, final int index, final boolean param) {
+        int failures = 0;
+
+        for (boolean curr = getVolatile(array, index);;++failures) {
+            for (int i = 0; i < failures; ++i) {
+                ConcurrentUtil.backoff();
+            }
+
+            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (boolean) (curr | param)))) {
+                return curr;
+            }
+        }
+    }
+
+    public static boolean getAndXorVolatileContended(final boolean[] array, final int index, final boolean param) {
+        int failures = 0;
+
+        for (boolean curr = getVolatile(array, index);;++failures) {
+            for (int i = 0; i < failures; ++i) {
+                ConcurrentUtil.backoff();
+            }
+
+            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (boolean) (curr ^ param)))) {
+                return curr;
+            }
+        }
+    }
+
+    public static boolean getAndSetVolatileContended(final boolean[] array, final int index, final boolean param) {
+        int failures = 0;
+
+        for (boolean curr = getVolatile(array, index);;++failures) {
+            for (int i = 0; i < failures; ++i) {
+                ConcurrentUtil.backoff();
+            }
+
+            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
+                return curr;
+            }
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    public static <T> T getPlain(final T[] array, final int index) {
+        final Object ret = OBJECT_ARRAY_HANDLE.get((Object[])array, index);
+        return (T)ret;
+    }
+
+    @SuppressWarnings("unchecked")
+    public static <T> T getOpaque(final T[] array, final int index) {
+        final Object ret = OBJECT_ARRAY_HANDLE.getOpaque((Object[])array, index);
+        return (T)ret;
+    }
+
+    @SuppressWarnings("unchecked")
+    public static <T> T getAcquire(final T[] array, final int index) {
+        final Object ret = OBJECT_ARRAY_HANDLE.getAcquire((Object[])array, index);
+        return (T)ret;
+    }
+
+    @SuppressWarnings("unchecked")
+    public static <T> T getVolatile(final T[] array, final int index) {
+        final Object ret = OBJECT_ARRAY_HANDLE.getVolatile((Object[])array, index);
+        return (T)ret;
+    }
+
+    public static <T> void setPlain(final T[] array, final int index, final T value) {
+        OBJECT_ARRAY_HANDLE.set((Object[])array, index, (Object)value);
+    }
+
+    public static <T> void setOpaque(final T[] array, final int index, final T value) {
+        OBJECT_ARRAY_HANDLE.setOpaque((Object[])array, index, (Object)value);
+    }
+
+    public static <T> void setRelease(final T[] array, final int index, final T value) {
+        OBJECT_ARRAY_HANDLE.setRelease((Object[])array, index, (Object)value);
+    }
+
+    public static <T> void setVolatile(final T[] array, final int index, final T value) {
+        OBJECT_ARRAY_HANDLE.setVolatile((Object[])array, index, (Object)value);
+    }
+
+    public static <T> void setVolatileContended(final T[] array, final int index, final T param) {
+        int failures = 0;
+
+        for (T curr = getVolatile(array, index);;++failures) {
+            for (int i = 0; i < failures; ++i) {
+                ConcurrentUtil.backoff();
+            }
+
+            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
+                return;
+            }
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    public static <T> T compareAndExchangeVolatile(final T[] array, final int index, final T expect, final T update) {
+        final Object ret = OBJECT_ARRAY_HANDLE.compareAndExchange((Object[])array, index, (Object)expect, (Object)update);
+        return (T)ret;
+    }
+
+    @SuppressWarnings("unchecked")
+    public static <T> T getAndSetVolatile(final T[] array, final int index, final T param) {
+        final Object ret = BYTE_ARRAY_HANDLE.getAndSet((Object[])array, index, (Object)param);
+        return (T)ret;
+    }
+
+    @SuppressWarnings("unchecked")
+    public static <T> T compareAndExchangeVolatileContended(final T[] array, final int index, final T expect, final T update) {
+        final Object ret = OBJECT_ARRAY_HANDLE.compareAndExchange((Object[])array, index, (Object)expect, (Object)update);
+        return (T)ret;
+    }
+
+    public static <T> T getAndSetVolatileContended(final T[] array, final int index, final T param) {
+        int failures = 0;
+
+        for (T curr = getVolatile(array, index);;++failures) {
+            for (int i = 0; i < failures; ++i) {
+                ConcurrentUtil.backoff();
+            }
+
+            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
+                return curr;
+            }
+        }
+    }
+}
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/CollectionUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/CollectionUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..9420b9822de99d3a31224642452835b0c986f7b4
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/concurrentutil/util/CollectionUtil.java
@@ -0,0 +1,31 @@
+package ca.spottedleaf.concurrentutil.util;
+
+import java.util.Collection;
+
+public final class CollectionUtil {
+
+    public static String toString(final Collection<?> collection, final String name) {
+        return CollectionUtil.toString(collection, name, new StringBuilder(name.length() + 128)).toString();
+    }
+
+    public static StringBuilder toString(final Collection<?> collection, final String name, final StringBuilder builder) {
+        builder.append(name).append("{elements={");
+
+        boolean first = true;
+
+        for (final Object element : collection) {
+            if (!first) {
+                builder.append(", ");
+            }
+            first = false;
+
+            builder.append('"').append(element).append('"');
+        }
+
+        return builder.append("}}");
+    }
+
+    private CollectionUtil() {
+        throw new RuntimeException();
+    }
+}
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/ConcurrentUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/ConcurrentUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..23ae82e55696a7e2ff0e0f9609c0df6a48bb8d1d
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/concurrentutil/util/ConcurrentUtil.java
@@ -0,0 +1,166 @@
+package ca.spottedleaf.concurrentutil.util;
+
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.VarHandle;
+import java.util.concurrent.locks.LockSupport;
+
+public final class ConcurrentUtil {
+
+    public static String genericToString(final Object object) {
+        return object == null ? "null" : object.getClass().getName() + ":" + object.hashCode() + ":" + object.toString();
+    }
+
+    public static void rethrow(Throwable exception) {
+        rethrow0(exception);
+    }
+
+    private static <T extends Throwable> void rethrow0(Throwable thr) throws T {
+        throw (T)thr;
+    }
+
+    public static VarHandle getVarHandle(final Class<?> lookIn, final String fieldName, final Class<?> fieldType) {
+        try {
+            return MethodHandles.privateLookupIn(lookIn, MethodHandles.lookup()).findVarHandle(lookIn, fieldName, fieldType);
+        } catch (final Exception ex) {
+            throw new RuntimeException(ex); // unreachable
+        }
+    }
+
+    public static VarHandle getStaticVarHandle(final Class<?> lookIn, final String fieldName, final Class<?> fieldType) {
+        try {
+            return MethodHandles.privateLookupIn(lookIn, MethodHandles.lookup()).findStaticVarHandle(lookIn, fieldName, fieldType);
+        } catch (final Exception ex) {
+            throw new RuntimeException(ex); // unreachable
+        }
+    }
+
+    /**
+     * Non-exponential backoff algorithm to use in lightly contended areas.
+     * @see ConcurrentUtil#exponentiallyBackoffSimple(long)
+     * @see ConcurrentUtil#exponentiallyBackoffComplex(long)
+     */
+    public static void backoff() {
+        Thread.onSpinWait();
+    }
+
+    /**
+     * Backoff algorithm to use for a short held lock (i.e compareAndExchange operation). Generally this should not be
+     * used when a thread can block another thread. Instead, use {@link ConcurrentUtil#exponentiallyBackoffComplex(long)}.
+     * @param counter The current counter.
+     * @return The counter plus 1.
+     * @see ConcurrentUtil#backoff()
+     * @see ConcurrentUtil#exponentiallyBackoffComplex(long)
+     */
+    public static long exponentiallyBackoffSimple(final long counter) {
+        for (long i = 0; i < counter; ++i) {
+            backoff();
+        }
+        return counter + 1L;
+    }
+
+    /**
+     * Backoff algorithm to use for a lock that can block other threads (i.e if another thread contending with this thread
+     * can be thrown off the scheduler). This lock should not be used for simple locks such as compareAndExchange.
+     * @param counter The current counter.
+     * @return The next (if any) step in the backoff logic.
+     * @see ConcurrentUtil#backoff()
+     * @see ConcurrentUtil#exponentiallyBackoffSimple(long)
+     */
+    public static long exponentiallyBackoffComplex(final long counter) {
+        // TODO experimentally determine counters
+        if (counter < 100L) {
+            return exponentiallyBackoffSimple(counter);
+        }
+        if (counter < 1_200L) {
+            Thread.yield();
+            LockSupport.parkNanos(1_000L);
+            return counter + 1L;
+        }
+        // scale 0.1ms (100us) per failure
+        Thread.yield();
+        LockSupport.parkNanos(100_000L * counter);
+        return counter + 1;
+    }
+
+    /**
+     * Simple exponential backoff that will linearly increase the time per failure, according to the scale.
+     * @param counter The current failure counter.
+     * @param scale Time per failure, in ns.
+     * @param max The maximum time to wait for, in ns.
+     * @return The next counter.
+     */
+    public static long linearLongBackoff(long counter, final long scale, long max) {
+        counter = Math.min(Long.MAX_VALUE, counter + 1); // prevent overflow
+        max = Math.max(0, max);
+
+        if (scale <= 0L) {
+            return counter;
+        }
+
+        long time = scale * counter;
+
+        if (time > max || time / scale != counter) {
+            time = max;
+        }
+
+        boolean interrupted = Thread.interrupted();
+        if (time > 1_000_000L) { // 1ms
+            Thread.yield();
+        }
+        LockSupport.parkNanos(time);
+        if (interrupted) {
+            Thread.currentThread().interrupt();
+        }
+        return counter;
+    }
+
+    /**
+     * Simple exponential backoff that will linearly increase the time per failure, according to the scale.
+     * @param counter The current failure counter.
+     * @param scale Time per failure, in ns.
+     * @param max The maximum time to wait for, in ns.
+     * @param deadline The deadline in ns. Deadline time source: {@link System#nanoTime()}.
+     * @return The next counter.
+     */
+    public static long linearLongBackoffDeadline(long counter, final long scale, long max, long deadline) {
+        counter = Math.min(Long.MAX_VALUE, counter + 1); // prevent overflow
+        max = Math.max(0, max);
+
+        if (scale <= 0L) {
+            return counter;
+        }
+
+        long time = scale * counter;
+
+        // check overflow
+        if (time / scale != counter) {
+            // overflew
+            --counter;
+            time = max;
+        } else if (time > max) {
+            time = max;
+        }
+
+        final long currTime = System.nanoTime();
+        final long diff = deadline - currTime;
+        if (diff <= 0) {
+            return counter;
+        }
+        if (diff <= 1_500_000L) { // 1.5ms
+            time = 100_000L; // 100us
+        } else if (time > 1_000_000L) { // 1ms
+            Thread.yield();
+        }
+
+        boolean interrupted = Thread.interrupted();
+        LockSupport.parkNanos(time);
+        if (interrupted) {
+            Thread.currentThread().interrupt();
+        }
+        return counter;
+    }
+
+    public static VarHandle getArrayHandle(final Class<?> type) {
+        return MethodHandles.arrayElementVarHandle(type);
+    }
+}
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/HashUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/HashUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..2b9f36211d1cbb4fcf1457c0a83592499e9aa23b
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/concurrentutil/util/HashUtil.java
@@ -0,0 +1,111 @@
+package ca.spottedleaf.concurrentutil.util;
+
+public final class HashUtil {
+
+    // Copied from fastutil HashCommon
+
+    /** 2<sup>32</sup> &middot; &phi;, &phi; = (&#x221A;5 &minus; 1)/2. */
+    private static final int INT_PHI = 0x9E3779B9;
+    /** The reciprocal of {@link #INT_PHI} modulo 2<sup>32</sup>. */
+    private static final int INV_INT_PHI = 0x144cbc89;
+    /** 2<sup>64</sup> &middot; &phi;, &phi; = (&#x221A;5 &minus; 1)/2. */
+    private static final long LONG_PHI = 0x9E3779B97F4A7C15L;
+    /** The reciprocal of {@link #LONG_PHI} modulo 2<sup>64</sup>. */
+    private static final long INV_LONG_PHI = 0xf1de83e19937733dL;
+
+    /** Avalanches the bits of an integer by applying the finalisation step of MurmurHash3.
+     *
+     * <p>This method implements the finalisation step of Austin Appleby's <a href="http://code.google.com/p/smhasher/">MurmurHash3</a>.
+     * Its purpose is to avalanche the bits of the argument to within 0.25% bias.
+     *
+     * @param x an integer.
+     * @return a hash value with good avalanching properties.
+     */
+    // additional note: this function is a bijection onto all integers
+    public static int murmurHash3(int x) {
+        x ^= x >>> 16;
+        x *= 0x85ebca6b;
+        x ^= x >>> 13;
+        x *= 0xc2b2ae35;
+        x ^= x >>> 16;
+        return x;
+    }
+
+
+    /** Avalanches the bits of a long integer by applying the finalisation step of MurmurHash3.
+     *
+     * <p>This method implements the finalisation step of Austin Appleby's <a href="http://code.google.com/p/smhasher/">MurmurHash3</a>.
+     * Its purpose is to avalanche the bits of the argument to within 0.25% bias.
+     *
+     * @param x a long integer.
+     * @return a hash value with good avalanching properties.
+     */
+    // additional note: this function is a bijection onto all longs
+    public static long murmurHash3(long x) {
+        x ^= x >>> 33;
+        x *= 0xff51afd7ed558ccdL;
+        x ^= x >>> 33;
+        x *= 0xc4ceb9fe1a85ec53L;
+        x ^= x >>> 33;
+        return x;
+    }
+
+    /** Quickly mixes the bits of an integer.
+     *
+     * <p>This method mixes the bits of the argument by multiplying by the golden ratio and
+     * xorshifting the result. It is borrowed from <a href="https://github.com/leventov/Koloboke">Koloboke</a>, and
+     * it has slightly worse behaviour than {@link #murmurHash3(int)} (in open-addressing hash tables the average number of probes
+     * is slightly larger), but it's much faster.
+     *
+     * @param x an integer.
+     * @return a hash value obtained by mixing the bits of {@code x}.
+     * @see #invMix(int)
+     */
+    // additional note: this function is a bijection onto all integers
+    public static int mix(final int x) {
+        final int h = x * INT_PHI;
+        return h ^ (h >>> 16);
+    }
+
+    /** The inverse of {@link #mix(int)}. This method is mainly useful to create unit tests.
+     *
+     * @param x an integer.
+     * @return a value that passed through {@link #mix(int)} would give {@code x}.
+     */
+    // additional note: this function is a bijection onto all integers
+    public static int invMix(final int x) {
+        return (x ^ x >>> 16) * INV_INT_PHI;
+    }
+
+    /** Quickly mixes the bits of a long integer.
+     *
+     * <p>This method mixes the bits of the argument by multiplying by the golden ratio and
+     * xorshifting twice the result. It is borrowed from <a href="https://github.com/leventov/Koloboke">Koloboke</a>, and
+     * it has slightly worse behaviour than {@link #murmurHash3(long)} (in open-addressing hash tables the average number of probes
+     * is slightly larger), but it's much faster.
+     *
+     * @param x a long integer.
+     * @return a hash value obtained by mixing the bits of {@code x}.
+     */
+    // additional note: this function is a bijection onto all longs
+    public static long mix(final long x) {
+        long h = x * LONG_PHI;
+        h ^= h >>> 32;
+        return h ^ (h >>> 16);
+    }
+
+    /** The inverse of {@link #mix(long)}. This method is mainly useful to create unit tests.
+     *
+     * @param x a long integer.
+     * @return a value that passed through {@link #mix(long)} would give {@code x}.
+     */
+    // additional note: this function is a bijection onto all longs
+    public static long invMix(long x) {
+        x ^= x >>> 32;
+        x ^= x >>> 16;
+        return (x ^ x >>> 32) * INV_LONG_PHI;
+    }
+
+
+    private HashUtil() {}
+}
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/IntPairUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/IntPairUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..4e61c477a56e645228d5a2015c26816954d17bf8
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/concurrentutil/util/IntPairUtil.java
@@ -0,0 +1,46 @@
+package ca.spottedleaf.concurrentutil.util;
+
+public final class IntPairUtil {
+
+    /**
+     * Packs the specified integers into one long value.
+     */
+    public static long key(final int left, final int right) {
+        return ((long)right << 32) | (left & 0xFFFFFFFFL);
+    }
+
+    /**
+     * Retrieves the left packed integer from the key
+     */
+    public static int left(final long key) {
+        return (int)key;
+    }
+
+    /**
+     * Retrieves the right packed integer from the key
+     */
+    public static int right(final long key) {
+        return (int)(key >>> 32);
+    }
+
+    public static String toString(final long key) {
+        return "{left:" + left(key) + ", right:" + right(key) + "}";
+    }
+
+    public static String toString(final long[] array, final int from, final int to) {
+        final StringBuilder ret = new StringBuilder();
+        ret.append("[");
+
+        for (int i = from; i < to; ++i) {
+            if (i != from) {
+                ret.append(", ");
+            }
+            ret.append(toString(array[i]));
+        }
+
+        ret.append("]");
+        return ret.toString();
+    }
+
+    private IntPairUtil() {}
+}
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/IntegerUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/IntegerUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..77699c5fa9681f9ec7aa05cbb50eb60828e193ab
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/concurrentutil/util/IntegerUtil.java
@@ -0,0 +1,176 @@
+package ca.spottedleaf.concurrentutil.util;
+
+public final class IntegerUtil {
+
+    public static final int HIGH_BIT_U32 = Integer.MIN_VALUE;
+    public static final long HIGH_BIT_U64 = Long.MIN_VALUE;
+
+    public static int ceilLog2(final int value) {
+        return Integer.SIZE - Integer.numberOfLeadingZeros(value - 1); // see doc of numberOfLeadingZeros
+    }
+
+    public static long ceilLog2(final long value) {
+        return Long.SIZE - Long.numberOfLeadingZeros(value - 1); // see doc of numberOfLeadingZeros
+    }
+
+    public static int floorLog2(final int value) {
+        // xor is optimized subtract for 2^n -1
+        // note that (2^n -1) - k = (2^n -1) ^ k for k <= (2^n - 1)
+        return (Integer.SIZE - 1) ^ Integer.numberOfLeadingZeros(value); // see doc of numberOfLeadingZeros
+    }
+
+    public static int floorLog2(final long value) {
+        // xor is optimized subtract for 2^n -1
+        // note that (2^n -1) - k = (2^n -1) ^ k for k <= (2^n - 1)
+        return (Long.SIZE - 1) ^ Long.numberOfLeadingZeros(value); // see doc of numberOfLeadingZeros
+    }
+
+    public static int roundCeilLog2(final int value) {
+        // optimized variant of 1 << (32 - leading(val - 1))
+        // given
+        // 1 << n = HIGH_BIT_32 >>> (31 - n) for n [0, 32)
+        // 1 << (32 - leading(val - 1)) = HIGH_BIT_32 >>> (31 - (32 - leading(val - 1)))
+        // HIGH_BIT_32 >>> (31 - (32 - leading(val - 1)))
+        // HIGH_BIT_32 >>> (31 - 32 + leading(val - 1))
+        // HIGH_BIT_32 >>> (-1 + leading(val - 1))
+        return HIGH_BIT_U32 >>> (Integer.numberOfLeadingZeros(value - 1) - 1);
+    }
+
+    public static long roundCeilLog2(final long value) {
+        // see logic documented above
+        return HIGH_BIT_U64 >>> (Long.numberOfLeadingZeros(value - 1) - 1);
+    }
+
+    public static int roundFloorLog2(final int value) {
+        // optimized variant of 1 << (31 - leading(val))
+        // given
+        // 1 << n = HIGH_BIT_32 >>> (31 - n) for n [0, 32)
+        // 1 << (31 - leading(val)) = HIGH_BIT_32 >> (31 - (31 - leading(val)))
+        // HIGH_BIT_32 >> (31 - (31 - leading(val)))
+        // HIGH_BIT_32 >> (31 - 31 + leading(val))
+        return HIGH_BIT_U32 >>> Integer.numberOfLeadingZeros(value);
+    }
+
+    public static long roundFloorLog2(final long value) {
+        // see logic documented above
+        return HIGH_BIT_U64 >>> Long.numberOfLeadingZeros(value);
+    }
+
+    public static boolean isPowerOfTwo(final int n) {
+        // 2^n has one bit
+        // note: this rets true for 0 still
+        return IntegerUtil.getTrailingBit(n) == n;
+    }
+
+    public static boolean isPowerOfTwo(final long n) {
+        // 2^n has one bit
+        // note: this rets true for 0 still
+        return IntegerUtil.getTrailingBit(n) == n;
+    }
+
+    public static int getTrailingBit(final int n) {
+        return -n & n;
+    }
+
+    public static long getTrailingBit(final long n) {
+        return -n & n;
+    }
+
+    public static int trailingZeros(final int n) {
+        return Integer.numberOfTrailingZeros(n);
+    }
+
+    public static int trailingZeros(final long n) {
+        return Long.numberOfTrailingZeros(n);
+    }
+
+    // from hacker's delight (signed division magic value)
+    public static int getDivisorMultiple(final long numbers) {
+        return (int)(numbers >>> 32);
+    }
+
+    // from hacker's delight (signed division magic value)
+    public static int getDivisorShift(final long numbers) {
+        return (int)numbers;
+    }
+
+    // copied from hacker's delight (signed division magic value)
+    // http://www.hackersdelight.org/hdcodetxt/magic.c.txt
+    public static long getDivisorNumbers(final int d) {
+        final int ad = branchlessAbs(d);
+
+        if (ad < 2) {
+            throw new IllegalArgumentException("|number| must be in [2, 2^31 -1], not: " + d);
+        }
+
+        final int two31 = 0x80000000;
+        final long mask = 0xFFFFFFFFL; // mask for enforcing unsigned behaviour
+
+        /*
+         Signed usage:
+         int number;
+         long magic = getDivisorNumbers(div);
+         long mul = magic >>> 32;
+         int sign = number >> 31;
+         int result = (int)(((long)number * mul) >>> magic) - sign;
+         */
+        /*
+         Unsigned usage: (note: fails for input > Integer.MAX_VALUE, only use when input < Integer.MAX_VALUE to avoid sign calculation)
+         int number;
+         long magic = getDivisorNumbers(div);
+         long mul = magic >>> 32;
+         int result = (int)(((long)number * mul) >>> magic);
+         */
+
+        int p = 31;
+
+        // all these variables are UNSIGNED!
+        int t = two31 + (d >>> 31);
+        int anc = t - 1 - (int)((t & mask)%ad);
+        int q1 = (int)((two31 & mask)/(anc & mask));
+        int r1 = two31 - q1*anc;
+        int q2 = (int)((two31 & mask)/(ad & mask));
+        int r2 = two31 - q2*ad;
+        int delta;
+
+        do {
+            p = p + 1;
+            q1 = 2*q1;                        // Update q1 = 2**p/|nc|.
+            r1 = 2*r1;                        // Update r1 = rem(2**p, |nc|).
+            if ((r1 & mask) >= (anc & mask)) {// (Must be an unsigned comparison here)
+                q1 = q1 + 1;
+                r1 = r1 - anc;
+            }
+            q2 = 2*q2;                       // Update q2 = 2**p/|d|.
+            r2 = 2*r2;                       // Update r2 = rem(2**p, |d|).
+            if ((r2 & mask) >= (ad & mask)) {// (Must be an unsigned comparison here)
+                q2 = q2 + 1;
+                r2 = r2 - ad;
+            }
+            delta = ad - r2;
+        } while ((q1 & mask) < (delta & mask) || (q1 == delta && r1 == 0));
+
+        int magicNum = q2 + 1;
+        if (d < 0) {
+            magicNum = -magicNum;
+        }
+        int shift = p;
+        return ((long)magicNum << 32) | shift;
+    }
+
+    public static int branchlessAbs(final int val) {
+        // -n = -1 ^ n + 1
+        final int mask = val >> (Integer.SIZE - 1); // -1 if < 0, 0 if >= 0
+        return (mask ^ val) - mask; // if val < 0, then (0 ^ val) - 0 else (-1 ^ val) + 1
+    }
+
+    public static long branchlessAbs(final long val) {
+        // -n = -1 ^ n + 1
+        final long mask = val >> (Long.SIZE - 1); // -1 if < 0, 0 if >= 0
+        return (mask ^ val) - mask; // if val < 0, then (0 ^ val) - 0 else (-1 ^ val) + 1
+    }
+
+    private IntegerUtil() {
+        throw new RuntimeException();
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/ThrowUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/ThrowUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..a3a8b5c6795c4d116e094e4c910553416f565b93
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/concurrentutil/util/ThrowUtil.java
@@ -0,0 +1,11 @@
+package ca.spottedleaf.concurrentutil.util;
+
+public final class ThrowUtil {
+
+    private ThrowUtil() {}
+
+    public static <T extends Throwable> void throwUnchecked(final Throwable thr) throws T {
+        throw (T)thr;
+    }
+
+}
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/TimeUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/TimeUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..63688716244066581d5b505703576e3340e3baf3
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/concurrentutil/util/TimeUtil.java
@@ -0,0 +1,60 @@
+package ca.spottedleaf.concurrentutil.util;
+
+public final class TimeUtil {
+
+    /*
+     * The comparator is not a valid comparator for every long value. To prove where it is valid, see below.
+     *
+     * For reflexivity, we have that x - x = 0. We then have that for any long value x that
+     * compareTimes(x, x) == 0, as expected.
+     *
+     * For symmetry, we have that x - y = -(y - x) except for when y - x = Long.MIN_VALUE.
+     * So, the difference between any times x and y must not be equal to Long.MIN_VALUE.
+     *
+     * As for the transitive relation, consider we have x,y such that x - y = a > 0 and z such that
+     * y - z = b > 0. Then, we will have that the x - z > 0 is equivalent to a + b > 0. For long values,
+     * this holds as long as a + b <= Long.MAX_VALUE.
+     *
+     * Also consider we have x, y such that x - y = a < 0 and z such that y - z = b < 0. Then, we will have
+     * that x - z < 0 is equivalent to a + b < 0. For long values, this holds as long as a + b >= -Long.MAX_VALUE.
+     *
+     * Thus, the comparator is only valid for timestamps such that abs(c - d) <= Long.MAX_VALUE for all timestamps
+     * c and d.
+     */
+
+    /**
+     * This function is appropriate to be used as a {@link java.util.Comparator} between two timestamps, which
+     * indicates whether the timestamps represented by t1, t2 that t1 is before, equal to, or after t2.
+     */
+    public static int compareTimes(final long t1, final long t2) {
+        final long diff = t1 - t2;
+
+        // HD, Section 2-7
+        return (int) ((diff >> 63) | (-diff >>> 63));
+    }
+
+    public static long getGreatestTime(final long t1, final long t2) {
+        final long diff = t1 - t2;
+        return diff < 0L ? t2 : t1;
+    }
+
+    public static long getLeastTime(final long t1, final long t2) {
+        final long diff = t1 - t2;
+        return diff > 0L ? t2 : t1;
+    }
+
+    public static long clampTime(final long value, final long min, final long max) {
+        final long diffMax = value - max;
+        final long diffMin = value - min;
+
+        if (diffMax > 0L) {
+            return max;
+        }
+        if (diffMin < 0L) {
+            return min;
+        }
+        return value;
+    }
+
+    private TimeUtil() {}
+}
diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/Validate.java b/src/main/java/ca/spottedleaf/concurrentutil/util/Validate.java
new file mode 100644
index 0000000000000000000000000000000000000000..382177d0d162fa3139c9078a873ce2504a2b17b2
--- /dev/null
+++ b/src/main/java/ca/spottedleaf/concurrentutil/util/Validate.java
@@ -0,0 +1,28 @@
+package ca.spottedleaf.concurrentutil.util;
+
+public final class Validate {
+
+    public static <T> T notNull(final T obj) {
+        if (obj == null) {
+            throw new NullPointerException();
+        }
+        return obj;
+    }
+
+    public static <T> T notNull(final T obj, final String msgIfNull) {
+        if (obj == null) {
+            throw new NullPointerException(msgIfNull);
+        }
+        return obj;
+    }
+
+    public static void arrayBounds(final int off, final int len, final int arrayLength, final String msgPrefix) {
+        if (off < 0 || len < 0 || (arrayLength - off) < len) {
+            throw new ArrayIndexOutOfBoundsException(msgPrefix + ": off: " + off + ", len: " + len + ", array length: " + arrayLength);
+        }
+    }
+
+    private Validate() {
+        throw new RuntimeException();
+    }
+}