From 2bfea35de3036a12c776a092462e27c8dc0ff73c Mon Sep 17 00:00:00 2001
From: Spottedleaf <Spottedleaf@users.noreply.github.com>
Date: Fri, 14 Jun 2024 10:47:33 -0700
Subject: [PATCH] Update ConcurrentUtil

Mostly for the primitive long to reference hashtable impl
---
 patches/server/ConcurrentUtil.patch | 3892 +++++++++++++++++++++++++--
 1 file changed, 3678 insertions(+), 214 deletions(-)

diff --git a/patches/server/ConcurrentUtil.patch b/patches/server/ConcurrentUtil.patch
index 0ee59124d6..ad373f03f9 100644
--- a/patches/server/ConcurrentUtil.patch
+++ b/patches/server/ConcurrentUtil.patch
@@ -14,6 +14,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +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;
@@ -405,6 +406,24 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +
 +    /**
++     * 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>
@@ -1422,6 +1441,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
 +import ca.spottedleaf.concurrentutil.util.Validate;
++
 +import java.lang.invoke.VarHandle;
 +import java.util.ConcurrentModificationException;
 +
@@ -1577,13 +1597,13 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
 +import ca.spottedleaf.concurrentutil.executor.Cancellable;
 +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
-+import com.mojang.logging.LogUtils;
 +import org.slf4j.Logger;
++import org.slf4j.LoggerFactory;
 +import java.util.function.BiConsumer;
 +
 +public final class Completable<T> {
 +
-+    private static final Logger LOGGER = LogUtils.getLogger();
++    private static final Logger LOGGER = LoggerFactory.getLogger(Completable.class);
 +
 +    private final MultiThreadedQueue<BiConsumer<T, Throwable>> waiters = new MultiThreadedQueue<>();
 +    private T result;
@@ -1610,6 +1630,13 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        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);
@@ -1635,6 +1662,13 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +    }
 +
++    /**
++     * 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);
@@ -1678,10 +1712,26 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 @@ -0,0 +0,0 @@
 +package ca.spottedleaf.concurrentutil.executor;
 +
-+import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
 +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 {
 +
 +    /**
@@ -1710,7 +1760,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +     */
 +    public long getTotalTasksExecuted();
 +
-+
 +    /**
 +     * Waits until this queue has had all of its tasks executed (NOT removed). See {@link #haveAllTasksExecuted()}
 +     * <p>
@@ -1723,7 +1772,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +     *     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 cal.
++     *     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
@@ -1739,17 +1788,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    /**
 +     * 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
@@ -1791,12 +1829,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +
 +    /**
-+     * Waits and executes tasks until the condition returns {@code true} or {@code System.nanoTime() >= deadline}.
++     * 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) && !condition.getAsBoolean() && (System.nanoTime() < deadline)) {
++        while ((System.nanoTime() - deadline < 0L) && !condition.getAsBoolean() && (System.nanoTime() - deadline < 0L)) {
 +            if (this.executeTask()) {
 +                failures = failures >>> 2;
 +            } else {
@@ -1806,11 +1844,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +
 +    /**
-+     * Waits and executes tasks until {@code System.nanoTime() >= deadline}.
++     * Waits and executes tasks until {@code System.nanoTime() - deadline >= 0}.
 +     */
 +    public default void executeUntil(final long deadline) {
 +        long failures = 0;
-+        while (System.nanoTime() < deadline) {
++        while (System.nanoTime() - deadline < 0L) {
 +            if (this.executeTask()) {
 +                failures = failures >>> 2;
 +            } else {
@@ -1831,6 +1869,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +     *
 +     * @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();
@@ -1838,13 +1877,18 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    /**
 +     * Returns whether this queue has shut down. Effectively, whether new tasks will be rejected - this method
-+     * does not indicate whether all of the tasks scheduled have been executed.
++     * 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 {
 +
 +        /**
@@ -2080,6 +2124,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +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 {
@@ -2141,12 +2189,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +
 +        // returns the higher priority of the two
-+        public static PrioritisedExecutor.Priority max(final Priority p1, final Priority p2) {
++        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 PrioritisedExecutor.Priority min(final Priority p1, final Priority p2) {
++        public static Priority min(final Priority p1, final Priority p2) {
 +            return p1.isLowerOrEqualPriority(p2) ? p1 : p2;
 +        }
 +
@@ -2198,14 +2246,14 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            return priority > than;
 +        }
 +
-+        static final PrioritisedExecutor.Priority[] PRIORITIES = PrioritisedExecutor.Priority.values();
++        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 PrioritisedExecutor.Priority getPriority(final int priority) {
++        public static Priority getPriority(final int priority) {
 +            return PRIORITIES[priority + 1];
 +        }
 +
@@ -2227,6 +2275,26 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +
 +    /**
++     * 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.
 +     *
@@ -2236,7 +2304,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +     * associated with the parameter
 +     */
 +    public default PrioritisedTask queueRunnable(final Runnable task) {
-+        return this.queueRunnable(task, PrioritisedExecutor.Priority.NORMAL);
++        return this.queueRunnable(task, Priority.NORMAL);
 +    }
 +
 +    /**
@@ -2251,10 +2319,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +     * @return {@code null} if the current thread immediately executed the task, else returns the prioritised task
 +     * associated with the parameter
 +     */
-+    public PrioritisedTask queueRunnable(final Runnable task, final PrioritisedExecutor.Priority priority);
++    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 BaseExecutor.BaseTask#queue()}.
++     * 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.
 +     *
@@ -2264,12 +2332,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +     * @throws UnsupportedOperationException If this executor does not support lazily queueing tasks
 +     * @return The prioritised task associated with the parameters
 +     */
-+    public default PrioritisedExecutor.PrioritisedTask createTask(final Runnable task) {
-+        return this.createTask(task, PrioritisedExecutor.Priority.NORMAL);
++    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 BaseExecutor.BaseTask#queue()}.
++     * 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.
@@ -2280,15 +2348,21 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +     * @throws UnsupportedOperationException If this executor does not support lazily queueing tasks
 +     * @return The prioritised task associated with the parameters
 +     */
-+    public PrioritisedExecutor.PrioritisedTask createTask(final Runnable task, final PrioritisedExecutor.Priority priority);
++    public 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 PrioritisedExecutor.Priority#COMPLETING} will be returned
++         * Returns the current priority. Note that {@link Priority#COMPLETING} will be returned
 +         * if this task is completing or has completed.
 +         */
-+        public PrioritisedExecutor.Priority getPriority();
++        public Priority getPriority();
 +
 +        /**
 +         * Attempts to set this task's priority level to the level specified.
@@ -2299,7 +2373,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +         * @return {@code true} if successful, {@code false} if this task is completing or has completed or the queue
 +         * this task was scheduled on was shutdown, or if the priority was already at the specified level.
 +         */
-+        public boolean setPriority(final PrioritisedExecutor.Priority priority);
++        public boolean setPriority(final Priority priority);
 +
 +        /**
 +         * Attempts to raise the priority to the priority level specified.
@@ -2309,7 +2383,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +         * @throws IllegalArgumentException If the priority is invalid
 +         * @return {@code false} if the current task is completing, {@code true} if the priority was raised to the specified level or was already at the specified level or higher.
 +         */
-+        public boolean raisePriority(final PrioritisedExecutor.Priority priority);
++        public boolean raisePriority(final Priority priority);
 +
 +        /**
 +         * Attempts to lower the priority to the priority level specified.
@@ -2319,7 +2393,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +         * @throws IllegalArgumentException If the priority is invalid
 +         * @return {@code false} if the current task is completing, {@code true} if the priority was lowered to the specified level or was already at the specified level or lower.
 +         */
-+        public boolean lowerPriority(final PrioritisedExecutor.Priority priority);
++        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
@@ -2331,8 +2405,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +package ca.spottedleaf.concurrentutil.executor.standard;
 +
 +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
-+import com.mojang.logging.LogUtils;
 +import org.slf4j.Logger;
++import org.slf4j.LoggerFactory;
 +import java.lang.invoke.VarHandle;
 +import java.util.concurrent.locks.LockSupport;
 +
@@ -2347,14 +2421,14 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 + */
 +public class PrioritisedQueueExecutorThread extends Thread implements PrioritisedExecutor {
 +
-+    private static final Logger LOGGER = LogUtils.getLogger();
++    private static final Logger LOGGER = LoggerFactory.getLogger(PrioritisedQueueExecutorThread.class);
 +
 +    protected final PrioritisedExecutor queue;
 +
 +    protected volatile boolean threadShutdown;
 +
-+    protected static final VarHandle THREAD_PARKED_HANDLE = ConcurrentUtil.getVarHandle(PrioritisedQueueExecutorThread.class, "threadParked", boolean.class);
 +    protected volatile boolean threadParked;
++    protected static final VarHandle THREAD_PARKED_HANDLE = ConcurrentUtil.getVarHandle(PrioritisedQueueExecutorThread.class, "threadParked", boolean.class);
 +
 +    protected volatile boolean halted;
 +
@@ -2429,6 +2503,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +    }
 +
++    /**
++     * Attempts to poll as many tasks as possible, returning when finished.
++     * @return Whether any tasks were executed.
++     */
 +    protected boolean pollTasks() {
 +        boolean ret = false;
 +
@@ -2473,7 +2551,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    @Override
 +    public PrioritisedTask createTask(final Runnable task, final Priority priority) {
-+        final PrioritisedExecutor.PrioritisedTask queueTask = this.queue.createTask(task, priority);
++        final PrioritisedTask queueTask = this.queue.createTask(task, priority);
 +
 +        // need to override queue() to notify us of tasks
 +        return new PrioritisedTask() {
@@ -2519,8 +2597,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +
 +    @Override
-+    public PrioritisedExecutor.PrioritisedTask queueRunnable(final Runnable task, final PrioritisedExecutor.Priority priority) {
-+        final PrioritisedExecutor.PrioritisedTask ret = this.queue.queueRunnable(task, priority);
++    public PrioritisedTask queueRunnable(final Runnable task, final Priority priority) {
++        final PrioritisedTask ret = this.queue.queueRunnable(task, priority);
 +
 +        this.notifyTasks();
 +
@@ -2633,9 +2711,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 @@ -0,0 +0,0 @@
 +package ca.spottedleaf.concurrentutil.executor.standard;
 +
-+import com.mojang.logging.LogUtils;
 +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;
@@ -2645,30 +2723,46 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +public final class PrioritisedThreadPool {
 +
-+    private static final Logger LOGGER = LogUtils.getLogger();
++    private static final Logger LOGGER = LoggerFactory.getLogger(PrioritisedThreadPool.class);
 +
-+    protected final PrioritisedThread[] threads;
-+    protected final TreeSet<PrioritisedPoolExecutorImpl> queues = new TreeSet<>(PrioritisedPoolExecutorImpl.comparator());
-+    protected final String name;
-+    protected final long queueMaxHoldTime;
++    private final PrioritisedThread[] threads;
++    private final TreeSet<PrioritisedPoolExecutorImpl> queues = new TreeSet<>(PrioritisedPoolExecutorImpl.comparator());
++    private final String name;
++    private final long queueMaxHoldTime;
 +
-+    protected final ReferenceOpenHashSet<PrioritisedPoolExecutorImpl> nonShutdownQueues = new ReferenceOpenHashSet<>();
-+    protected final ReferenceOpenHashSet<PrioritisedPoolExecutorImpl> activeQueues = new ReferenceOpenHashSet<>();
++    private final ReferenceOpenHashSet<PrioritisedPoolExecutorImpl> nonShutdownQueues = new ReferenceOpenHashSet<>();
++    private final ReferenceOpenHashSet<PrioritisedPoolExecutorImpl> activeQueues = new ReferenceOpenHashSet<>();
 +
-+    protected boolean shutdown;
++    private boolean shutdown;
 +
-+    protected long schedulingIdGenerator;
++    private long schedulingIdGenerator;
 +
-+    protected static final long DEFAULT_QUEUE_HOLD_TIME = (long)(5.0e6);
++    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) {
@@ -2700,16 +2794,32 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +    }
 +
++    /**
++     * Returns an array representing the threads backing this thread pool.
++     */
 +    public Thread[] getThreads() {
 +        return Arrays.copyOf(this.threads, this.threads.length, Thread[].class);
 +    }
 +
-+    public PrioritisedPoolExecutor createExecutor(final String name, final int parallelism) {
++    /**
++     * 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));
++            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);
 +
@@ -2805,6 +2915,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +    }
 +
++    /**
++     * 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) {
@@ -2965,12 +3081,14 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +        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) {
++        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() {
@@ -2979,6 +3097,19 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                    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) {
@@ -3239,8 +3370,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    protected long taskIdGenerator = 0;
 +
 +    @Override
-+    public PrioritisedExecutor.PrioritisedTask queueRunnable(final Runnable task, final PrioritisedExecutor.Priority priority) throws IllegalStateException, IllegalArgumentException {
-+        if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
++    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) {
@@ -3273,7 +3404,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    @Override
 +    public PrioritisedExecutor.PrioritisedTask createTask(final Runnable task, final Priority priority) {
-+        if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
++        if (!Priority.isValidPriority(priority)) {
 +            throw new IllegalArgumentException("Priority " + priority + " is invalid");
 +        }
 +        if (task == null) {
@@ -3305,7 +3436,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return this.poll(Priority.IDLE);
 +    }
 +
-+    protected PrioritisedTask poll(final PrioritisedExecutor.Priority minPriority) {
++    protected PrioritisedTask poll(final Priority minPriority) {
 +        final ArrayDeque<PrioritisedTask>[] queues = this.queues;
 +        synchronized (queues) {
 +            final int max = minPriority.priority;
@@ -3382,10 +3513,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        protected static final long NOT_SCHEDULED_ID = -1L;
 +
 +        protected Runnable runnable;
-+        protected volatile PrioritisedExecutor.Priority priority;
++        protected volatile Priority priority;
 +
-+        protected PrioritisedTask(final long id, final Runnable runnable, final PrioritisedExecutor.Priority priority, final PrioritisedThreadedTaskQueue queue) {
-+            if (!PrioritisedExecutor.Priority.isValidPriority(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);
 +            }
 +
@@ -3395,8 +3526,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            this.id = id;
 +        }
 +
-+        protected PrioritisedTask(final Runnable runnable, final PrioritisedExecutor.Priority priority, final PrioritisedThreadedTaskQueue queue) {
-+            if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
++        protected PrioritisedTask(final Runnable runnable, final Priority priority, final PrioritisedThreadedTaskQueue queue) {
++            if (!Priority.isValidPriority(priority)) {
 +                throw new IllegalArgumentException("Invalid priority " + priority);
 +            }
 +
@@ -3417,8 +3548,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                    throw new IllegalStateException("Queue has shutdown");
 +                }
 +
-+                final PrioritisedExecutor.Priority priority = this.priority;
-+                if (priority == PrioritisedExecutor.Priority.COMPLETING) {
++                final Priority priority = this.priority;
++                if (priority == Priority.COMPLETING) {
 +                    return false;
 +                }
 +
@@ -3437,11 +3568,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +
 +        protected boolean trySetCompleting(final int minPriority) {
-+            final PrioritisedExecutor.Priority oldPriority = this.priority;
-+            if (oldPriority != PrioritisedExecutor.Priority.COMPLETING && oldPriority.isHigherOrEqualPriority(minPriority)) {
-+                this.priority = PrioritisedExecutor.Priority.COMPLETING;
++            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, PrioritisedExecutor.Priority.COMPLETING);
++                    this.queue.priorityChange(this, oldPriority, Priority.COMPLETING);
 +                }
 +                return true;
 +            }
@@ -3450,19 +3581,19 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +
 +        @Override
-+        public PrioritisedExecutor.Priority getPriority() {
++        public Priority getPriority() {
 +            return this.priority;
 +        }
 +
 +        @Override
-+        public boolean setPriority(final PrioritisedExecutor.Priority priority) {
-+            if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
++        public boolean setPriority(final Priority priority) {
++            if (!Priority.isValidPriority(priority)) {
 +                throw new IllegalArgumentException("Invalid priority " + priority);
 +            }
 +            synchronized (this.queue.queues) {
-+                final PrioritisedExecutor.Priority curr = this.priority;
++                final Priority curr = this.priority;
 +
-+                if (curr == PrioritisedExecutor.Priority.COMPLETING) {
++                if (curr == Priority.COMPLETING) {
 +                    return false;
 +                }
 +
@@ -3483,15 +3614,15 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +
 +        @Override
-+        public boolean raisePriority(final PrioritisedExecutor.Priority priority) {
-+            if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
++        public boolean raisePriority(final Priority priority) {
++            if (!Priority.isValidPriority(priority)) {
 +                throw new IllegalArgumentException("Invalid priority " + priority);
 +            }
 +
 +            synchronized (this.queue.queues) {
-+                final PrioritisedExecutor.Priority curr = this.priority;
++                final Priority curr = this.priority;
 +
-+                if (curr == PrioritisedExecutor.Priority.COMPLETING) {
++                if (curr == Priority.COMPLETING) {
 +                    return false;
 +                }
 +
@@ -3512,15 +3643,15 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +
 +        @Override
-+        public boolean lowerPriority(final PrioritisedExecutor.Priority priority) {
-+            if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
++        public boolean lowerPriority(final Priority priority) {
++            if (!Priority.isValidPriority(priority)) {
 +                throw new IllegalArgumentException("Invalid priority " + priority);
 +            }
 +
 +            synchronized (this.queue.queues) {
-+                final PrioritisedExecutor.Priority curr = this.priority;
++                final Priority curr = this.priority;
 +
-+                if (curr == PrioritisedExecutor.Priority.COMPLETING) {
++                if (curr == Priority.COMPLETING) {
 +                    return false;
 +                }
 +
@@ -3545,14 +3676,14 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            final long id;
 +            synchronized (this.queue.queues) {
 +                final Priority oldPriority = this.priority;
-+                if (oldPriority == PrioritisedExecutor.Priority.COMPLETING) {
++                if (oldPriority == Priority.COMPLETING) {
 +                    return false;
 +                }
 +
-+                this.priority = PrioritisedExecutor.Priority.COMPLETING;
++                this.priority = Priority.COMPLETING;
 +                // call priority change callback
 +                if ((id = this.id) != NOT_SCHEDULED_ID) {
-+                    this.queue.priorityChange(this, oldPriority, PrioritisedExecutor.Priority.COMPLETING);
++                    this.queue.priorityChange(this, oldPriority, Priority.COMPLETING);
 +                }
 +            }
 +            this.runnable = null;
@@ -3578,14 +3709,14 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        public boolean execute() {
 +            synchronized (this.queue.queues) {
 +                final Priority oldPriority = this.priority;
-+                if (oldPriority == PrioritisedExecutor.Priority.COMPLETING) {
++                if (oldPriority == Priority.COMPLETING) {
 +                    return false;
 +                }
 +
-+                this.priority = PrioritisedExecutor.Priority.COMPLETING;
++                this.priority = Priority.COMPLETING;
 +                // call priority change callback
 +                if (this.id != NOT_SCHEDULED_ID) {
-+                    this.queue.priorityChange(this, oldPriority, PrioritisedExecutor.Priority.COMPLETING);
++                    this.queue.priorityChange(this, oldPriority, Priority.COMPLETING);
 +                }
 +            }
 +
@@ -3594,6 +3725,2077 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +    }
 +}
+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..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/function/BiLong1Function.java
+@@ -0,0 +0,0 @@
++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..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/function/BiLongObjectConsumer.java
+@@ -0,0 +0,0 @@
++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..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/lock/ReentrantAreaLock.java
+@@ -0,0 +0,0 @@
++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..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/map/ConcurrentLong2ReferenceChainedHashTable.java
+@@ -0,0 +0,0 @@
++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 TableEntry<V>[] newTable = resizeChain.table;
++            if (newTable == null) {
++                this.currentTable = null;
++                return null;
++            }
++
++            // the increment is a multiple of table.length, so we can undo the increments on idx by taking the
++            // mod
++            int newIdx = index & (newTable.length - 1);
++
++            final ResizeChain<V> newChain = this.resizeChain = resizeChain.prev;
++            final TableEntry<V>[] prevTable = newChain.table;
++            final int increment;
++            if (prevTable == null) {
++                increment = 1;
++            } else {
++                increment = prevTable.length;
++            }
++
++            // done with the upper table, so we can skip the resize node
++            newIdx += increment;
++
++            this.increment = increment;
++            this.nextBin = newIdx;
++            this.currentTable = newTable;
++
++            return newTable;
++        }
++
++        private TableEntry<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.next = next;
++                this.prev = prev;
++            }
++        }
++    }
++
++    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..0000000000000000000000000000000000000000
@@ -3602,11 +5804,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 @@ -0,0 +0,0 @@
 +package ca.spottedleaf.concurrentutil.map;
 +
-+import ca.spottedleaf.concurrentutil.util.ArrayUtil;
 +import ca.spottedleaf.concurrentutil.util.CollectionUtil;
 +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
++import ca.spottedleaf.concurrentutil.util.HashUtil;
++import ca.spottedleaf.concurrentutil.util.IntegerUtil;
 +import ca.spottedleaf.concurrentutil.util.Validate;
-+import io.papermc.paper.util.IntegerUtil;
 +import java.lang.invoke.VarHandle;
 +import java.util.ArrayList;
 +import java.util.Arrays;
@@ -3724,7 +5926,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    /**
 +     * Constructs this map with the specified capacity and load factor.
 +     * @param capacity specified capacity, > 0
-+     * @param loadFactor specified load factor, > 0 and finite
++     * @param loadFactor specified load factor, > 0 && finite
 +     */
 +    public SWMRHashTable(final int capacity, final float loadFactor) {
 +        final int tableSize = getCapacityFor(capacity);
@@ -3772,7 +5974,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +     * 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 and finite
++     * @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) {
@@ -3780,6 +5982,15 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        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;
 +    }
@@ -3799,7 +6010,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        final int hash = SWMRHashTable.getHash(key);
 +        final TableEntry<K, V>[] table = this.getTableAcquire();
 +
-+        for (TableEntry<K, V> curr = ArrayUtil.getOpaque(table, hash & (table.length - 1)); curr != null; curr = curr.getNextOpaque()) {
++        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;
 +            }
@@ -3826,15 +6037,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    /** must be deterministic given a key */
 +    private static int getHash(final Object key) {
 +        int hash = key == null ? 0 : key.hashCode();
-+        // inlined IntegerUtil#hash0
-+        hash *= 0x36935555;
-+        hash ^= hash >>> 16;
-+        return hash;
-+    }
-+
-+    static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
-+    static final int spread(int h) {
-+        return (h ^ (h >>> 16)) & HASH_BITS;
++        return HashUtil.mix(hash);
 +    }
 +
 +    // rets -1 if capacity*loadFactor is too large
@@ -3856,10 +6059,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            return true;
 +        }
 +        /* Make no attempt to deal with concurrent modifications */
-+        if (!(obj instanceof Map)) {
++        if (!(obj instanceof Map<?, ?> other)) {
 +            return false;
 +        }
-+        final Map<?, ?> other = (Map<?, ?>)obj;
 +
 +        if (this.size() != other.size()) {
 +            return false;
@@ -3868,7 +6070,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        final TableEntry<K, V>[] table = this.getTableAcquire();
 +
 +        for (int i = 0, len = table.length; i < len; ++i) {
-+            for (TableEntry<K, V> curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
++            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);
@@ -3891,7 +6093,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        final TableEntry<K, V>[] table = this.getTableAcquire();
 +
 +        for (int i = 0, len = table.length; i < len; ++i) {
-+            for (TableEntry<K, V> curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
++            for (TableEntry<K, V> curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
 +                hash += curr.hashCode();
 +            }
 +        }
@@ -3905,7 +6107,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    @Override
 +    public String toString() {
 +        final StringBuilder builder = new StringBuilder(64);
-+        builder.append("SingleWriterMultiReaderHashMap:{");
++        builder.append("SWMRHashTable:{");
 +
 +        this.forEach((final K key, final V value) -> {
 +            builder.append("{key: \"").append(key).append("\", value: \"").append(value).append("\"}");
@@ -3926,7 +6128,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +     * {@inheritDoc}
 +     */
 +    @Override
-+    public Iterator<Entry<K, V>> iterator() {
++    public Iterator<Map.Entry<K, V>> iterator() {
 +        return new EntryIterator<>(this.getTableAcquire(), this);
 +    }
 +
@@ -3934,12 +6136,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +     * {@inheritDoc}
 +     */
 +    @Override
-+    public void forEach(final Consumer<? super Entry<K, V>> action) {
++    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 = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
++            for (TableEntry<K, V> curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
 +                action.accept(curr);
 +            }
 +        }
@@ -3954,7 +6156,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +        final TableEntry<K, V>[] table = this.getTableAcquire();
 +        for (int i = 0, len = table.length; i < len; ++i) {
-+            for (TableEntry<K, V> curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
++            for (TableEntry<K, V> curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
 +                final V value = curr.getValueAcquire();
 +
 +                action.accept(curr.key, value);
@@ -3971,7 +6173,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +        final TableEntry<K, V>[] table = this.getTableAcquire();
 +        for (int i = 0, len = table.length; i < len; ++i) {
-+            for (TableEntry<K, V> curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
++            for (TableEntry<K, V> curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
 +                action.accept(curr.key);
 +            }
 +        }
@@ -3986,7 +6188,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +        final TableEntry<K, V>[] table = this.getTableAcquire();
 +        for (int i = 0, len = table.length; i < len; ++i) {
-+            for (TableEntry<K, V> curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
++            for (TableEntry<K, V> curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
 +                final V value = curr.getValueAcquire();
 +
 +                action.accept(value);
@@ -4046,7 +6248,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +        final TableEntry<K, V>[] table = this.getTableAcquire();
 +        for (int i = 0, len = table.length; i < len; ++i) {
-+            for (TableEntry<K, V> curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
++            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;
@@ -4086,9 +6288,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return this.getSizeAcquire() == 0;
 +    }
 +
-+    protected Set<K> keyset;
-+    protected Collection<V> values;
-+    protected Set<Map.Entry<K, V>> entrySet;
++    protected KeySet<K, V> keyset;
++    protected ValueCollection<K, V> values;
++    protected EntrySet<K, V> entrySet;
 +
 +    @Override
 +    public Set<K> keySet() {
@@ -4187,7 +6389,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        final TableEntry<K, V> head = table[index];
 +        if (head == null) {
 +            final TableEntry<K, V> insert = new TableEntry<>(hash, key, value);
-+            ArrayUtil.setRelease(table, index, insert);
++            setAtIndexRelease(table, index, insert);
 +            this.addToSize(1);
 +            return null;
 +        }
@@ -4242,7 +6444,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                ++removed;
 +                this.removeFromSizePlain(1); /* required in case predicate throws an exception */
 +
-+                ArrayUtil.setRelease(table, i, curr = curr.getNextPlain());
++                setAtIndexRelease(table, i, curr = curr.getNextPlain());
 +
 +                if (curr == null) {
 +                    continue bin_iteration_loop;
@@ -4276,7 +6478,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +     * @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 Entry<K, V>> predicate) {
++    public int removeEntryIf(final Predicate<? super Map.Entry<K, V>> predicate) {
 +        Validate.notNull(predicate, "Null predicate");
 +
 +        int removed = 0;
@@ -4295,7 +6497,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                ++removed;
 +                this.removeFromSizePlain(1); /* required in case predicate throws an exception */
 +
-+                ArrayUtil.setRelease(table, i, curr = curr.getNextPlain());
++                setAtIndexRelease(table, i, curr = curr.getNextPlain());
 +
 +                if (curr == null) {
 +                    continue bin_iteration_loop;
@@ -4369,7 +6571,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                return false;
 +            }
 +
-+            ArrayUtil.setRelease(table, index, head.getNextPlain());
++            setAtIndexRelease(table, index, head.getNextPlain());
 +            this.removeFromSize(1);
 +
 +            return true;
@@ -4403,7 +6605,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +
 +        if (hash == head.hash && (head.key == key || head.key.equals(key))) {
-+            ArrayUtil.setRelease(table, index, head.getNextPlain());
++            setAtIndexRelease(table, index, head.getNextPlain());
 +            this.removeFromSize(1);
 +
 +            return head.getValuePlain();
@@ -4541,7 +6743,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +                final TableEntry<K, V> insert = new TableEntry<>(hash, key, newVal);
 +                if (prev == null) {
-+                    ArrayUtil.setRelease(table, index, insert);
++                    setAtIndexRelease(table, index, insert);
 +                } else {
 +                    prev.setNextRelease(insert);
 +                }
@@ -4560,7 +6762,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                }
 +
 +                if (prev == null) {
-+                    ArrayUtil.setRelease(table, index, curr.getNextPlain());
++                    setAtIndexRelease(table, index, curr.getNextPlain());
 +                } else {
 +                    prev.setNextRelease(curr.getNextPlain());
 +                }
@@ -4596,7 +6798,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            }
 +
 +            if (prev == null) {
-+                ArrayUtil.setRelease(table, index, curr.getNextPlain());
++                setAtIndexRelease(table, index, curr.getNextPlain());
 +            } else {
 +                prev.setNextRelease(curr.getNextPlain());
 +            }
@@ -4637,7 +6839,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +            final TableEntry<K, V> insert = new TableEntry<>(hash, key, newVal);
 +            if (prev == null) {
-+                ArrayUtil.setRelease(table, index, insert);
++                setAtIndexRelease(table, index, insert);
 +            } else {
 +                prev.setNextRelease(insert);
 +            }
@@ -4665,7 +6867,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            if (curr == null) {
 +                final TableEntry<K, V> insert = new TableEntry<>(hash, key, value);
 +                if (prev == null) {
-+                    ArrayUtil.setRelease(table, index, insert);
++                    setAtIndexRelease(table, index, insert);
 +                } else {
 +                    prev.setNextRelease(insert);
 +                }
@@ -4684,7 +6886,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                }
 +
 +                if (prev == null) {
-+                    ArrayUtil.setRelease(table, index, curr.getNextPlain());
++                    setAtIndexRelease(table, index, curr.getNextPlain());
 +                } else {
 +                    prev.setNextRelease(curr.getNextPlain());
 +                }
@@ -4698,6 +6900,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    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;
@@ -4749,35 +6953,19 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            this.value = value;
 +        }
 +
-+        /**
-+         * {@inheritDoc}
-+         */
 +        @Override
 +        public K getKey() {
 +            return this.key;
 +        }
 +
-+        /**
-+         * {@inheritDoc}
-+         */
 +        @Override
 +        public V getValue() {
 +            return this.getValueAcquire();
 +        }
 +
-+        /**
-+         * {@inheritDoc}
-+         */
 +        @Override
 +        public V setValue(final V value) {
-+            if (value == null) {
-+                throw new NullPointerException();
-+            }
-+
-+            final V curr = this.getValuePlain();
-+
-+            this.setValueRelease(value);
-+            return curr;
++            throw new UnsupportedOperationException();
 +        }
 +
 +        protected static int hash(final Object key, final Object value) {
@@ -4801,10 +6989,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                return true;
 +            }
 +
-+            if (!(obj instanceof Map.Entry)) {
++            if (!(obj instanceof Map.Entry<?, ?> other)) {
 +                return false;
 +            }
-+            final Map.Entry<?, ?> other = (Map.Entry<?, ?>)obj;
 +            final Object otherKey = other.getKey();
 +            final Object otherValue = other.getValue();
 +
@@ -4832,7 +7019,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            this.map = map;
 +            int tableIndex = 0;
 +            for (int len = table.length; tableIndex < len; ++tableIndex) {
-+                final TableEntry<K, V> entry = ArrayUtil.getOpaque(table, tableIndex);
++                final TableEntry<K, V> entry = getAtIndexOpaque(table, tableIndex);
 +                if (entry != null) {
 +                    this.nextEntry = entry;
 +                    this.tableIndex = tableIndex + 1;
@@ -4870,7 +7057,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +            // nothing in chain, so find next available bin
 +            for (;tableIndex < tableLength; ++tableIndex) {
-+                next = ArrayUtil.getOpaque(table, tableIndex);
++                next = getAtIndexOpaque(table, tableIndex);
 +                if (next != null) {
 +                    this.nextEntry = next;
 +                    this.tableIndex = tableIndex + 1;
@@ -5082,10 +7269,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +        @Override
 +        public boolean remove(final Object object) {
-+            if (!(object instanceof Map.Entry<?, ?>)) {
++            if (!(object instanceof Map.Entry<?, ?> entry)) {
 +                return false;
 +            }
-+            final Map.Entry<?, ?> entry = (Map.Entry<?, ?>)object;
 +
 +            final Object key;
 +            final Object value;
@@ -5117,21 +7303,20 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +
 +        @Override
-+        public Iterator<Entry<K, V>> iterator() {
++        public Iterator<Map.Entry<K, V>> iterator() {
 +            return new EntryIterator<>(this.map.getTableAcquire(), this.map);
 +        }
 +
 +        @Override
-+        public void forEach(final Consumer<? super Entry<K, V>> action) {
++        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)) {
++            if (!(object instanceof Map.Entry<?, ?> entry)) {
 +                return false;
 +            }
-+            final Map.Entry<?, ?> entry = (Map.Entry<?, ?>)object;
 +
 +            final Object key;
 +            final Object value;
@@ -5281,10 +7466,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 @@ -0,0 +0,0 @@
 +package ca.spottedleaf.concurrentutil.map;
 +
-+import ca.spottedleaf.concurrentutil.util.ArrayUtil;
++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 io.papermc.paper.util.IntegerUtil;
 +import java.lang.invoke.VarHandle;
 +import java.util.Arrays;
 +import java.util.function.Consumer;
@@ -5370,7 +7556,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    /**
 +     * Constructs this map with the specified capacity and load factor.
 +     * @param capacity specified capacity, > 0
-+     * @param loadFactor specified load factor, > 0 and finite
++     * @param loadFactor specified load factor, > 0 && finite
 +     */
 +    public SWMRLong2ObjectHashTable(final int capacity, final float loadFactor) {
 +        final int tableSize = getCapacityFor(capacity);
@@ -5418,7 +7604,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +     * 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 and finite
++     * @param loadFactor specified load factor, > 0 && finite
 +     * @param other The specified map.
 +     */
 +    public SWMRLong2ObjectHashTable(final int capacity, final float loadFactor, final SWMRLong2ObjectHashTable<V> other) {
@@ -5426,6 +7612,15 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        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;
 +    }
@@ -5445,7 +7640,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        final int hash = SWMRLong2ObjectHashTable.getHash(key);
 +        final TableEntry<V>[] table = this.getTableAcquire();
 +
-+        for (TableEntry<V> curr = ArrayUtil.getOpaque(table, hash & (table.length - 1)); curr != null; curr = curr.getNextOpaque()) {
++        for (TableEntry<V> curr = getAtIndexOpaque(table, hash & (table.length - 1)); curr != null; curr = curr.getNextOpaque()) {
 +            if (key == curr.key) {
 +                return curr;
 +            }
@@ -5471,7 +7666,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    /** must be deterministic given a key */
 +    protected static int getHash(final long key) {
-+        return (int)it.unimi.dsi.fastutil.HashCommon.mix(key);
++        return (int)HashUtil.mix(key);
 +    }
 +
 +    // rets -1 if capacity*loadFactor is too large
@@ -5493,10 +7688,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            return true;
 +        }
 +        /* Make no attempt to deal with concurrent modifications */
-+        if (!(obj instanceof SWMRLong2ObjectHashTable)) {
++        if (!(obj instanceof SWMRLong2ObjectHashTable<?> other)) {
 +            return false;
 +        }
-+        final SWMRLong2ObjectHashTable<?> other = (SWMRLong2ObjectHashTable<?>)obj;
 +
 +        if (this.size() != other.size()) {
 +            return false;
@@ -5505,7 +7699,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        final TableEntry<V>[] table = this.getTableAcquire();
 +
 +        for (int i = 0, len = table.length; i < len; ++i) {
-+            for (TableEntry<V> curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
++            for (TableEntry<V> curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
 +                final V value = curr.getValueAcquire();
 +
 +                final Object otherValue = other.get(curr.key);
@@ -5528,7 +7722,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        final TableEntry<V>[] table = this.getTableAcquire();
 +
 +        for (int i = 0, len = table.length; i < len; ++i) {
-+            for (TableEntry<V> curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
++            for (TableEntry<V> curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
 +                hash += curr.hashCode();
 +            }
 +        }
@@ -5562,22 +7756,17 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    /**
 +     * {@inheritDoc}
 +     */
-+    public void forEach(final Consumer<? super SWMRLong2ObjectHashTable.TableEntry<V>> action) {
++    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 = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
++            for (TableEntry<V> curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
 +                action.accept(curr);
 +            }
 +        }
 +    }
 +
-+    @FunctionalInterface
-+    public static interface BiLongObjectConsumer<V> {
-+        public void accept(final long key, final V value);
-+    }
-+
 +    /**
 +     * {@inheritDoc}
 +     */
@@ -5586,7 +7775,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +        final TableEntry<V>[] table = this.getTableAcquire();
 +        for (int i = 0, len = table.length; i < len; ++i) {
-+            for (TableEntry<V> curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
++            for (TableEntry<V> curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
 +                final V value = curr.getValueAcquire();
 +
 +                action.accept(curr.key, value);
@@ -5603,7 +7792,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +        final TableEntry<V>[] table = this.getTableAcquire();
 +        for (int i = 0, len = table.length; i < len; ++i) {
-+            for (TableEntry<V> curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
++            for (TableEntry<V> curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
 +                action.accept(curr.key);
 +            }
 +        }
@@ -5618,7 +7807,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +        final TableEntry<V>[] table = this.getTableAcquire();
 +        for (int i = 0, len = table.length; i < len; ++i) {
-+            for (TableEntry<V> curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
++            for (TableEntry<V> curr = getAtIndexOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
 +                final V value = curr.getValueAcquire();
 +
 +                action.accept(value);
@@ -5739,7 +7928,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        final TableEntry<V> head = table[index];
 +        if (head == null) {
 +            final TableEntry<V> insert = new TableEntry<>(key, value);
-+            ArrayUtil.setRelease(table, index, insert);
++            setAtIndexRelease(table, index, insert);
 +            this.addToSize(1);
 +            return null;
 +        }
@@ -5797,7 +7986,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +
 +        if (head.key == key) {
-+            ArrayUtil.setRelease(table, index, head.getNextPlain());
++            setAtIndexRelease(table, index, head.getNextPlain());
 +            this.removeFromSize(1);
 +
 +            return head.getValuePlain();
@@ -5815,6 +8004,44 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        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}
 +     */
@@ -5822,6 +8049,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        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}
 +     */
@@ -5847,6 +8078,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    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;
 +
@@ -5903,51 +8136,847 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        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..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/scheduler/SchedulerThreadPool.java
+@@ -0,0 +0,0 @@
++package ca.spottedleaf.concurrentutil.scheduler;
 +
-+        /**
-+         * {@inheritDoc}
-+         */
-+        public V setValue(final V value) {
-+            if (value == null) {
-+                throw new NullPointerException();
++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");
 +            }
 +
-+            final V curr = this.getValuePlain();
++            task.schedulerOwnedBy = this;
 +
-+            this.setValueRelease(value);
-+            return curr;
++            this.insertFresh(task);
 +        }
++    }
 +
-+        protected static int hash(final long key, final Object value) {
-+            return SWMRLong2ObjectHashTable.getHash(key) ^ (value == null ? 0 : value.hashCode());
-+        }
++    /**
++     * 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;
 +
-+        /**
-+         * {@inheritDoc}
-+         */
-+        @Override
-+        public int hashCode() {
-+            return hash(this.key, this.getValueAcquire());
-+        }
++                // 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);
++                }
 +
-+        /**
-+         * {@inheritDoc}
-+         */
-+        @Override
-+        public boolean equals(final Object obj) {
-+            if (this == obj) {
 +                return true;
 +            }
 +
-+            if (!(obj instanceof TableEntry<?>)) {
-+                return false;
++            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;
 +            }
-+            final TableEntry<?> other = (TableEntry<?>)obj;
-+            final long otherKey = other.getKey();
-+            final long thisKey = this.getKey();
-+            final Object otherValue = other.getValueAcquire();
-+            final V thisVal = this.getValueAcquire();
-+            return (thisKey == otherKey) && (thisVal == otherValue || thisVal.equals(otherValue));
++            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..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/set/LinkedSortedSet.java
+@@ -0,0 +0,0 @@
++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;
 +        }
 +    }
 +}
@@ -6982,6 +10011,441 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        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..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/util/HashUtil.java
+@@ -0,0 +0,0 @@
++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..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/util/IntPairUtil.java
+@@ -0,0 +0,0 @@
++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..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/util/IntegerUtil.java
+@@ -0,0 +0,0 @@
++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..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/util/ThrowUtil.java
+@@ -0,0 +0,0 @@
++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..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/util/TimeUtil.java
+@@ -0,0 +0,0 @@
++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..0000000000000000000000000000000000000000