diff --git a/patches/server/Actually-unload-POI-data.patch b/patches/server/Actually-unload-POI-data.patch
index a7fde503b9..d94a20ed52 100644
--- a/patches/server/Actually-unload-POI-data.patch
+++ b/patches/server/Actually-unload-POI-data.patch
@@ -9,18 +9,30 @@ sometimes it is.
 This patch also prevents the saving/unloading of POI data when
 world saving is disabled.
 
+diff --git a/src/main/java/net/minecraft/server/ChunkSystem.java b/src/main/java/net/minecraft/server/ChunkSystem.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/ChunkSystem.java
++++ b/src/main/java/net/minecraft/server/ChunkSystem.java
+@@ -0,0 +0,0 @@ public final class ChunkSystem {
+         for (int index = 0, len = chunkMap.regionManagers.size(); index < len; ++index) {
+             chunkMap.regionManagers.get(index).addChunk(holder.pos.x, holder.pos.z);
+         }
++        chunkMap.getPoiManager().dequeueUnload(holder.pos.longKey); // Paper - unload POI data
+     }
+ 
+     public static void onChunkHolderDelete(final ServerLevel level, final ChunkHolder holder) {
+@@ -0,0 +0,0 @@ public final class ChunkSystem {
+         for (int index = 0, len = chunkMap.regionManagers.size(); index < len; ++index) {
+             chunkMap.regionManagers.get(index).removeChunk(holder.pos.x, holder.pos.z);
+         }
++        chunkMap.getPoiManager().queueUnload(holder.pos.longKey, MinecraftServer.currentTickLong + 1); // Paper - unload POI data
+     }
+ 
+     public static void onChunkBorder(LevelChunk chunk, ChunkHolder holder) {
 diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
 --- a/src/main/java/net/minecraft/server/level/ChunkMap.java
 +++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
-                     }
-                     // Paper end
-                 }
-+                this.getPoiManager().dequeueUnload(holder.pos.longKey); // Paper - unload POI data
- 
-                 this.updatingChunks.queueUpdate(pos, holder); // Paper - Don't copy
-                 this.modified = true;
 @@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
  
      private void processUnloads(BooleanSupplier shouldKeepTicking) {
@@ -30,22 +42,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
              long j = longiterator.nextLong();
              ChunkHolder playerchunk = this.updatingChunks.queueRemove(j); // Paper - Don't copy
  
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
-                         this.regionManagers.get(index).removeChunk(holder.pos.x, holder.pos.z);
-                     }
-                     // Paper end
-+                    this.getPoiManager().queueUnload(holder.pos.longKey, MinecraftServer.currentTickLong + 1); // Paper - unload POI data
-                     if (ichunkaccess instanceof LevelChunk) {
-                         ((LevelChunk) ichunkaccess).setLoaded(false);
-                     }
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
-                 for (int index = 0, len = this.regionManagers.size(); index < len; ++index) {
-                     this.regionManagers.get(index).removeChunk(holder.pos.x, holder.pos.z);
-                 }
-+                this.getPoiManager().queueUnload(holder.pos.longKey, MinecraftServer.currentTickLong + 1); // Paper - unload POI data
-             } // Paper end
-                 } finally { this.unloadingPlayerChunk = unloadingBefore; } // Paper - do not allow ticket level changes while unloading chunks
- 
 @@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
                  }
                  this.poiManager.loadInData(pos, chunkHolder.poiData);
diff --git a/patches/server/Add-APIs-to-replace-OfflinePlayer-getLastPlayed.patch b/patches/server/Add-APIs-to-replace-OfflinePlayer-getLastPlayed.patch
index a66f236c3e..acf1da0faa 100644
--- a/patches/server/Add-APIs-to-replace-OfflinePlayer-getLastPlayed.patch
+++ b/patches/server/Add-APIs-to-replace-OfflinePlayer-getLastPlayed.patch
@@ -32,9 +32,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 --- a/src/main/java/net/minecraft/server/players/PlayerList.java
 +++ b/src/main/java/net/minecraft/server/players/PlayerList.java
 @@ -0,0 +0,0 @@ public abstract class PlayerList {
-     }
  
      public void placeNewPlayer(Connection connection, ServerPlayer player) {
+         player.isRealPlayer = true; // Paper - Chunk priority
 +        player.loginTime = System.currentTimeMillis(); // Paper
          GameProfile gameprofile = player.getGameProfile();
          GameProfileCache usercache = this.server.getProfileCache();
diff --git a/patches/server/Add-Early-Warning-Feature-to-WatchDog.patch b/patches/server/Add-Early-Warning-Feature-to-WatchDog.patch
index 9484ae6134..31a33111ac 100644
--- a/patches/server/Add-Early-Warning-Feature-to-WatchDog.patch
+++ b/patches/server/Add-Early-Warning-Feature-to-WatchDog.patch
@@ -122,6 +122,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                // Paper end - Different message for short timeout
                  log.log( Level.SEVERE, "------------------------------" );
                  log.log( Level.SEVERE, "Server thread dump (Look for plugins here before reporting to Paper!):" ); // Paper
+                 com.destroystokyo.paper.io.chunk.ChunkTaskManager.dumpAllChunkLoadInfo(); // Paper
                  WatchdogThread.dumpThread( ManagementFactory.getThreadMXBean().getThreadInfo( MinecraftServer.getServer().serverThread.getId(), Integer.MAX_VALUE ), log );
                  log.log( Level.SEVERE, "------------------------------" );
                  //
diff --git a/patches/server/Add-Plugin-Tickets-to-API-Chunk-Methods.patch b/patches/server/Add-Plugin-Tickets-to-API-Chunk-Methods.patch
index 484d24771b..7c0ab821b6 100644
--- a/patches/server/Add-Plugin-Tickets-to-API-Chunk-Methods.patch
+++ b/patches/server/Add-Plugin-Tickets-to-API-Chunk-Methods.patch
@@ -112,10 +112,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
          return true;
          // Paper end
 @@ -0,0 +0,0 @@ public class CraftWorld extends CraftRegionAccessor implements World {
- 
-         return this.world.getChunkSource().getChunkAtAsynchronously(x, z, gen, urgent).thenComposeAsync((either) -> {
-             net.minecraft.world.level.chunk.LevelChunk chunk = (net.minecraft.world.level.chunk.LevelChunk) either.left().orElse(null);
-+            if (chunk != null) addTicket(x, z); // Paper
-             return java.util.concurrent.CompletableFuture.completedFuture(chunk == null ? null : chunk.getBukkitChunk());
-         }, net.minecraft.server.MinecraftServer.getServer());
-     }
+         net.minecraft.server.ChunkSystem.scheduleChunkLoad(this.getHandle(), x, z, gen, ChunkStatus.FULL, true, priority, (c) -> {
+             net.minecraft.server.MinecraftServer.getServer().scheduleOnMain(() -> {
+                 net.minecraft.world.level.chunk.LevelChunk chunk = (net.minecraft.world.level.chunk.LevelChunk)c;
++                if (chunk != null) addTicket(x, z); // Paper
+                 ret.complete(chunk == null ? null : chunk.getBukkitChunk());
+             });
+         });
diff --git a/patches/server/Add-velocity-warnings.patch b/patches/server/Add-velocity-warnings.patch
index f7fa993bbd..5225494b80 100644
--- a/patches/server/Add-velocity-warnings.patch
+++ b/patches/server/Add-velocity-warnings.patch
@@ -85,4 +85,4 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                // Paper end
                  log.log( Level.SEVERE, "------------------------------" );
                  log.log( Level.SEVERE, "Server thread dump (Look for plugins here before reporting to Paper!):" ); // Paper
-                 WatchdogThread.dumpThread( ManagementFactory.getThreadMXBean().getThreadInfo( MinecraftServer.getServer().serverThread.getId(), Integer.MAX_VALUE ), log );
+                 com.destroystokyo.paper.io.chunk.ChunkTaskManager.dumpAllChunkLoadInfo(); // Paper
diff --git a/patches/server/Adventure.patch b/patches/server/Adventure.patch
index e02ea5d09e..1f973f573d 100644
--- a/patches/server/Adventure.patch
+++ b/patches/server/Adventure.patch
@@ -2703,9 +2703,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      private static final Random rand = new Random();
  
 @@ -0,0 +0,0 @@ public class CraftWorld extends CraftRegionAccessor implements World {
-         return this.spigot;
+ 
+         return ret;
      }
-     // Spigot end
 +
 +    // Paper start - implement pointers
 +    @Override
@@ -2719,7 +2719,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +        return this.adventure$pointers;
 +    }
-+    // Paper end
+     // Paper end
  }
 diff --git a/src/main/java/org/bukkit/craftbukkit/Main.java b/src/main/java/org/bukkit/craftbukkit/Main.java
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
diff --git a/patches/server/Async-GameProfileCache-saving.patch b/patches/server/Async-GameProfileCache-saving.patch
index b8e18acaf8..5a99f753b6 100644
--- a/patches/server/Async-GameProfileCache-saving.patch
+++ b/patches/server/Async-GameProfileCache-saving.patch
@@ -16,7 +16,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            this.getProfileCache().save(false); // Paper
          }
          // Spigot end
- 
+         com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE.close(true, true); // Paper
 diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
 --- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
diff --git a/patches/server/Async-catch-modifications-to-critical-entity-state.patch b/patches/server/Async-catch-modifications-to-critical-entity-state.patch
index 4d386210f5..ce05bbdfd2 100644
--- a/patches/server/Async-catch-modifications-to-critical-entity-state.patch
+++ b/patches/server/Async-catch-modifications-to-critical-entity-state.patch
@@ -24,9 +24,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
      private boolean addEntity(T entity, boolean existing) {
 +        org.spigotmc.AsyncCatcher.catchOp("Entity add"); // Paper
-         if (!this.addEntityUuid(entity)) {
-             return false;
-         } else {
+         // Paper start - chunk system hooks
+         if (existing) {
+             // I don't want to know why this is a generic type.
 @@ -0,0 +0,0 @@ public class PersistentEntitySectionManager<T extends EntityAccess> implements A
      }
  
diff --git a/patches/server/AsyncTabCompleteEvent.patch b/patches/server/AsyncTabCompleteEvent.patch
index bcdf37cd22..b1c26b4ea2 100644
--- a/patches/server/AsyncTabCompleteEvent.patch
+++ b/patches/server/AsyncTabCompleteEvent.patch
@@ -37,6 +37,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            server.scheduleOnMain(() -> this.disconnect(Component.translatable("disconnect.spam", new Object[0]))); // Paper
              return;
          }
+         // Paper start
+@@ -0,0 +0,0 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
+         }
+         // Paper end
          // CraftBukkit end
 +        // Paper start - async tab completion
 +        TAB_COMPLETE_EXECUTOR.execute(() -> {
diff --git a/patches/server/Asynchronous-chunk-IO-and-loading.patch b/patches/server/Asynchronous-chunk-IO-and-loading.patch
index 753e693701..d350fddbbe 100644
--- a/patches/server/Asynchronous-chunk-IO-and-loading.patch
+++ b/patches/server/Asynchronous-chunk-IO-and-loading.patch
@@ -2282,7 +2282,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 --- a/src/main/java/net/minecraft/server/MinecraftServer.java
 +++ b/src/main/java/net/minecraft/server/MinecraftServer.java
 @@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
-             this.getProfileCache().save(false); // Paper
+             this.getProfileCache().save();
          }
          // Spigot end
 -
@@ -2302,6 +2302,31 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
              this.poiManager.close();
          } finally {
              super.close();
+@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+     }
+ 
+     protected void saveAllChunks(boolean flush) {
++        // Paper start - do not overload I/O threads with too much work when saving
++        int[] saved = new int[1];
++        int maxAsyncSaves = 50;
++        Runnable onChunkSave = () -> {
++            if (++saved[0] >= maxAsyncSaves) {
++                saved[0] = 0;
++                com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE.flush();
++            }
++        };
++        // Paper end - do not overload I/O threads with too much work when saving
+         if (flush) {
+             List<ChunkHolder> list = (List) net.minecraft.server.ChunkSystem.getVisibleChunkHolders(this.level).stream().filter(ChunkHolder::wasAccessibleSinceLastSave).peek(ChunkHolder::refreshAccessibility).collect(Collectors.toList()); // Paper
+             MutableBoolean mutableboolean = new MutableBoolean();
+@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+                 }).filter((ichunkaccess) -> {
+                     return ichunkaccess instanceof ImposterProtoChunk || ichunkaccess instanceof LevelChunk;
+                 }).filter(this::save).forEach((ichunkaccess) -> {
++                    onChunkSave.run(); // Paper - do not overload I/O threads with too much work when saving
+                     mutableboolean.setTrue();
+                 });
+             } while (mutableboolean.isTrue());
 @@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
              this.processUnloads(() -> {
                  return true;
@@ -2310,7 +2335,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            //this.flushWorker(); // Paper - nuke IOWorker
 +            this.level.asyncChunkTaskManager.flush(); // Paper - flush to preserve behavior compat with pre-async behaviour
          } else {
-             this.visibleChunkMap.values().forEach(this::saveChunkIfNeeded);
+             net.minecraft.server.ChunkSystem.getVisibleChunkHolders(this.level).forEach(this::saveChunkIfNeeded);
          }
 @@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
      protected void tick(BooleanSupplier shouldKeepTicking) {
@@ -2824,7 +2849,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 --- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
 +++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
 @@ -0,0 +0,0 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic
-             server.scheduleOnMain(() -> this.disconnect(Component.translatable("disconnect.spam", new Object[0]))); // Paper
+             this.disconnect(Component.translatable("disconnect.spam"));
              return;
          }
 +        // Paper start
@@ -2835,8 +2860,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +        // Paper end
          // CraftBukkit end
-         // Paper start - async tab completion
-         TAB_COMPLETE_EXECUTOR.execute(() -> {
+         StringReader stringreader = new StringReader(packet.getCommand());
+ 
 diff --git a/src/main/java/net/minecraft/util/worldupdate/WorldUpgrader.java b/src/main/java/net/minecraft/util/worldupdate/WorldUpgrader.java
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
 --- a/src/main/java/net/minecraft/util/worldupdate/WorldUpgrader.java
@@ -3365,7 +3390,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            // Paper end
              return regionfile;
          } else {
-             if (this.regionCache.size() >= io.papermc.paper.configuration.GlobalConfiguration.get().misc.regionFileCacheSize) { // Paper - configurable
+             if (this.regionCache.size() >= 256) {
 @@ -0,0 +0,0 @@ public class RegionFileStorage implements AutoCloseable {
              RegionFile regionfile1 = new RegionFile(path1, this.folder, this.sync);
  
@@ -3410,13 +3435,13 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 -        RegionFile regionfile = this.getRegionFile(pos, false); // CraftBukkit
 +        RegionFile regionfile = this.getRegionFile(pos, false, true); // CraftBukkit // Paper
 +        try { // Paper
-         int attempts = 0; Exception laste = null; while (attempts++ < 5) { try { // Paper
  
          if (nbt == null) {
+             regionfile.clear(pos);
 @@ -0,0 +0,0 @@ public class RegionFileStorage implements AutoCloseable {
-             net.minecraft.server.MinecraftServer.LOGGER.error("Failed to save chunk", laste);
+             }
          }
-         // Paper end
+ 
 +        } finally { // Paper start
 +            regionfile.fileLock.unlock();
 +        } // Paper end
@@ -3554,95 +3579,14 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
 +    // Paper end
  }
-diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
-+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
-@@ -0,0 +0,0 @@ public class CraftWorld extends CraftRegionAccessor implements World {
-     public DragonBattle getEnderDragonBattle() {
-         return (this.getHandle().dragonFight() == null) ? null : new CraftDragonBattle(this.getHandle().dragonFight());
-     }
-+    // Paper start
-+    @Override
-+    public java.util.concurrent.CompletableFuture<Chunk> getChunkAtAsync(int x, int z, boolean gen, boolean urgent) {
-+        if (Bukkit.isPrimaryThread()) {
-+            net.minecraft.world.level.chunk.LevelChunk immediate = this.world.getChunkSource().getChunkAtIfLoadedImmediately(x, z);
-+            if (immediate != null) {
-+                return java.util.concurrent.CompletableFuture.completedFuture(immediate.getBukkitChunk());
-+            }
-+        } else {
-+            java.util.concurrent.CompletableFuture<Chunk> future = new java.util.concurrent.CompletableFuture<Chunk>();
-+            world.getServer().execute(() -> {
-+                getChunkAtAsync(x, z, gen, urgent).whenComplete((chunk, err) -> {
-+                    if (err != null) {
-+                        future.completeExceptionally(err);
-+                    } else {
-+                        future.complete(chunk);
-+                    }
-+                });
-+            });
-+            return future;
-+        }
-+
-+        return this.world.getChunkSource().getChunkAtAsynchronously(x, z, gen, urgent).thenComposeAsync((either) -> {
-+            net.minecraft.world.level.chunk.LevelChunk chunk = (net.minecraft.world.level.chunk.LevelChunk) either.left().orElse(null);
-+            return java.util.concurrent.CompletableFuture.completedFuture(chunk == null ? null : chunk.getBukkitChunk());
-+        }, net.minecraft.server.MinecraftServer.getServer());
-+    }
-+    // Paper end
- 
-     @Override
-     public PersistentDataContainer getPersistentDataContainer() {
-diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
-+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
-@@ -0,0 +0,0 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity {
-         this.entity.setYHeadRot(yaw);
-     }
- 
-+    // Paper start
-+    @Override
-+    public java.util.concurrent.CompletableFuture<Boolean> teleportAsync(Location location, TeleportCause cause) {
-+        Preconditions.checkArgument(location != null, "location");
-+        location.checkFinite();
-+        Location locationClone = location.clone(); // clone so we don't need to worry about mutations after this call.
-+
-+        net.minecraft.server.level.ServerLevel world = ((CraftWorld)locationClone.getWorld()).getHandle();
-+        java.util.concurrent.CompletableFuture<Boolean> ret = new java.util.concurrent.CompletableFuture<>();
-+
-+        world.loadChunksForMoveAsync(getHandle().getBoundingBoxAt(locationClone.getX(), locationClone.getY(), locationClone.getZ()), location.getX(), location.getZ(), (list) -> {
-+            net.minecraft.server.level.ServerChunkCache chunkProviderServer = world.getChunkSource();
-+            for (net.minecraft.world.level.chunk.ChunkAccess chunk : list) {
-+                chunkProviderServer.addTicketAtLevel(net.minecraft.server.level.TicketType.POST_TELEPORT, chunk.getPos(), 33, CraftEntity.this.getEntityId());
-+            }
-+            net.minecraft.server.MinecraftServer.getServer().scheduleOnMain(() -> {
-+                try {
-+                    ret.complete(CraftEntity.this.teleport(locationClone, cause) ? Boolean.TRUE : Boolean.FALSE);
-+                } catch (Throwable throwable) {
-+                    if (throwable instanceof ThreadDeath) {
-+                        throw (ThreadDeath)throwable;
-+                    }
-+                    ret.completeExceptionally(throwable);
-+                }
-+            });
-+        });
-+
-+        return ret;
-+    }
-+    // Paper end
-+
-     @Override
-     public boolean teleport(Location location) {
-         return this.teleport(location, TeleportCause.PLUGIN);
 diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/spigotmc/WatchdogThread.java
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
 --- a/src/main/java/org/spigotmc/WatchdogThread.java
 +++ b/src/main/java/org/spigotmc/WatchdogThread.java
 @@ -0,0 +0,0 @@ public class WatchdogThread extends Thread
-                 // Paper end - Different message for short timeout
+                 //
                  log.log( Level.SEVERE, "------------------------------" );
-                 log.log( Level.SEVERE, "Server thread dump (Look for plugins here before reporting to Paper!):" ); // Paper
+                 log.log( Level.SEVERE, "Server thread dump (Look for plugins here before reporting to Spigot!):" );
 +                com.destroystokyo.paper.io.chunk.ChunkTaskManager.dumpAllChunkLoadInfo(); // Paper
                  WatchdogThread.dumpThread( ManagementFactory.getThreadMXBean().getThreadInfo( MinecraftServer.getServer().serverThread.getId(), Integer.MAX_VALUE ), log );
                  log.log( Level.SEVERE, "------------------------------" );
diff --git a/patches/server/Basic-PlayerProfile-API.patch b/patches/server/Basic-PlayerProfile-API.patch
index de9324fede..a2a0f22dbb 100644
--- a/patches/server/Basic-PlayerProfile-API.patch
+++ b/patches/server/Basic-PlayerProfile-API.patch
@@ -564,11 +564,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +import com.destroystokyo.paper.profile.CraftPlayerProfile;
 +import com.destroystokyo.paper.profile.PlayerProfile;
  import com.google.common.util.concurrent.ThreadFactoryBuilder;
- import it.unimi.dsi.fastutil.objects.ObjectRBTreeSet;
- import java.lang.ref.Cleaner;
-@@ -0,0 +0,0 @@ import net.minecraft.world.level.ChunkPos;
- import net.minecraft.world.level.ClipContext;
- import net.minecraft.world.level.Level;
+ import com.google.gson.JsonArray;
+ import com.google.gson.JsonObject;
+@@ -0,0 +0,0 @@ import net.minecraft.world.level.Level;
+ import net.minecraft.world.level.chunk.ChunkAccess;
+ import net.minecraft.world.level.chunk.ChunkStatus;
  import org.apache.commons.lang.exception.ExceptionUtils;
 +import com.mojang.authlib.GameProfile;
  import org.bukkit.Location;
diff --git a/patches/server/Catch-JsonParseException-in-Entity-and-TE-names.patch b/patches/server/Catch-JsonParseException-in-Entity-and-TE-names.patch
index 1d72f29a22..3d91df7fa6 100644
--- a/patches/server/Catch-JsonParseException-in-Entity-and-TE-names.patch
+++ b/patches/server/Catch-JsonParseException-in-Entity-and-TE-names.patch
@@ -22,9 +22,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  import net.minecraft.core.Direction;
 +import net.minecraft.nbt.CompoundTag;
 +import net.minecraft.network.chat.Component;
- import net.minecraft.server.level.ServerLevel;
- import net.minecraft.world.entity.Entity;
- import net.minecraft.world.level.ChunkPos;
+ import net.minecraft.server.level.ChunkHolder;
+ import net.minecraft.server.level.ChunkMap;
+ import net.minecraft.server.level.DistanceManager;
 @@ -0,0 +0,0 @@ public final class MCUtil {
          }
      }
@@ -44,8 +44,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return null;
 +    }
 +
-     public static int getTicketLevelFor(net.minecraft.world.level.chunk.ChunkStatus status) {
-         return net.minecraft.server.level.ChunkMap.MAX_VIEW_DISTANCE + net.minecraft.world.level.chunk.ChunkStatus.getDistance(status);
+     public static ChunkStatus getChunkStatus(ChunkHolder chunk) {
+         return chunk.getChunkHolderStatus();
      }
 diff --git a/src/main/java/net/minecraft/world/level/BaseCommandBlock.java b/src/main/java/net/minecraft/world/level/BaseCommandBlock.java
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
diff --git a/patches/server/Chunk-Save-Reattempt.patch b/patches/server/Chunk-Save-Reattempt.patch
index ad86739e08..708cf94dc6 100644
--- a/patches/server/Chunk-Save-Reattempt.patch
+++ b/patches/server/Chunk-Save-Reattempt.patch
@@ -23,29 +23,29 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 --- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
 +++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
 @@ -0,0 +0,0 @@ public class RegionFileStorage implements AutoCloseable {
- 
      protected void write(ChunkPos pos, @Nullable CompoundTag nbt) throws IOException {
-         RegionFile regionfile = this.getRegionFile(pos, false); // CraftBukkit
+         RegionFile regionfile = this.getRegionFile(pos, false, true); // CraftBukkit // Paper
+         try { // Paper
 +        int attempts = 0; Exception laste = null; while (attempts++ < 5) { try { // Paper
  
          if (nbt == null) {
              regionfile.clear(pos);
 @@ -0,0 +0,0 @@ public class RegionFileStorage implements AutoCloseable {
+                 dataoutputstream.close();
              }
          }
- 
-+            // Paper start
-+            return;
++        // Paper start
++        return;
 +        } catch (Exception ex)  {
 +            laste = ex;
 +        }
 +        }
-+
+ 
 +        if (laste != null) {
 +            com.destroystokyo.paper.exception.ServerInternalException.reportInternalException(laste);
-+            net.minecraft.server.MinecraftServer.LOGGER.error("Failed to save chunk", laste);
++            net.minecraft.server.MinecraftServer.LOGGER.error("Failed to save chunk " + pos, laste);
 +        }
 +        // Paper end
-     }
- 
-     public void close() throws IOException {
+         } finally { // Paper start
+             regionfile.fileLock.unlock();
+         } // Paper end
diff --git a/patches/server/Chunk-debug-command.patch b/patches/server/Chunk-debug-command.patch
index 10da488739..8986463d4c 100644
--- a/patches/server/Chunk-debug-command.patch
+++ b/patches/server/Chunk-debug-command.patch
@@ -148,14 +148,14 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            int ticking = 0;
 +            int entityTicking = 0;
 +
-+            for (final ChunkHolder chunk : world.getChunkSource().chunkMap.updatingChunkMap.values()) {
++            for (final ChunkHolder chunk : net.minecraft.server.ChunkSystem.getVisibleChunkHolders(world)) {
 +                if (chunk.getFullChunkNowUnchecked() == null) {
 +                    continue;
 +                }
 +
 +                ++total;
 +
-+                ChunkHolder.FullChunkStatus state = ChunkHolder.getFullChunkStatus(chunk.getTicketLevel());
++                ChunkHolder.FullChunkStatus state = chunk.getFullStatus();
 +
 +                switch (state) {
 +                    case INACCESSIBLE -> ++inactive;
@@ -226,14 +226,22 @@ diff --git a/src/main/java/net/minecraft/server/MCUtil.java b/src/main/java/net/
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
 --- a/src/main/java/net/minecraft/server/MCUtil.java
 +++ b/src/main/java/net/minecraft/server/MCUtil.java
-@@ -0,0 +0,0 @@ import net.minecraft.core.BlockPos;
+@@ -0,0 +0,0 @@
+ package net.minecraft.server;
+ 
+ import com.google.common.util.concurrent.ThreadFactoryBuilder;
++import com.google.gson.JsonArray;
++import com.google.gson.JsonObject;
++import com.google.gson.internal.Streams;
++import com.google.gson.stream.JsonWriter;
++import com.mojang.datafixers.util.Either;
+ import it.unimi.dsi.fastutil.objects.ObjectRBTreeSet;
+ import java.lang.ref.Cleaner;
+ import net.minecraft.core.BlockPos;
  import net.minecraft.core.Direction;
- import net.minecraft.nbt.CompoundTag;
- import net.minecraft.network.chat.Component;
 +import net.minecraft.server.level.ChunkHolder;
 +import net.minecraft.server.level.ChunkMap;
 +import net.minecraft.server.level.DistanceManager;
-+import net.minecraft.server.level.ServerChunkCache;
  import net.minecraft.server.level.ServerLevel;
 +import net.minecraft.server.level.ServerPlayer;
 +import net.minecraft.server.level.Ticket;
@@ -244,22 +252,14 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +import net.minecraft.world.level.chunk.ChunkAccess;
 +import net.minecraft.world.level.chunk.ChunkStatus;
  import org.apache.commons.lang.exception.ExceptionUtils;
-+import com.google.gson.JsonArray;
-+import com.google.gson.JsonObject;
-+import com.google.gson.internal.Streams;
-+import com.google.gson.stream.JsonWriter;
- import com.mojang.authlib.GameProfile;
-+import com.mojang.datafixers.util.Either;
-+import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
  import org.bukkit.Location;
  import org.bukkit.block.BlockFace;
- import org.bukkit.craftbukkit.CraftWorld;
 @@ -0,0 +0,0 @@ import org.spigotmc.AsyncCatcher;
  
  import javax.annotation.Nonnull;
  import javax.annotation.Nullable;
 +import java.io.*;
-+import java.util.ArrayList;
++import java.nio.charset.StandardCharsets;
  import java.util.List;
  import java.util.Queue;
 +import java.util.Set;
@@ -267,19 +267,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  import java.util.concurrent.ExecutionException;
  import java.util.concurrent.LinkedBlockingQueue;
 @@ -0,0 +0,0 @@ public final class MCUtil {
-         return null;
+         }
      }
  
 +    public static ChunkStatus getChunkStatus(ChunkHolder chunk) {
-+        List<ChunkStatus> statuses = net.minecraft.server.level.ServerChunkCache.CHUNK_STATUSES;
-+        for (int i = statuses.size() - 1; i >= 0; --i) {
-+            ChunkStatus curr = statuses.get(i);
-+            CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> future = chunk.getFutureIfPresentUnchecked(curr);
-+            if (future != ChunkHolder.UNLOADED_CHUNK_FUTURE) {
-+                return curr;
-+            }
-+        }
-+        return null; // unloaded
++        return chunk.getChunkHolderStatus();
 +    }
 +
 +    public static void dumpChunks(File file) throws IOException {
@@ -337,9 +329,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +            ServerLevel world = ((org.bukkit.craftbukkit.CraftWorld)bukkitWorld).getHandle();
 +            ChunkMap chunkMap = world.getChunkSource().chunkMap;
-+            Long2ObjectLinkedOpenHashMap<ChunkHolder> visibleChunks = chunkMap.visibleChunkMap;
 +            DistanceManager chunkMapDistance = chunkMap.distanceManager;
-+            List<ChunkHolder> allChunks = new ArrayList<>(visibleChunks.values());
++            List<ChunkHolder> allChunks = net.minecraft.server.ChunkSystem.getVisibleChunkHolders(world);
 +            List<ServerPlayer> players = world.players;
 +
 +            int fullLoadedChunks = 0;
@@ -362,7 +353,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            worldData.addProperty("view-distance", world.spigotConfig.viewDistance);
 +            worldData.addProperty("keep-spawn-loaded", world.keepSpawnInMemory);
 +            worldData.addProperty("keep-spawn-loaded-range", world.paperConfig().spawn.keepSpawnLoadedRange * 16);
-+            worldData.addProperty("visible-chunk-count", visibleChunks.size());
++            worldData.addProperty("visible-chunk-count", allChunks.size());
 +            worldData.addProperty("loaded-chunk-count", chunkMap.entitiesInLevel.size());
 +            worldData.addProperty("verified-fully-loaded-chunks", fullLoadedChunks);
 +
@@ -431,7 +422,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +        String fileData = stringWriter.toString();
 +
-+        try (PrintStream out = new PrintStream(new FileOutputStream(file), false, "UTF-8")) {
++        try (PrintStream out = new PrintStream(new FileOutputStream(file), false, StandardCharsets.UTF_8)) {
 +            out.print(fileData);
 +        }
 +    }
diff --git a/patches/server/ChunkMapDistance-CME.patch b/patches/server/ChunkMapDistance-CME.patch
index 8087fce876..64770b9915 100644
--- a/patches/server/ChunkMapDistance-CME.patch
+++ b/patches/server/ChunkMapDistance-CME.patch
@@ -15,7 +15,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    boolean isUpdateQueued = false; // Paper
      private final ChunkMap chunkMap; // Paper
  
-     public ChunkHolder(ChunkPos pos, int level, LevelHeightAccessor world, LevelLightEngine lightingProvider, ChunkHolder.LevelChangeListener levelUpdateListener, ChunkHolder.PlayerProvider playersWatchingChunkProvider) {
+     // Paper start
 diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
 --- a/src/main/java/net/minecraft/server/level/DistanceManager.java
diff --git a/patches/server/ConcurrentUtil.patch b/patches/server/ConcurrentUtil.patch
new file mode 100644
index 0000000000..9d641d67ba
--- /dev/null
+++ b/patches/server/ConcurrentUtil.patch
@@ -0,0 +1,6864 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Spottedleaf <Spottedleaf@users.noreply.github.com>
+Date: Sun, 23 Jan 2022 22:58:11 -0800
+Subject: [PATCH] ConcurrentUtil
+
+
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/collection/MultiThreadedQueue.java b/src/main/java/ca/spottedleaf/concurrentutil/collection/MultiThreadedQueue.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/collection/MultiThreadedQueue.java
+@@ -0,0 +0,0 @@
++package ca.spottedleaf.concurrentutil.collection;
++
++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
++import ca.spottedleaf.concurrentutil.util.Validate;
++import java.lang.invoke.VarHandle;
++import java.util.ArrayList;
++import java.util.Collection;
++import java.util.Iterator;
++import java.util.List;
++import java.util.NoSuchElementException;
++import java.util.Queue;
++import java.util.Spliterator;
++import java.util.Spliterators;
++import java.util.function.Consumer;
++import java.util.function.IntFunction;
++import java.util.function.Predicate;
++
++/**
++ * MT-Safe linked first in first out ordered queue.
++ *
++ * This queue should out-perform {@link java.util.concurrent.ConcurrentLinkedQueue} in high-contention reads/writes, and is
++ * not any slower in lower contention reads/writes.
++ * <p>
++ * Note that this queue breaks the specification laid out by {@link Collection}, see {@link #preventAdds()} and {@link Collection#add(Object)}.
++ * </p>
++ * <p><b>
++ * This queue will only unlink linked nodes through the {@link #peek()} and {@link #poll()} methods, and this is only if
++ * they are at the head of the queue.
++ * </b></p>
++ * @param <E> Type of element in this queue.
++ */
++public class MultiThreadedQueue<E> implements Queue<E> {
++
++    protected volatile LinkedNode<E> head; /* Always non-null, high chance of being the actual head */
++
++    protected volatile LinkedNode<E> tail; /* Always non-null, high chance of being the actual tail */
++
++    /* Note that it is possible to reach head from tail. */
++
++    /* IMPL NOTE: Leave hashCode and equals to their defaults */
++
++    protected static final VarHandle HEAD_HANDLE = ConcurrentUtil.getVarHandle(MultiThreadedQueue.class, "head", LinkedNode.class);
++    protected static final VarHandle TAIL_HANDLE = ConcurrentUtil.getVarHandle(MultiThreadedQueue.class, "tail", LinkedNode.class);
++
++    /* head */
++
++    protected final void setHeadPlain(final LinkedNode<E> newHead) {
++        HEAD_HANDLE.set(this, newHead);
++    }
++
++    protected final void setHeadOpaque(final LinkedNode<E> newHead) {
++        HEAD_HANDLE.setOpaque(this, newHead);
++    }
++
++    @SuppressWarnings("unchecked")
++    protected final LinkedNode<E> getHeadPlain() {
++        return (LinkedNode<E>)HEAD_HANDLE.get(this);
++    }
++
++    @SuppressWarnings("unchecked")
++    protected final LinkedNode<E> getHeadOpaque() {
++        return (LinkedNode<E>)HEAD_HANDLE.getOpaque(this);
++    }
++
++    @SuppressWarnings("unchecked")
++    protected final LinkedNode<E> getHeadAcquire() {
++        return (LinkedNode<E>)HEAD_HANDLE.getAcquire(this);
++    }
++
++    /* tail */
++
++    protected final void setTailPlain(final LinkedNode<E> newTail) {
++        TAIL_HANDLE.set(this, newTail);
++    }
++
++    protected final void setTailOpaque(final LinkedNode<E> newTail) {
++        TAIL_HANDLE.setOpaque(this, newTail);
++    }
++
++    @SuppressWarnings("unchecked")
++    protected final LinkedNode<E> getTailPlain() {
++        return (LinkedNode<E>)TAIL_HANDLE.get(this);
++    }
++
++    @SuppressWarnings("unchecked")
++    protected final LinkedNode<E> getTailOpaque() {
++        return (LinkedNode<E>)TAIL_HANDLE.getOpaque(this);
++    }
++
++    /**
++     * Constructs a {@code MultiThreadedQueue}, initially empty.
++     * <p>
++     * The returned object may not be published without synchronization.
++     * </p>
++     */
++    public MultiThreadedQueue() {
++        final LinkedNode<E> value = new LinkedNode<>(null, null);
++        this.setHeadPlain(value);
++        this.setTailPlain(value);
++    }
++
++    /**
++     * Constructs a {@code MultiThreadedQueue}, initially containing all elements in the specified {@code collection}.
++     * <p>
++     * The returned object may not be published without synchronization.
++     * </p>
++     * @param collection The specified collection.
++     * @throws NullPointerException If {@code collection} is {@code null} or contains {@code null} elements.
++     */
++    public MultiThreadedQueue(final Iterable<? extends E> collection) {
++        final Iterator<? extends E> elements = collection.iterator();
++
++        if (!elements.hasNext()) {
++            final LinkedNode<E> value = new LinkedNode<>(null, null);
++            this.setHeadPlain(value);
++            this.setTailPlain(value);
++            return;
++        }
++
++        final LinkedNode<E> head = new LinkedNode<>(Validate.notNull(elements.next(), "Null element"), null);
++        LinkedNode<E> tail = head;
++
++        while (elements.hasNext()) {
++            final LinkedNode<E> next = new LinkedNode<>(Validate.notNull(elements.next(), "Null element"), null);
++            tail.setNextPlain(next);
++            tail = next;
++        }
++
++        this.setHeadPlain(head);
++        this.setTailPlain(tail);
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    @Override
++    public E remove() throws NoSuchElementException {
++        final E ret = this.poll();
++
++        if (ret == null) {
++            throw new NoSuchElementException();
++        }
++
++        return ret;
++    }
++
++    /**
++     * {@inheritDoc}
++     * <p>
++     * Contrary to the specification of {@link Collection#add}, this method will fail to add the element to this queue
++     * and return {@code false} if this queue is add-blocked.
++     * </p>
++     */
++    @Override
++    public boolean add(final E element) {
++        return this.offer(element);
++    }
++
++    /**
++     * Adds the specified element to the tail of this queue. If this queue is currently add-locked, then the queue is
++     * released from that lock and this element is added. The unlock operation and addition of the specified
++     * element is atomic.
++     * @param element The specified element.
++     * @return {@code true} if this queue previously allowed additions
++     */
++    public boolean forceAdd(final E element) {
++        final LinkedNode<E> node = new LinkedNode<>(element, null);
++
++        return !this.forceAppendList(node, node);
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    @Override
++    public E element() throws NoSuchElementException {
++        final E ret = this.peek();
++
++        if (ret == null) {
++            throw new NoSuchElementException();
++        }
++
++        return ret;
++    }
++
++    /**
++     * {@inheritDoc}
++     * <p>
++     * This method may also return {@code false} to indicate an element was not added if this queue is add-blocked.
++     * </p>
++     */
++    @Override
++    public boolean offer(final E element) {
++        Validate.notNull(element, "Null element");
++
++        final LinkedNode<E> node = new LinkedNode<>(element, null);
++
++        return this.appendList(node, node);
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    @Override
++    public E peek() {
++        for (LinkedNode<E> head = this.getHeadOpaque(), curr = head;;) {
++            final LinkedNode<E> next = curr.getNextVolatile();
++            final E element = curr.getElementPlain(); /* Likely in sync */
++
++            if (element != null) {
++                if (this.getHeadOpaque() == head && curr != head) {
++                    this.setHeadOpaque(curr);
++                }
++                return element;
++            }
++
++            if (next == null || curr == next) {
++                return null;
++            }
++            curr = next;
++        }
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    @Override
++    public E poll() {
++        return this.removeHead();
++    }
++
++    /**
++     * Retrieves and removes the head of this queue if it matches the specified predicate. If this queue is empty
++     * or the head does not match the predicate, this function returns {@code null}.
++     * <p>
++     * The predicate may be invoked multiple or no times in this call.
++     * </p>
++     * @param predicate The specified predicate.
++     * @return The head if it matches the predicate, or {@code null} if it did not or this queue is empty.
++     */
++    public E pollIf(final Predicate<E> predicate) {
++        return this.removeHead(Validate.notNull(predicate, "Null predicate"));
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    @Override
++    public void clear() {
++        //noinspection StatementWithEmptyBody
++        while (this.poll() != null);
++    }
++
++    /**
++     * Prevents elements from being added to this queue. Once this is called, any attempt to add to this queue will fail.
++     * <p>
++     * This function is MT-Safe.
++     * </p>
++     * @return {@code true} if the queue was modified to prevent additions, {@code false} if it already prevented additions.
++     */
++    public boolean preventAdds() {
++        final LinkedNode<E> deadEnd = new LinkedNode<>(null, null);
++        deadEnd.setNextPlain(deadEnd);
++
++        if (!this.appendList(deadEnd, deadEnd)) {
++            return false;
++        }
++
++        this.setTailPlain(deadEnd); /* (try to) Ensure tail is set for the following #allowAdds call */
++        return true;
++    }
++
++    /**
++     * Allows elements to be added to this queue once again. Note that this function has undefined behaviour if
++     * {@link #preventAdds()} is not called beforehand. The benefit of this function over {@link #tryAllowAdds()}
++     * is that this function might perform better.
++     * <p>
++     * This function is not MT-Safe.
++     * </p>
++     */
++    public void allowAdds() {
++        LinkedNode<E> tail = this.getTailPlain();
++
++        /* We need to find the tail given the cas on tail isn't atomic (nor volatile) in this.appendList */
++        /* Thus it is possible for an outdated tail to be set */
++        while (tail != (tail = tail.getNextPlain())) {}
++
++        tail.setNextVolatile(null);
++    }
++
++    /**
++     * Tries to allow elements to be added to this queue. Returns {@code true} if the queue was previous add-locked,
++     * {@code false} otherwise.
++     * <p>
++     * This function is MT-Safe, however it should not be used with {@link #allowAdds()}.
++     * </p>
++     * @return {@code true} if the queue was previously add-locked, {@code false} otherwise.
++     */
++    public boolean tryAllowAdds() {
++        LinkedNode<E> tail = this.getTailPlain();
++
++        for (int failures = 0;;) {
++            /* We need to find the tail given the cas on tail isn't atomic (nor volatile) in this.appendList */
++            /* Thus it is possible for an outdated tail to be set */
++            while (tail != (tail = tail.getNextAcquire())) {
++                if (tail == null) {
++                    return false;
++                }
++            }
++
++            for (int i = 0; i < failures; ++i) {
++                ConcurrentUtil.backoff();
++            }
++
++            if (tail == (tail = tail.compareExchangeNextVolatile(tail, null))) {
++                return true;
++            }
++
++            if (tail == null) {
++                return false;
++            }
++            ++failures;
++        }
++    }
++
++    /**
++     * Atomically adds the specified element to this queue or allows additions to the queue. If additions
++     * are not allowed, the element is not added.
++     * <p>
++     * This function is MT-Safe.
++     * </p>
++     * @param element The specified element.
++     * @return {@code true} if the queue now allows additions, {@code false} if the element was added.
++     */
++    public boolean addOrAllowAdds(final E element) {
++        Validate.notNull(element, "Null element");
++        int failures = 0;
++
++        final LinkedNode<E> append = new LinkedNode<>(element, null);
++
++        for (LinkedNode<E> currTail = this.getTailOpaque(), curr = currTail;;) {
++            /* It has been experimentally shown that placing the read before the backoff results in significantly greater performance */
++            /* It is likely due to a cache miss caused by another write to the next field */
++            final LinkedNode<E> next = curr.getNextVolatile();
++
++            for (int i = 0; i < failures; ++i) {
++                ConcurrentUtil.backoff();
++            }
++
++            if (next == null) {
++                final LinkedNode<E> compared = curr.compareExchangeNextVolatile(null, append);
++
++                if (compared == null) {
++                    /* Added */
++                    /* Avoid CASing on tail more than we need to */
++                    /* CAS to avoid setting an out-of-date tail */
++                    if (this.getTailOpaque() == currTail) {
++                        this.setTailOpaque(append);
++                    }
++                    return false; // we added
++                }
++
++                ++failures;
++                curr = compared;
++                continue;
++            } else if (next == curr) {
++                final LinkedNode<E> compared = curr.compareExchangeNextVolatile(curr, null);
++
++                if (compared == curr) {
++                    return true; // we let additions through
++                }
++
++                ++failures;
++
++                if (compared != null) {
++                    curr = compared;
++                }
++                continue;
++            }
++
++            if (curr == currTail) {
++                /* Tail is likely not up-to-date */
++                curr = next;
++            } else {
++                /* Try to update to tail */
++                if (currTail == (currTail = this.getTailOpaque())) {
++                    curr = next;
++                } else {
++                    curr = currTail;
++                }
++            }
++        }
++    }
++
++    /**
++     * Atomically removes the head from this queue if it exists, otherwise prevents additions to this queue if no
++     * head is removed.
++     * <p>
++     * This function is MT-Safe.
++     * </p>
++     * If the queue is already add-blocked and empty then no operation is performed.
++     * @return {@code null} if the queue is now add-blocked or was previously add-blocked, else returns
++     * an non-null value which was the previous head of queue.
++     */
++    public E pollOrBlockAdds() {
++        int failures = 0;
++        for (LinkedNode<E> head = this.getHeadOpaque(), curr = head;;) {
++            final E currentVal = curr.getElementVolatile();
++            final LinkedNode<E> next = curr.getNextOpaque();
++
++            if (next == curr) {
++                return null; /* Additions are already blocked */
++            }
++
++            for (int i = 0; i < failures; ++i) {
++                ConcurrentUtil.backoff();
++            }
++
++            if (currentVal != null) {
++                if (curr.getAndSetElementVolatile(null) == null) {
++                    ++failures;
++                    continue;
++                }
++
++                /* "CAS" to avoid setting an out-of-date head */
++                if (this.getHeadOpaque() == head) {
++                    this.setHeadOpaque(next != null ? next : curr);
++                }
++
++                return currentVal;
++            }
++
++            if (next == null) {
++                /* Try to update stale head */
++                if (curr != head && this.getHeadOpaque() == head) {
++                    this.setHeadOpaque(curr);
++                }
++
++                final LinkedNode<E> compared = curr.compareExchangeNextVolatile(null, curr);
++
++                if (compared != null) {
++                    // failed to block additions
++                    curr = compared;
++                    ++failures;
++                    continue;
++                }
++
++                return null; /* We blocked additions */
++            }
++
++            if (head == curr) {
++                /* head is likely not up-to-date */
++                curr = next;
++            } else {
++                /* Try to update to head */
++                if (head == (head = this.getHeadOpaque())) {
++                    curr = next;
++                } else {
++                    curr = head;
++                }
++            }
++        }
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    @Override
++    public boolean remove(final Object object) {
++        Validate.notNull(object, "Null object to remove");
++
++        for (LinkedNode<E> curr = this.getHeadOpaque();;) {
++            final LinkedNode<E> next = curr.getNextVolatile();
++            final E element = curr.getElementPlain(); /* Likely in sync */
++
++            if (element != null) {
++                if ((element == object || element.equals(object)) && curr.getAndSetElementVolatile(null) == element) {
++                    return true;
++                }
++            }
++
++            if (next == curr || next == null) {
++                break;
++            }
++            curr = next;
++        }
++
++        return false;
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    @Override
++    public boolean removeIf(final Predicate<? super E> filter) {
++        Validate.notNull(filter, "Null filter");
++
++        boolean ret = false;
++
++        for (LinkedNode<E> curr = this.getHeadOpaque();;) {
++            final LinkedNode<E> next = curr.getNextVolatile();
++            final E element = curr.getElementPlain(); /* Likely in sync */
++
++            if (element != null) {
++                ret |= filter.test(element) && curr.getAndSetElementVolatile(null) == element;
++            }
++
++            if (next == null || next == curr) {
++                break;
++            }
++            curr = next;
++        }
++
++        return ret;
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    @Override
++    public boolean removeAll(final Collection<?> collection) {
++        Validate.notNull(collection, "Null collection");
++
++        boolean ret = false;
++
++        /* Volatile is required to synchronize with the write to the first element */
++        for (LinkedNode<E> curr = this.getHeadOpaque();;) {
++            final LinkedNode<E> next = curr.getNextVolatile();
++            final E element = curr.getElementPlain(); /* Likely in sync */
++
++            if (element != null) {
++                ret |= collection.contains(element) && curr.getAndSetElementVolatile(null) == element;
++            }
++
++            if (next == null || next == curr) {
++                break;
++            }
++            curr = next;
++        }
++
++        return ret;
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    @Override
++    public boolean retainAll(final Collection<?> collection) {
++        Validate.notNull(collection, "Null collection");
++
++        boolean ret = false;
++
++        for (LinkedNode<E> curr = this.getHeadOpaque();;) {
++            final LinkedNode<E> next = curr.getNextVolatile();
++            final E element = curr.getElementPlain(); /* Likely in sync */
++
++            if (element != null) {
++                ret |= !collection.contains(element) && curr.getAndSetElementVolatile(null) == element;
++            }
++
++            if (next == null || next == curr) {
++                break;
++            }
++            curr = next;
++        }
++
++        return ret;
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    @Override
++    public Object[] toArray() {
++        final List<E> ret = new ArrayList<>();
++
++        for (LinkedNode<E> curr = this.getHeadOpaque();;) {
++            final LinkedNode<E> next = curr.getNextVolatile();
++            final E element = curr.getElementPlain(); /* Likely in sync */
++
++            if (element != null) {
++                ret.add(element);
++            }
++
++            if (next == null || next == curr) {
++                break;
++            }
++            curr = next;
++        }
++
++        return ret.toArray();
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    @Override
++    public <T> T[] toArray(final T[] array) {
++        final List<T> ret = new ArrayList<>();
++
++        for (LinkedNode<E> curr = this.getHeadOpaque();;) {
++            final LinkedNode<E> next = curr.getNextVolatile();
++            final E element = curr.getElementPlain(); /* Likely in sync */
++
++            if (element != null) {
++                //noinspection unchecked
++                ret.add((T)element);
++            }
++
++            if (next == null || next == curr) {
++                break;
++            }
++            curr = next;
++        }
++
++        return ret.toArray(array);
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    @Override
++    public <T> T[] toArray(final IntFunction<T[]> generator) {
++        Validate.notNull(generator, "Null generator");
++
++        final List<T> ret = new ArrayList<>();
++
++        for (LinkedNode<E> curr = this.getHeadOpaque();;) {
++            final LinkedNode<E> next = curr.getNextVolatile();
++            final E element = curr.getElementPlain(); /* Likely in sync */
++
++            if (element != null) {
++                //noinspection unchecked
++                ret.add((T)element);
++            }
++
++            if (next == null || next == curr) {
++                break;
++            }
++            curr = next;
++        }
++
++        return ret.toArray(generator);
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    @Override
++    public String toString() {
++        final StringBuilder builder = new StringBuilder();
++
++        builder.append("MultiThreadedQueue: {elements: {");
++
++        int deadEntries = 0;
++        int totalEntries = 0;
++        int aliveEntries = 0;
++
++        boolean addLocked = false;
++
++        for (LinkedNode<E> curr = this.getHeadOpaque();; ++totalEntries) {
++            final LinkedNode<E> next = curr.getNextVolatile();
++            final E element = curr.getElementPlain(); /* Likely in sync */
++
++            if (element == null) {
++                ++deadEntries;
++            } else {
++                ++aliveEntries;
++            }
++
++            if (totalEntries != 0) {
++                builder.append(", ");
++            }
++
++            builder.append(totalEntries).append(": \"").append(element).append('"');
++
++            if (next == null) {
++                break;
++            }
++            if (curr == next) {
++                addLocked = true;
++                break;
++            }
++            curr = next;
++        }
++
++        builder.append("}, total_entries: \"").append(totalEntries).append("\", alive_entries: \"").append(aliveEntries)
++            .append("\", dead_entries:").append(deadEntries).append("\", add_locked: \"").append(addLocked)
++            .append("\"}");
++
++        return builder.toString();
++    }
++
++    /**
++     * Adds all elements from the specified collection to this queue. The addition is atomic.
++     * @param collection The specified collection.
++     * @return {@code true} if all elements were added successfully, or {@code false} if this queue is add-blocked, or
++     * {@code false} if the specified collection contains no elements.
++     */
++    @Override
++    public boolean addAll(final Collection<? extends E> collection) {
++        return this.addAll((Iterable<? extends E>)collection);
++    }
++
++    /**
++     * Adds all elements from the specified iterable object to this queue. The addition is atomic.
++     * @param iterable The specified iterable object.
++     * @return {@code true} if all elements were added successfully, or {@code false} if this queue is add-blocked, or
++     * {@code false} if the specified iterable contains no elements.
++     */
++    public boolean addAll(final Iterable<? extends E> iterable) {
++        Validate.notNull(iterable, "Null iterable");
++
++        final Iterator<? extends E> elements = iterable.iterator();
++        if (!elements.hasNext()) {
++            return false;
++        }
++
++        /* Build a list of nodes to append */
++        /* This is an much faster due to the fact that zero additional synchronization is performed */
++
++        final LinkedNode<E> head = new LinkedNode<>(Validate.notNull(elements.next(), "Null element"), null);
++        LinkedNode<E> tail = head;
++
++        while (elements.hasNext()) {
++            final LinkedNode<E> next = new LinkedNode<>(Validate.notNull(elements.next(), "Null element"), null);
++            tail.setNextPlain(next);
++            tail = next;
++        }
++
++        return this.appendList(head, tail);
++    }
++
++    /**
++     * Adds all of the elements from the specified array to this queue.
++     * @param items The specified array.
++     * @return {@code true} if all elements were added successfully, or {@code false} if this queue is add-blocked, or
++     * {@code false} if the specified array has a length of 0.
++     */
++    public boolean addAll(final E[] items) {
++        return this.addAll(items, 0, items.length);
++    }
++
++    /**
++     * Adds all of the elements from the specified array to this queue.
++     * @param items The specified array.
++     * @param off The offset in the array.
++     * @param len The number of items.
++     * @return {@code true} if all elements were added successfully, or {@code false} if this queue is add-blocked, or
++     * {@code false} if the specified array has a length of 0.
++     */
++    public boolean addAll(final E[] items, final int off, final int len) {
++        Validate.notNull(items, "Items may not be null");
++        Validate.arrayBounds(off, len, items.length, "Items array indices out of bounds");
++
++        if (len == 0) {
++            return false;
++        }
++
++        final LinkedNode<E> head = new LinkedNode<>(Validate.notNull(items[off], "Null element"), null);
++        LinkedNode<E> tail = head;
++
++        for (int i = 1; i < len; ++i) {
++            final LinkedNode<E> next = new LinkedNode<>(Validate.notNull(items[off + i], "Null element"), null);
++            tail.setNextPlain(next);
++            tail = next;
++        }
++
++        return this.appendList(head, tail);
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    @Override
++    public boolean containsAll(final Collection<?> collection) {
++        Validate.notNull(collection, "Null collection");
++
++        for (final Object element : collection) {
++            if (!this.contains(element)) {
++                return false;
++            }
++        }
++        return false;
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    @Override
++    public Iterator<E> iterator() {
++        return new LinkedIterator<>(this.getHeadOpaque());
++    }
++
++    /**
++     * {@inheritDoc}
++     * <p>
++     * Note that this function is computed non-atomically and in O(n) time. The value returned may not be representative of
++     * the queue in its current state.
++     * </p>
++     */
++    @Override
++    public int size() {
++        int size = 0;
++
++        /* Volatile is required to synchronize with the write to the first element */
++        for (LinkedNode<E> curr = this.getHeadOpaque();;) {
++            final LinkedNode<E> next = curr.getNextVolatile();
++            final E element = curr.getElementPlain(); /* Likely in sync */
++
++            if (element != null) {
++                ++size;
++            }
++
++            if (next == null || next == curr) {
++                break;
++            }
++            curr = next;
++        }
++
++        return size;
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    @Override
++    public boolean isEmpty() {
++        return this.peek() == null;
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    @Override
++    public boolean contains(final Object object) {
++        Validate.notNull(object, "Null object");
++
++        for (LinkedNode<E> curr = this.getHeadOpaque();;) {
++            final LinkedNode<E> next = curr.getNextVolatile();
++            final E element = curr.getElementPlain(); /* Likely in sync */
++
++            if (element != null && (element == object || element.equals(object))) {
++                return true;
++            }
++
++            if (next == null || next == curr) {
++                break;
++            }
++            curr = next;
++        }
++
++        return false;
++    }
++
++    /**
++     * Finds the first element in this queue that matches the predicate.
++     * @param predicate The predicate to test elements against.
++     * @return The first element that matched the predicate, {@code null} if none matched.
++     */
++    public E find(final Predicate<E> predicate) {
++        Validate.notNull(predicate, "Null predicate");
++
++        for (LinkedNode<E> curr = this.getHeadOpaque();;) {
++            final LinkedNode<E> next = curr.getNextVolatile();
++            final E element = curr.getElementPlain(); /* Likely in sync */
++
++            if (element != null && predicate.test(element)) {
++                return element;
++            }
++
++            if (next == null || next == curr) {
++                break;
++            }
++            curr = next;
++        }
++
++        return null;
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    @Override
++    public void forEach(final Consumer<? super E> action) {
++        Validate.notNull(action, "Null action");
++
++        for (LinkedNode<E> curr = this.getHeadOpaque();;) {
++            final LinkedNode<E> next = curr.getNextVolatile();
++            final E element = curr.getElementPlain(); /* Likely in sync */
++
++            if (element != null) {
++                action.accept(element);
++            }
++
++            if (next == null || next == curr) {
++                break;
++            }
++            curr = next;
++        }
++    }
++
++    // return true if normal addition, false if the queue previously disallowed additions
++    protected final boolean forceAppendList(final LinkedNode<E> head, final LinkedNode<E> tail) {
++        int failures = 0;
++
++        for (LinkedNode<E> currTail = this.getTailOpaque(), curr = currTail;;) {
++            /* It has been experimentally shown that placing the read before the backoff results in significantly greater performance */
++            /* It is likely due to a cache miss caused by another write to the next field */
++            final LinkedNode<E> next = curr.getNextVolatile();
++
++            for (int i = 0; i < failures; ++i) {
++                ConcurrentUtil.backoff();
++            }
++
++            if (next == null || next == curr) {
++                final LinkedNode<E> compared = curr.compareExchangeNextVolatile(next, head);
++
++                if (compared == next) {
++                    /* Added */
++                    /* Avoid CASing on tail more than we need to */
++                    /* "CAS" to avoid setting an out-of-date tail */
++                    if (this.getTailOpaque() == currTail) {
++                        this.setTailOpaque(tail);
++                    }
++                    return next != curr;
++                }
++
++                ++failures;
++                curr = compared;
++                continue;
++            }
++
++            if (curr == currTail) {
++                /* Tail is likely not up-to-date */
++                curr = next;
++            } else {
++                /* Try to update to tail */
++                if (currTail == (currTail = this.getTailOpaque())) {
++                    curr = next;
++                } else {
++                    curr = currTail;
++                }
++            }
++        }
++    }
++
++    // return true if successful, false otherwise
++    protected final boolean appendList(final LinkedNode<E> head, final LinkedNode<E> tail) {
++        int failures = 0;
++
++        for (LinkedNode<E> currTail = this.getTailOpaque(), curr = currTail;;) {
++            /* It has been experimentally shown that placing the read before the backoff results in significantly greater performance */
++            /* It is likely due to a cache miss caused by another write to the next field */
++            final LinkedNode<E> next = curr.getNextVolatile();
++
++            if (next == curr) {
++                /* Additions are stopped */
++                return false;
++            }
++
++            for (int i = 0; i < failures; ++i) {
++                ConcurrentUtil.backoff();
++            }
++
++            if (next == null) {
++                final LinkedNode<E> compared = curr.compareExchangeNextVolatile(null, head);
++
++                if (compared == null) {
++                    /* Added */
++                    /* Avoid CASing on tail more than we need to */
++                    /* CAS to avoid setting an out-of-date tail */
++                    if (this.getTailOpaque() == currTail) {
++                        this.setTailOpaque(tail);
++                    }
++                    return true;
++                }
++
++                ++failures;
++                curr = compared;
++                continue;
++            }
++
++            if (curr == currTail) {
++                /* Tail is likely not up-to-date */
++                curr = next;
++            } else {
++                /* Try to update to tail */
++                if (currTail == (currTail = this.getTailOpaque())) {
++                    curr = next;
++                } else {
++                    curr = currTail;
++                }
++            }
++        }
++    }
++
++    protected final E removeHead(final Predicate<E> predicate) {
++        int failures = 0;
++        for (LinkedNode<E> head = this.getHeadOpaque(), curr = head;;) {
++            // volatile here synchronizes-with writes to element
++            final LinkedNode<E> next = curr.getNextVolatile();
++            final E currentVal = curr.getElementPlain();
++
++            for (int i = 0; i < failures; ++i) {
++                ConcurrentUtil.backoff();
++            }
++
++            if (currentVal != null) {
++                if (!predicate.test(currentVal)) {
++                    /* Try to update stale head */
++                    if (curr != head && this.getHeadOpaque() == head) {
++                        this.setHeadOpaque(curr);
++                    }
++                    return null;
++                }
++                if (curr.getAndSetElementVolatile(null) == null) {
++                    /* Failed to get head */
++                    if (curr == (curr = next) || next == null) {
++                        return null;
++                    }
++                    ++failures;
++                    continue;
++                }
++
++                /* "CAS" to avoid setting an out-of-date head */
++                if (this.getHeadOpaque() == head) {
++                    this.setHeadOpaque(next != null ? next : curr);
++                }
++
++                return currentVal;
++            }
++
++            if (curr == next || next == null) {
++                /* Try to update stale head */
++                if (curr != head && this.getHeadOpaque() == head) {
++                    this.setHeadOpaque(curr);
++                }
++                return null; /* End of queue */
++            }
++
++            if (head == curr) {
++                /* head is likely not up-to-date */
++                curr = next;
++            } else {
++                /* Try to update to head */
++                if (head == (head = this.getHeadOpaque())) {
++                    curr = next;
++                } else {
++                    curr = head;
++                }
++            }
++        }
++    }
++
++    protected final E removeHead() {
++        int failures = 0;
++        for (LinkedNode<E> head = this.getHeadOpaque(), curr = head;;) {
++            final LinkedNode<E> next = curr.getNextVolatile();
++            final E currentVal = curr.getElementPlain();
++
++            for (int i = 0; i < failures; ++i) {
++                ConcurrentUtil.backoff();
++            }
++
++            if (currentVal != null) {
++                if (curr.getAndSetElementVolatile(null) == null) {
++                    /* Failed to get head */
++                    if (curr == (curr = next) || next == null) {
++                        return null;
++                    }
++                    ++failures;
++                    continue;
++                }
++
++                /* "CAS" to avoid setting an out-of-date head */
++                if (this.getHeadOpaque() == head) {
++                    this.setHeadOpaque(next != null ? next : curr);
++                }
++
++                return currentVal;
++            }
++
++            if (curr == next || next == null) {
++                /* Try to update stale head */
++                if (curr != head && this.getHeadOpaque() == head) {
++                    this.setHeadOpaque(curr);
++                }
++                return null; /* End of queue */
++            }
++
++            if (head == curr) {
++                /* head is likely not up-to-date */
++                curr = next;
++            } else {
++                /* Try to update to head */
++                if (head == (head = this.getHeadOpaque())) {
++                    curr = next;
++                } else {
++                    curr = head;
++                }
++            }
++        }
++    }
++
++    /**
++     * Empties the queue into the specified consumer. This function is optimized for single-threaded reads, and should
++     * be faster than a loop on {@link #poll()}.
++     * <p>
++     * This function is not MT-Safe. This function cannot be called with other read operations ({@link #peek()}, {@link #poll()},
++     * {@link #clear()}, etc).
++     * Write operations are safe to be called concurrently.
++     * </p>
++     * @param consumer The consumer to accept the elements.
++     * @return The total number of elements drained.
++     */
++    public int drain(final Consumer<E> consumer) {
++        return this.drain(consumer, false, ConcurrentUtil::rethrow);
++    }
++
++    /**
++     * Empties the queue into the specified consumer. This function is optimized for single-threaded reads, and should
++     * be faster than a loop on {@link #poll()}.
++     * <p>
++     * If {@code preventAdds} is {@code true}, then after this function returns the queue is guaranteed to be empty and
++     * additions to the queue will fail.
++     * </p>
++     * <p>
++     * This function is not MT-Safe. This function cannot be called with other read operations ({@link #peek()}, {@link #poll()},
++     * {@link #clear()}, etc).
++     * Write operations are safe to be called concurrently.
++     * </p>
++     * @param consumer The consumer to accept the elements.
++     * @param preventAdds Whether to prevent additions to this queue after draining.
++     * @return The total number of elements drained.
++     */
++    public int drain(final Consumer<E> consumer, final boolean preventAdds) {
++        return this.drain(consumer, preventAdds, ConcurrentUtil::rethrow);
++    }
++
++    /**
++     * Empties the queue into the specified consumer. This function is optimized for single-threaded reads, and should
++     * be faster than a loop on {@link #poll()}.
++     * <p>
++     * If {@code preventAdds} is {@code true}, then after this function returns the queue is guaranteed to be empty and
++     * additions to the queue will fail.
++     * </p>
++     * <p>
++     * This function is not MT-Safe. This function cannot be called with other read operations ({@link #peek()}, {@link #poll()},
++     * {@link #clear()}, {@link #remove(Object)} etc).
++     * Only write operations are safe to be called concurrently.
++     * </p>
++     * @param consumer The consumer to accept the elements.
++     * @param preventAdds Whether to prevent additions to this queue after draining.
++     * @param exceptionHandler Invoked when the consumer raises an exception.
++     * @return The total number of elements drained.
++     */
++    public int drain(final Consumer<E> consumer, final boolean preventAdds, final Consumer<Throwable> exceptionHandler) {
++        Validate.notNull(consumer, "Null consumer");
++        Validate.notNull(exceptionHandler, "Null exception handler");
++
++        /* This function assumes proper synchronization is made to ensure drain and no other read function are called concurrently */
++        /* This allows plain write usages instead of opaque or higher */
++        int total = 0;
++
++        final LinkedNode<E> head = this.getHeadAcquire(); /* Required to synchronize with the write to the first element field */
++        LinkedNode<E> curr = head;
++
++        for (;;) {
++            /* Volatile acquires with the write to the element field */
++            final E currentVal = curr.getElementPlain();
++            LinkedNode<E> next = curr.getNextVolatile();
++
++            if (next == curr) {
++                /* Add-locked nodes always have a null value */
++                break;
++            }
++
++            if (currentVal == null) {
++                if (next == null) {
++                    if (preventAdds && (next = curr.compareExchangeNextVolatile(null, curr)) != null) {
++                        // failed to prevent adds, continue
++                        curr = next;
++                        continue;
++                    } else {
++                        // we're done here
++                        break;
++                    }
++                }
++                curr = next;
++                continue;
++            }
++
++            try {
++                consumer.accept(currentVal);
++            } catch (final Exception ex) {
++                this.setHeadOpaque(next != null ? next : curr); /* Avoid perf penalty (of reiterating) if the exception handler decides to re-throw */
++                curr.setElementOpaque(null); /* set here, we might re-throw */
++
++                exceptionHandler.accept(ex);
++            }
++
++            curr.setElementOpaque(null);
++
++            ++total;
++
++            if (next == null) {
++                if (preventAdds && (next = curr.compareExchangeNextVolatile(null, curr)) != null) {
++                    /* Retry with next value */
++                    curr = next;
++                    continue;
++                }
++                break;
++            }
++
++            curr = next;
++        }
++        if (curr != head) {
++            this.setHeadOpaque(curr); /* While this may be a plain write, eventually publish it for methods such as find. */
++        }
++        return total;
++    }
++
++    @Override
++    public Spliterator<E> spliterator() { // TODO implement
++        return Spliterators.spliterator(this, Spliterator.CONCURRENT |
++            Spliterator.NONNULL | Spliterator.ORDERED);
++    }
++
++    protected static final class LinkedNode<E> {
++
++        protected volatile Object element;
++        protected volatile LinkedNode<E> next;
++
++        protected static final VarHandle ELEMENT_HANDLE = ConcurrentUtil.getVarHandle(LinkedNode.class, "element", Object.class);
++        protected static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(LinkedNode.class, "next", LinkedNode.class);
++
++        protected LinkedNode(final Object element, final LinkedNode<E> next) {
++            ELEMENT_HANDLE.set(this, element);
++            NEXT_HANDLE.set(this, next);
++        }
++
++        /* element */
++
++        @SuppressWarnings("unchecked")
++        protected final E getElementPlain() {
++            return (E)ELEMENT_HANDLE.get(this);
++        }
++
++        @SuppressWarnings("unchecked")
++        protected final E getElementVolatile() {
++            return (E)ELEMENT_HANDLE.getVolatile(this);
++        }
++
++        protected final void setElementPlain(final E update) {
++            ELEMENT_HANDLE.set(this, (Object)update);
++        }
++
++        protected final void setElementOpaque(final E update) {
++            ELEMENT_HANDLE.setOpaque(this, (Object)update);
++        }
++
++        protected final void setElementVolatile(final E update) {
++            ELEMENT_HANDLE.setVolatile(this, (Object)update);
++        }
++
++        @SuppressWarnings("unchecked")
++        protected final E getAndSetElementVolatile(final E update) {
++            return (E)ELEMENT_HANDLE.getAndSet(this, update);
++        }
++
++        @SuppressWarnings("unchecked")
++        protected final E compareExchangeElementVolatile(final E expect, final E update) {
++            return (E)ELEMENT_HANDLE.compareAndExchange(this, expect, update);
++        }
++
++        /* next */
++
++        @SuppressWarnings("unchecked")
++        protected final LinkedNode<E> getNextPlain() {
++            return (LinkedNode<E>)NEXT_HANDLE.get(this);
++        }
++
++        @SuppressWarnings("unchecked")
++        protected final LinkedNode<E> getNextOpaque() {
++            return (LinkedNode<E>)NEXT_HANDLE.getOpaque(this);
++        }
++
++        @SuppressWarnings("unchecked")
++        protected final LinkedNode<E> getNextAcquire() {
++            return (LinkedNode<E>)NEXT_HANDLE.getAcquire(this);
++        }
++
++        @SuppressWarnings("unchecked")
++        protected final LinkedNode<E> getNextVolatile() {
++            return (LinkedNode<E>)NEXT_HANDLE.getVolatile(this);
++        }
++
++        protected final void setNextPlain(final LinkedNode<E> next) {
++            NEXT_HANDLE.set(this, next);
++        }
++
++        protected final void setNextVolatile(final LinkedNode<E> next) {
++            NEXT_HANDLE.setVolatile(this, next);
++        }
++
++        @SuppressWarnings("unchecked")
++        protected final LinkedNode<E> compareExchangeNextVolatile(final LinkedNode<E> expect, final LinkedNode<E> set) {
++            return (LinkedNode<E>)NEXT_HANDLE.compareAndExchange(this, expect, set);
++        }
++    }
++
++    protected static final class LinkedIterator<E> implements Iterator<E> {
++
++        protected LinkedNode<E> curr; /* last returned by next() */
++        protected LinkedNode<E> next; /* next to return from next() */
++        protected E nextElement; /* cached to avoid a race condition with removing or polling */
++
++        protected LinkedIterator(final LinkedNode<E> start) {
++            /* setup nextElement and next */
++            for (LinkedNode<E> curr = start;;) {
++                final LinkedNode<E> next = curr.getNextVolatile();
++
++                final E element = curr.getElementPlain();
++
++                if (element != null) {
++                    this.nextElement = element;
++                    this.next = curr;
++                    break;
++                }
++
++                if (next == null || next == curr) {
++                    break;
++                }
++                curr = next;
++            }
++        }
++
++        protected final void findNext() {
++            /* only called if this.nextElement != null, which means this.next != null */
++            for (LinkedNode<E> curr = this.next;;) {
++                final LinkedNode<E> next = curr.getNextVolatile();
++
++                if (next == null || next == curr) {
++                    break;
++                }
++
++                final E element = next.getElementPlain();
++
++                if (element != null) {
++                    this.nextElement = element;
++                    this.curr = this.next; /* this.next will be the value returned from next(), set this.curr for remove() */
++                    this.next = next;
++                    return;
++                }
++                curr = next;
++            }
++
++            /* out of nodes to iterate */
++            /* keep curr for remove() calls */
++            this.next = null;
++            this.nextElement = null;
++        }
++
++        /**
++         * {@inheritDoc}
++         */
++        @Override
++        public boolean hasNext() {
++            return this.nextElement != null;
++        }
++
++        /**
++         * {@inheritDoc}
++         */
++        @Override
++        public E next() {
++            final E element = this.nextElement;
++
++            if (element == null) {
++                throw new NoSuchElementException();
++            }
++
++            this.findNext();
++
++            return element;
++        }
++
++        /**
++         * {@inheritDoc}
++         */
++        @Override
++        public void remove() {
++            if (this.curr == null) {
++                throw new IllegalStateException();
++            }
++
++            this.curr.setElementVolatile(null);
++            this.curr = null;
++        }
++    }
++}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/completable/Completable.java b/src/main/java/ca/spottedleaf/concurrentutil/completable/Completable.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/completable/Completable.java
+@@ -0,0 +0,0 @@
++package ca.spottedleaf.concurrentutil.completable;
++
++import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
++import ca.spottedleaf.concurrentutil.executor.Cancellable;
++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
++import com.mojang.logging.LogUtils;
++import org.slf4j.Logger;
++import java.util.function.BiConsumer;
++
++public final class Completable<T> {
++
++    private static final Logger LOGGER = LogUtils.getLogger();
++
++    private final MultiThreadedQueue<BiConsumer<T, Throwable>> waiters = new MultiThreadedQueue<>();
++    private T result;
++    private Throwable throwable;
++    private volatile boolean completed;
++
++    public boolean isCompleted() {
++        return this.completed;
++    }
++
++    /**
++     * Note: Can only use after calling {@link #addAsynchronousWaiter(BiConsumer)}, as this function performs zero
++     * synchronisation
++     */
++    public T getResult() {
++        return this.result;
++    }
++
++    /**
++     * Note: Can only use after calling {@link #addAsynchronousWaiter(BiConsumer)}, as this function performs zero
++     * synchronisation
++     */
++    public Throwable getThrowable() {
++        return this.throwable;
++    }
++
++    public Cancellable addAsynchronousWaiter(final BiConsumer<T, Throwable> consumer) {
++        if (this.waiters.add(consumer)) {
++            return new CancellableImpl(consumer);
++        }
++        return null;
++    }
++
++    private void completeAllWaiters(final T result, final Throwable throwable) {
++        this.completed = true;
++        BiConsumer<T, Throwable> waiter;
++        while ((waiter = this.waiters.pollOrBlockAdds()) != null) {
++            this.completeWaiter(waiter, result, throwable);
++        }
++    }
++
++    private void completeWaiter(final BiConsumer<T, Throwable> consumer, final T result, final Throwable throwable) {
++        try {
++            consumer.accept(result, throwable);
++        } catch (final ThreadDeath death) {
++            throw death;
++        } catch (final Throwable throwable2) {
++            LOGGER.error("Failed to complete callback " + ConcurrentUtil.genericToString(consumer), throwable2);
++        }
++    }
++
++    public Cancellable addWaiter(final BiConsumer<T, Throwable> consumer) {
++        if (this.waiters.add(consumer)) {
++            return new CancellableImpl(consumer);
++        }
++        this.completeWaiter(consumer, this.result, this.throwable);
++        return new CancellableImpl(consumer);
++    }
++
++    public void complete(final T result) {
++        this.result = result;
++        this.completeAllWaiters(result, null);
++    }
++
++    public void completeWithThrowable(final Throwable throwable) {
++        if (throwable == null) {
++            throw new NullPointerException("Throwable cannot be null");
++        }
++        this.throwable = throwable;
++        this.completeAllWaiters(null, throwable);
++    }
++
++    private final class CancellableImpl implements Cancellable {
++
++        private final BiConsumer<T, Throwable> waiter;
++
++        private CancellableImpl(final BiConsumer<T, Throwable> waiter) {
++            this.waiter = waiter;
++        }
++
++        @Override
++        public boolean cancel() {
++            return Completable.this.waiters.remove(this.waiter);
++        }
++    }
++}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/BaseExecutor.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/BaseExecutor.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/BaseExecutor.java
+@@ -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;
++
++public interface BaseExecutor {
++
++    /**
++     * Returns whether every task scheduled to this queue has been removed and executed or cancelled. If no tasks have been queued,
++     * returns {@code true}.
++     *
++     * @return {@code true} if all tasks that have been queued have finished executing or no tasks have been queued, {@code false} otherwise.
++     */
++    public default boolean haveAllTasksExecuted() {
++        // order is important
++        // if new tasks are scheduled between the reading of these variables, scheduled is guaranteed to be higher -
++        // so our check fails, and we try again
++        final long completed = this.getTotalTasksExecuted();
++        final long scheduled = this.getTotalTasksScheduled();
++
++        return completed == scheduled;
++    }
++
++    /**
++     * Returns the number of tasks that have been scheduled or execute or are pending to be scheduled.
++     */
++    public long getTotalTasksScheduled();
++
++    /**
++     * Returns the number of tasks that have fully been executed.
++     */
++    public long getTotalTasksExecuted();
++
++
++    /**
++     * Waits until this queue has had all of its tasks executed (NOT removed). See {@link #haveAllTasksExecuted()}
++     * <p>
++     *     This call is most effective after a {@link #shutdown()} call, as the shutdown call guarantees no tasks can
++     *     be executed and the waitUntilAllExecuted call makes sure the queue is empty. Effectively, using shutdown then using
++     *     waitUntilAllExecuted ensures this queue is empty - and most importantly, will remain empty.
++     * </p>
++     * <p>
++     *     This method is not guaranteed to be immediately responsive to queue state, so calls may take significantly more
++     *     time than expected. Effectively, do not rely on this call being fast - even if there are few tasks scheduled.
++     * </p>
++     * <p>
++     *     Note: Interruptions to the the current thread have no effect. Interrupt status is also not affected by this cal.
++     * </p>
++     *
++     * @throws IllegalStateException If the current thread is not allowed to wait
++     */
++    public default void waitUntilAllExecuted() throws IllegalStateException {
++        long failures = 9L; // start out at 1ms
++
++        while (!this.haveAllTasksExecuted()) {
++            Thread.yield();
++            failures = ConcurrentUtil.linearLongBackoff(failures, 500_000L, 5_000_000L); // 500us, 5ms
++        }
++    }
++
++    /**
++     * 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
++     */
++    public boolean executeTask() throws IllegalStateException;
++
++    /**
++     * Executes all queued tasks.
++     *
++     * @return {@code true} if a task was executed, {@code false} otherwise
++     * @throws IllegalStateException If the current thread is not allowed to execute a task
++     */
++    public default boolean executeAll() {
++        if (!this.executeTask()) {
++            return false;
++        }
++
++        while (this.executeTask());
++
++        return true;
++    }
++
++    /**
++     * Waits and executes tasks until the condition returns {@code true}.
++     * <p>
++     *     WARNING: This function is <i>not</i> suitable for waiting until a deadline!
++     *     Use {@link #executeUntil(long)} or {@link #executeConditionally(BooleanSupplier, long)} instead.
++     * </p>
++     */
++    public default void executeConditionally(final BooleanSupplier condition) {
++        long failures = 0;
++        while (!condition.getAsBoolean()) {
++            if (this.executeTask()) {
++                failures = failures >>> 2;
++            } else {
++                failures = ConcurrentUtil.linearLongBackoff(failures, 100_000L, 10_000_000L); // 100us, 10ms
++            }
++        }
++    }
++
++    /**
++     * Waits and executes tasks until the condition returns {@code true} or {@code System.nanoTime() >= deadline}.
++     */
++    public default void executeConditionally(final BooleanSupplier condition, final long deadline) {
++        long failures = 0;
++        // double check deadline; we don't know how expensive the condition is
++        while ((System.nanoTime() < deadline) && !condition.getAsBoolean() && (System.nanoTime() < deadline)) {
++            if (this.executeTask()) {
++                failures = failures >>> 2;
++            } else {
++                failures = ConcurrentUtil.linearLongBackoffDeadline(failures, 100_000L, 10_000_000L, deadline); // 100us, 10ms
++            }
++        }
++    }
++
++    /**
++     * Waits and executes tasks until {@code System.nanoTime() >= deadline}.
++     */
++    public default void executeUntil(final long deadline) {
++        long failures = 0;
++        while (System.nanoTime() < deadline) {
++            if (this.executeTask()) {
++                failures = failures >>> 2;
++            } else {
++                failures = ConcurrentUtil.linearLongBackoffDeadline(failures, 100_000L, 10_000_000L, deadline); // 100us, 10ms
++            }
++        }
++    }
++
++    /**
++     * Prevent further additions to this queue. Attempts to add after this call has completed (potentially during) will
++     * result in {@link IllegalStateException} being thrown.
++     * <p>
++     *     This operation is atomic with respect to other shutdown calls
++     * </p>
++     * <p>
++     *     After this call has completed, regardless of return value, this queue will be shutdown.
++     * </p>
++     *
++     * @return {@code true} if the queue was shutdown, {@code false} if it has shut down already
++     * @throws UnsupportedOperationException If this queue does not support shutdown
++     */
++    public default boolean shutdown() throws UnsupportedOperationException {
++        throw new UnsupportedOperationException();
++    }
++
++    /**
++     * Returns whether this queue has shut down. Effectively, whether new tasks will be rejected - this method
++     * does not indicate whether all of the tasks scheduled have been executed.
++     * @return Returns whether this queue has shut down.
++     */
++    public default boolean isShutdown() {
++        return false;
++    }
++
++    public static interface BaseTask extends Cancellable {
++
++        /**
++         * Causes a lazily queued task to become queued or executed
++         *
++         * @throws IllegalStateException If the backing queue has shutdown
++         * @return {@code true} If the task was queued, {@code false} if the task was already queued/cancelled/executed
++         */
++        public boolean queue();
++
++        /**
++         * Forces this task to be marked as completed.
++         *
++         * @return {@code true} if the task was cancelled, {@code false} if the task has already completed or is being completed.
++         */
++        @Override
++        public boolean cancel();
++
++        /**
++         * Executes this task. This will also mark the task as completing.
++         * <p>
++         *     Exceptions thrown from the runnable will be rethrown.
++         * </p>
++         *
++         * @return {@code true} if this task was executed, {@code false} if it was already marked as completed.
++         */
++        public boolean execute();
++    }
++}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/Cancellable.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/Cancellable.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/Cancellable.java
+@@ -0,0 +0,0 @@
++package ca.spottedleaf.concurrentutil.executor;
++
++/**
++ * Interface specifying that something can be cancelled.
++ */
++public interface Cancellable {
++
++    /**
++     * Tries to cancel this task. If the task is in a stage that is too late to be cancelled, then this function
++     * will return {@code false}. If the task is already cancelled, then this function returns {@code false}. Only
++     * when this function successfully stops this task from being completed will it return {@code true}.
++     */
++    public boolean cancel();
++}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/DelayedPrioritisedTask.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/DelayedPrioritisedTask.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/DelayedPrioritisedTask.java
+@@ -0,0 +0,0 @@
++package ca.spottedleaf.concurrentutil.executor.standard;
++
++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
++import java.lang.invoke.VarHandle;
++
++public class DelayedPrioritisedTask {
++
++    protected volatile int priority;
++    protected static final VarHandle PRIORITY_HANDLE = ConcurrentUtil.getVarHandle(DelayedPrioritisedTask.class, "priority", int.class);
++
++    protected static final int PRIORITY_SET = Integer.MIN_VALUE >>> 0;
++
++    protected final int getPriorityVolatile() {
++        return (int)PRIORITY_HANDLE.getVolatile((DelayedPrioritisedTask)this);
++    }
++
++    protected final int compareAndExchangePriorityVolatile(final int expect, final int update) {
++        return (int)PRIORITY_HANDLE.compareAndExchange((DelayedPrioritisedTask)this, (int)expect, (int)update);
++    }
++
++    protected final int getAndOrPriorityVolatile(final int val) {
++        return (int)PRIORITY_HANDLE.getAndBitwiseOr((DelayedPrioritisedTask)this, (int)val);
++    }
++
++    protected final void setPriorityPlain(final int val) {
++        PRIORITY_HANDLE.set((DelayedPrioritisedTask)this, (int)val);
++    }
++
++    protected volatile PrioritisedExecutor.PrioritisedTask task;
++    protected static final VarHandle TASK_HANDLE = ConcurrentUtil.getVarHandle(DelayedPrioritisedTask.class, "task", PrioritisedExecutor.PrioritisedTask.class);
++
++    protected PrioritisedExecutor.PrioritisedTask getTaskPlain() {
++        return (PrioritisedExecutor.PrioritisedTask)TASK_HANDLE.get((DelayedPrioritisedTask)this);
++    }
++
++    protected PrioritisedExecutor.PrioritisedTask getTaskVolatile() {
++        return (PrioritisedExecutor.PrioritisedTask)TASK_HANDLE.getVolatile((DelayedPrioritisedTask)this);
++    }
++
++    protected final PrioritisedExecutor.PrioritisedTask compareAndExchangeTaskVolatile(final PrioritisedExecutor.PrioritisedTask expect, final PrioritisedExecutor.PrioritisedTask update) {
++        return (PrioritisedExecutor.PrioritisedTask)TASK_HANDLE.compareAndExchange((DelayedPrioritisedTask)this, (PrioritisedExecutor.PrioritisedTask)expect, (PrioritisedExecutor.PrioritisedTask)update);
++    }
++
++    public DelayedPrioritisedTask(final PrioritisedExecutor.Priority priority) {
++        this.setPriorityPlain(priority.priority);
++    }
++
++    // only public for debugging
++    public int getPriorityInternal() {
++        return this.getPriorityVolatile();
++    }
++
++    public PrioritisedExecutor.PrioritisedTask getTask() {
++        return this.getTaskVolatile();
++    }
++
++    public void setTask(final PrioritisedExecutor.PrioritisedTask task) {
++        int priority = this.getPriorityVolatile();
++
++        if (this.compareAndExchangeTaskVolatile(null, task) != null) {
++            throw new IllegalStateException("setTask() called twice");
++        }
++
++        int failures = 0;
++        for (;;) {
++            task.setPriority(PrioritisedExecutor.Priority.getPriority(priority));
++
++            if (priority == (priority = this.compareAndExchangePriorityVolatile(priority, priority | PRIORITY_SET))) {
++                return;
++            }
++
++            ++failures;
++            for (int i = 0; i < failures; ++i) {
++                ConcurrentUtil.backoff();
++            }
++        }
++    }
++
++    public PrioritisedExecutor.Priority getPriority() {
++        final int priority = this.getPriorityVolatile();
++        if ((priority & PRIORITY_SET) != 0) {
++            return this.task.getPriority();
++        }
++
++        return PrioritisedExecutor.Priority.getPriority(priority);
++    }
++
++    public void raisePriority(final PrioritisedExecutor.Priority priority) {
++        if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
++            throw new IllegalArgumentException("Invalid priority " + priority);
++        }
++
++        int failures = 0;
++        for (int curr = this.getPriorityVolatile();;) {
++            if ((curr & PRIORITY_SET) != 0) {
++                this.getTaskPlain().raisePriority(priority);
++                return;
++            }
++
++            if (!priority.isLowerPriority(curr)) {
++                return;
++            }
++
++            if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) {
++                return;
++            }
++
++            // failed, retry
++
++            ++failures;
++            for (int i = 0; i < failures; ++i) {
++                ConcurrentUtil.backoff();
++            }
++        }
++    }
++
++    public void setPriority(final PrioritisedExecutor.Priority priority) {
++        if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
++            throw new IllegalArgumentException("Invalid priority " + priority);
++        }
++
++        int failures = 0;
++        for (int curr = this.getPriorityVolatile();;) {
++            if ((curr & PRIORITY_SET) != 0) {
++                this.getTaskPlain().setPriority(priority);
++                return;
++            }
++
++            if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) {
++                return;
++            }
++
++            // failed, retry
++
++            ++failures;
++            for (int i = 0; i < failures; ++i) {
++                ConcurrentUtil.backoff();
++            }
++        }
++    }
++
++    public void lowerPriority(final PrioritisedExecutor.Priority priority) {
++        if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
++            throw new IllegalArgumentException("Invalid priority " + priority);
++        }
++
++        int failures = 0;
++        for (int curr = this.getPriorityVolatile();;) {
++            if ((curr & PRIORITY_SET) != 0) {
++                this.getTaskPlain().lowerPriority(priority);
++                return;
++            }
++
++            if (!priority.isHigherPriority(curr)) {
++                return;
++            }
++
++            if (curr == (curr = this.compareAndExchangePriorityVolatile(curr, priority.priority))) {
++                return;
++            }
++
++            // failed, retry
++
++            ++failures;
++            for (int i = 0; i < failures; ++i) {
++                ConcurrentUtil.backoff();
++            }
++        }
++    }
++}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedExecutor.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedExecutor.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedExecutor.java
+@@ -0,0 +0,0 @@
++package ca.spottedleaf.concurrentutil.executor.standard;
++
++import ca.spottedleaf.concurrentutil.executor.BaseExecutor;
++
++public interface PrioritisedExecutor extends BaseExecutor {
++
++    public static enum Priority {
++
++        /**
++         * Priority value indicating the task has completed or is being completed.
++         * This priority cannot be used to schedule tasks.
++         */
++        COMPLETING(-1),
++
++        /**
++         * Absolute highest priority, should only be used for when a task is blocking a time-critical thread.
++         */
++        BLOCKING(),
++
++        /**
++         * Should only be used for urgent but not time-critical tasks.
++         */
++        HIGHEST(),
++
++        /**
++         * Two priorities above normal.
++         */
++        HIGHER(),
++
++        /**
++         * One priority above normal.
++         */
++        HIGH(),
++
++        /**
++         * Default priority.
++         */
++        NORMAL(),
++
++        /**
++         * One priority below normal.
++         */
++        LOW(),
++
++        /**
++         * Two priorities below normal.
++         */
++        LOWER(),
++
++        /**
++         * Use for tasks that should eventually execute, but are not needed to.
++         */
++        LOWEST(),
++
++        /**
++         * Use for tasks that can be delayed indefinitely.
++         */
++        IDLE();
++
++        // returns whether the priority can be scheduled
++        public static boolean isValidPriority(final Priority priority) {
++            return priority != null && priority != Priority.COMPLETING;
++        }
++
++        // returns the higher priority of the two
++        public static PrioritisedExecutor.Priority max(final Priority p1, final Priority p2) {
++            return p1.isHigherOrEqualPriority(p2) ? p1 : p2;
++        }
++
++        // returns the lower priroity of the two
++        public static PrioritisedExecutor.Priority min(final Priority p1, final Priority p2) {
++            return p1.isLowerOrEqualPriority(p2) ? p1 : p2;
++        }
++
++        public boolean isHigherOrEqualPriority(final Priority than) {
++            return this.priority <= than.priority;
++        }
++
++        public boolean isHigherPriority(final Priority than) {
++            return this.priority < than.priority;
++        }
++
++        public boolean isLowerOrEqualPriority(final Priority than) {
++            return this.priority >= than.priority;
++        }
++
++        public boolean isLowerPriority(final Priority than) {
++            return this.priority > than.priority;
++        }
++
++        public boolean isHigherOrEqualPriority(final int than) {
++            return this.priority <= than;
++        }
++
++        public boolean isHigherPriority(final int than) {
++            return this.priority < than;
++        }
++
++        public boolean isLowerOrEqualPriority(final int than) {
++            return this.priority >= than;
++        }
++
++        public boolean isLowerPriority(final int than) {
++            return this.priority > than;
++        }
++
++        public static boolean isHigherOrEqualPriority(final int priority, final int than) {
++            return priority <= than;
++        }
++
++        public static boolean isHigherPriority(final int priority, final int than) {
++            return priority < than;
++        }
++
++        public static boolean isLowerOrEqualPriority(final int priority, final int than) {
++            return priority >= than;
++        }
++
++        public static boolean isLowerPriority(final int priority, final int than) {
++            return priority > than;
++        }
++
++        static final PrioritisedExecutor.Priority[] PRIORITIES = PrioritisedExecutor.Priority.values();
++
++        /** includes special priorities */
++        public static final int TOTAL_PRIORITIES = PRIORITIES.length;
++
++        public static final int TOTAL_SCHEDULABLE_PRIORITIES = TOTAL_PRIORITIES - 1;
++
++        public static PrioritisedExecutor.Priority getPriority(final int priority) {
++            return PRIORITIES[priority + 1];
++        }
++
++        private static int priorityCounter;
++
++        private static int nextCounter() {
++            return priorityCounter++;
++        }
++
++        public final int priority;
++
++        Priority() {
++            this(nextCounter());
++        }
++
++        Priority(final int priority) {
++            this.priority = priority;
++        }
++    }
++
++    /**
++     * Queues or executes a task at {@link Priority#NORMAL} priority.
++     * @param task The task to run.
++     *
++     * @throws IllegalStateException If this queue has shutdown.
++     * @throws NullPointerException If the task is null
++     * @return {@code null} if the current thread immediately executed the task, else returns the prioritised task
++     * associated with the parameter
++     */
++    public default PrioritisedTask queueRunnable(final Runnable task) {
++        return this.queueRunnable(task, PrioritisedExecutor.Priority.NORMAL);
++    }
++
++    /**
++     * Queues or executes a task.
++     *
++     * @param task The task to run.
++     * @param priority The priority for the task.
++     *
++     * @throws IllegalStateException If this queue has shutdown.
++     * @throws NullPointerException If the task is null
++     * @throws IllegalArgumentException If the priority is invalid.
++     * @return {@code null} if the current thread immediately executed the task, else returns the prioritised task
++     * associated with the parameter
++     */
++    public PrioritisedTask queueRunnable(final Runnable task, final PrioritisedExecutor.Priority priority);
++
++    /**
++     * Creates, but does not execute or queue the task. The task must later be queued via {@link BaseExecutor.BaseTask#queue()}.
++     *
++     * @param task The task to run.
++     *
++     * @throws IllegalStateException If this queue has shutdown.
++     * @throws NullPointerException If the task is null
++     * @throws IllegalArgumentException If the priority is invalid.
++     * @throws UnsupportedOperationException If this executor does not support lazily queueing tasks
++     * @return The prioritised task associated with the parameters
++     */
++    public default PrioritisedExecutor.PrioritisedTask createTask(final Runnable task) {
++        return this.createTask(task, PrioritisedExecutor.Priority.NORMAL);
++    }
++
++    /**
++     * Creates, but does not execute or queue the task. The task must later be queued via {@link BaseExecutor.BaseTask#queue()}.
++     *
++     * @param task The task to run.
++     * @param priority The priority for the task.
++     *
++     * @throws IllegalStateException If this queue has shutdown.
++     * @throws NullPointerException If the task is null
++     * @throws IllegalArgumentException If the priority is invalid.
++     * @throws UnsupportedOperationException If this executor does not support lazily queueing tasks
++     * @return The prioritised task associated with the parameters
++     */
++    public PrioritisedExecutor.PrioritisedTask createTask(final Runnable task, final PrioritisedExecutor.Priority priority);
++
++    public static interface PrioritisedTask extends BaseTask {
++
++        /**
++         * Returns the current priority. Note that {@link PrioritisedExecutor.Priority#COMPLETING} will be returned
++         * if this task is completing or has completed.
++         */
++        public PrioritisedExecutor.Priority getPriority();
++
++        /**
++         * Attempts to set this task's priority level to the level specified.
++         *
++         * @param priority Specified priority level.
++         *
++         * @throws IllegalArgumentException If the priority is invalid
++         * @return {@code true} if successful, {@code false} if this task is completing or has completed or the queue
++         * this task was scheduled on was shutdown, or if the priority was already at the specified level.
++         */
++        public boolean setPriority(final PrioritisedExecutor.Priority priority);
++
++        /**
++         * Attempts to raise the priority to the priority level specified.
++         *
++         * @param priority Priority specified
++         *
++         * @throws IllegalArgumentException If the priority is invalid
++         * @return {@code false} if the current task is completing, {@code true} if the priority was raised to the specified level or was already at the specified level or higher.
++         */
++        public boolean raisePriority(final PrioritisedExecutor.Priority priority);
++
++        /**
++         * Attempts to lower the priority to the priority level specified.
++         *
++         * @param priority Priority specified
++         *
++         * @throws IllegalArgumentException If the priority is invalid
++         * @return {@code false} if the current task is completing, {@code true} if the priority was lowered to the specified level or was already at the specified level or lower.
++         */
++        public boolean lowerPriority(final PrioritisedExecutor.Priority priority);
++    }
++}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedQueueExecutorThread.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedQueueExecutorThread.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedQueueExecutorThread.java
+@@ -0,0 +0,0 @@
++package ca.spottedleaf.concurrentutil.executor.standard;
++
++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
++import com.mojang.logging.LogUtils;
++import org.slf4j.Logger;
++import java.lang.invoke.VarHandle;
++import java.util.concurrent.locks.LockSupport;
++
++/**
++ * Thread which will continuously drain from a specified queue.
++ * <p>
++ *     Note: When using this thread, queue additions to the underlying {@link #queue} are not sufficient to get this thread
++ *     to execute the task. The function {@link #notifyTasks()} must be used after scheduling a task. For expected behaviour
++ *     of task scheduling (thread wakes up after tasks are scheduled), use the methods provided on {@link PrioritisedExecutor}
++ *     methods.
++ * </p>
++ */
++public class PrioritisedQueueExecutorThread extends Thread implements PrioritisedExecutor {
++
++    private static final Logger LOGGER = LogUtils.getLogger();
++
++    protected final PrioritisedExecutor queue;
++
++    protected volatile boolean threadShutdown;
++
++    protected static final VarHandle THREAD_PARKED_HANDLE = ConcurrentUtil.getVarHandle(PrioritisedQueueExecutorThread.class, "threadParked", boolean.class);
++    protected volatile boolean threadParked;
++
++    protected volatile boolean halted;
++
++    protected final long spinWaitTime;
++
++    static final long DEFAULT_SPINWAIT_TIME = (long)(0.1e6);// 0.1ms
++
++    public PrioritisedQueueExecutorThread(final PrioritisedExecutor queue) {
++        this(queue, DEFAULT_SPINWAIT_TIME); // 0.1ms
++    }
++
++    public PrioritisedQueueExecutorThread(final PrioritisedExecutor queue, final long spinWaitTime) { // in ns
++        this.queue = queue;
++        this.spinWaitTime = spinWaitTime;
++    }
++
++    @Override
++    public void run() {
++        final long spinWaitTime = this.spinWaitTime;
++
++        main_loop:
++        for (;;) {
++            this.pollTasks();
++
++            // spinwait
++
++            final long start = System.nanoTime();
++
++            for (;;) {
++                // If we are interrupted for any reason, park() will always return immediately. Clear so that we don't needlessly use cpu in such an event.
++                Thread.interrupted();
++                Thread.yield();
++                LockSupport.parkNanos("Spinwaiting on tasks", 10_000L); // 10us
++
++                if (this.pollTasks()) {
++                    // restart loop, found tasks
++                    continue main_loop;
++                }
++
++                if (this.handleClose()) {
++                    return; // we're done
++                }
++
++                if ((System.nanoTime() - start) >= spinWaitTime) {
++                    break;
++                }
++            }
++
++            if (this.handleClose()) {
++                return;
++            }
++
++            this.setThreadParkedVolatile(true);
++
++            // We need to parse here to avoid a race condition where a thread queues a task before we set parked to true
++            // (i.e it will not notify us)
++            if (this.pollTasks()) {
++                this.setThreadParkedVolatile(false);
++                continue;
++            }
++
++            if (this.handleClose()) {
++                return;
++            }
++
++            // we don't need to check parked before sleeping, but we do need to check parked in a do-while loop
++            // LockSupport.park() can fail for any reason
++            while (this.getThreadParkedVolatile()) {
++                Thread.interrupted();
++                LockSupport.park("Waiting on tasks");
++            }
++        }
++    }
++
++    protected boolean pollTasks() {
++        boolean ret = false;
++
++        for (;;) {
++            if (this.halted) {
++                break;
++            }
++            try {
++                if (!this.queue.executeTask()) {
++                    break;
++                }
++                ret = true;
++            } catch (final ThreadDeath death) {
++                throw death; // goodbye world...
++            } catch (final Throwable throwable) {
++                LOGGER.error("Exception thrown from prioritized runnable task in thread '" + this.getName() + "'", throwable);
++            }
++        }
++
++        return ret;
++    }
++
++    protected boolean handleClose() {
++        if (this.threadShutdown) {
++            this.pollTasks(); // this ensures we've emptied the queue
++            return true;
++        }
++        return false;
++    }
++
++    /**
++     * Notify this thread that a task has been added to its queue
++     * @return {@code true} if this thread was waiting for tasks, {@code false} if it is executing tasks
++     */
++    public boolean notifyTasks() {
++        if (this.getThreadParkedVolatile() && this.exchangeThreadParkedVolatile(false)) {
++            LockSupport.unpark(this);
++            return true;
++        }
++        return false;
++    }
++
++    @Override
++    public PrioritisedTask createTask(final Runnable task, final Priority priority) {
++        final PrioritisedExecutor.PrioritisedTask queueTask = this.queue.createTask(task, priority);
++
++        // need to override queue() to notify us of tasks
++        return new PrioritisedTask() {
++            @Override
++            public Priority getPriority() {
++                return queueTask.getPriority();
++            }
++
++            @Override
++            public boolean setPriority(final Priority priority) {
++                return queueTask.setPriority(priority);
++            }
++
++            @Override
++            public boolean raisePriority(final Priority priority) {
++                return queueTask.raisePriority(priority);
++            }
++
++            @Override
++            public boolean lowerPriority(final Priority priority) {
++                return queueTask.lowerPriority(priority);
++            }
++
++            @Override
++            public boolean queue() {
++                final boolean ret = queueTask.queue();
++                if (ret) {
++                    PrioritisedQueueExecutorThread.this.notifyTasks();
++                }
++                return ret;
++            }
++
++            @Override
++            public boolean cancel() {
++                return queueTask.cancel();
++            }
++
++            @Override
++            public boolean execute() {
++                return queueTask.execute();
++            }
++        };
++    }
++
++    @Override
++    public PrioritisedExecutor.PrioritisedTask queueRunnable(final Runnable task, final PrioritisedExecutor.Priority priority) {
++        final PrioritisedExecutor.PrioritisedTask ret = this.queue.queueRunnable(task, priority);
++
++        this.notifyTasks();
++
++        return ret;
++    }
++
++    @Override
++    public boolean haveAllTasksExecuted() {
++        return this.queue.haveAllTasksExecuted();
++    }
++
++    @Override
++    public long getTotalTasksExecuted() {
++        return this.queue.getTotalTasksExecuted();
++    }
++
++    @Override
++    public long getTotalTasksScheduled() {
++        return this.queue.getTotalTasksScheduled();
++    }
++
++    /**
++     * {@inheritDoc}
++     * @throws IllegalStateException If the current thread is {@code this} thread, or the underlying queue throws this exception.
++     */
++    @Override
++    public void waitUntilAllExecuted() throws IllegalStateException {
++        if (Thread.currentThread() == this) {
++            throw new IllegalStateException("Cannot block on our own queue");
++        }
++        this.queue.waitUntilAllExecuted();
++    }
++
++    /**
++     * {@inheritDoc}
++     * @throws IllegalStateException Always
++     */
++    @Override
++    public boolean executeTask() throws IllegalStateException {
++        throw new IllegalStateException();
++    }
++
++    /**
++     * Closes this queue executor's queue. Optionally waits for all tasks in queue to be executed if {@code wait} is true.
++     * <p>
++     *     This function is MT-Safe.
++     * </p>
++     * @param wait If this call is to wait until the queue is empty and there are no tasks executing in the queue.
++     * @param killQueue Whether to shutdown this thread's queue
++     * @return whether this thread shut down the queue
++     * @see #halt(boolean)
++     */
++    public boolean close(final boolean wait, final boolean killQueue) {
++        final boolean ret = killQueue && this.queue.shutdown();
++        this.threadShutdown = true;
++
++        // force thread to respond to the shutdown
++        this.setThreadParkedVolatile(false);
++        LockSupport.unpark(this);
++
++        if (wait) {
++            this.waitUntilAllExecuted();
++        }
++
++        return ret;
++    }
++
++
++    /**
++     * Causes this thread to exit without draining the queue. To ensure tasks are completed, use {@link #close(boolean, boolean)}.
++     * <p>
++     *     This is not safe to call with {@link #close(boolean, boolean)} if <code>wait = true</code>, in which case
++     *     the waiting thread may block indefinitely.
++     * </p>
++     * <p>
++     *     This function is MT-Safe.
++     * </p>
++     * @param killQueue Whether to shutdown this thread's queue
++     * @see #close(boolean, boolean)
++     */
++    public void halt(final boolean killQueue) {
++        if (killQueue) {
++            this.queue.shutdown();
++        }
++        this.threadShutdown = true;
++        this.halted = true;
++
++        // force thread to respond to the shutdown
++        this.setThreadParkedVolatile(false);
++        LockSupport.unpark(this);
++    }
++
++    protected final boolean getThreadParkedVolatile() {
++        return (boolean)THREAD_PARKED_HANDLE.getVolatile(this);
++    }
++
++    protected final boolean exchangeThreadParkedVolatile(final boolean value) {
++        return (boolean)THREAD_PARKED_HANDLE.getAndSet(this, value);
++    }
++
++    protected final void setThreadParkedVolatile(final boolean value) {
++        THREAD_PARKED_HANDLE.setVolatile(this, value);
++    }
++}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadPool.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadPool.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadPool.java
+@@ -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 java.util.ArrayList;
++import java.util.Arrays;
++import java.util.Comparator;
++import java.util.TreeSet;
++import java.util.concurrent.atomic.AtomicBoolean;
++import java.util.function.BiConsumer;
++
++public final class PrioritisedThreadPool {
++
++    private static final Logger LOGGER = LogUtils.getLogger();
++
++    protected final PrioritisedThread[] threads;
++    protected final TreeSet<PrioritisedPoolExecutorImpl> queues = new TreeSet<>(PrioritisedPoolExecutorImpl.comparator());
++    protected final String name;
++    protected final long queueMaxHoldTime;
++
++    protected final ReferenceOpenHashSet<PrioritisedPoolExecutorImpl> nonShutdownQueues = new ReferenceOpenHashSet<>();
++    protected final ReferenceOpenHashSet<PrioritisedPoolExecutorImpl> activeQueues = new ReferenceOpenHashSet<>();
++
++    protected boolean shutdown;
++
++    protected long schedulingIdGenerator;
++
++    protected static final long DEFAULT_QUEUE_HOLD_TIME = (long)(5.0e6);
++
++    public PrioritisedThreadPool(final String name, final int threads) {
++        this(name, threads, null);
++    }
++
++    public PrioritisedThreadPool(final String name, final int threads, final BiConsumer<Thread, Integer> threadModifier) {
++        this(name, threads, threadModifier, DEFAULT_QUEUE_HOLD_TIME); // 5ms
++    }
++
++    public PrioritisedThreadPool(final String name, final int threads, final BiConsumer<Thread, Integer> threadModifier,
++                                 final long queueHoldTime) { // in ns
++        if (threads <= 0) {
++            throw new IllegalArgumentException("Thread count must be > 0, not " + threads);
++        }
++        if (name == null) {
++            throw new IllegalArgumentException("Name cannot be null");
++        }
++        this.name = name;
++        this.queueMaxHoldTime = queueHoldTime;
++
++        this.threads = new PrioritisedThread[threads];
++        for (int i = 0; i < threads; ++i) {
++            this.threads[i] = new PrioritisedThread(this);
++
++            // set default attributes
++            this.threads[i].setName("Prioritised thread for pool '" + name + "' #" + i);
++            this.threads[i].setUncaughtExceptionHandler((final Thread thread, final Throwable throwable) -> {
++                LOGGER.error("Uncaught exception in thread " + thread.getName(), throwable);
++            });
++
++            // let thread modifier override defaults
++            if (threadModifier != null) {
++                threadModifier.accept(this.threads[i], Integer.valueOf(i));
++            }
++
++            // now the thread can start
++            this.threads[i].start();
++        }
++    }
++
++    public Thread[] getThreads() {
++        return Arrays.copyOf(this.threads, this.threads.length, Thread[].class);
++    }
++
++    public PrioritisedPoolExecutor createExecutor(final String name, final int parallelism) {
++        synchronized (this.nonShutdownQueues) {
++            if (this.shutdown) {
++                throw new IllegalStateException("Queue is shutdown: " + this.toString());
++            }
++            final PrioritisedPoolExecutorImpl ret = new PrioritisedPoolExecutorImpl(this, name, Math.min(Math.max(1, parallelism), this.threads.length));
++
++            this.nonShutdownQueues.add(ret);
++
++            synchronized (this.activeQueues) {
++                this.activeQueues.add(ret);
++            }
++
++            return ret;
++        }
++    }
++
++    /**
++     * Prevents creation of new queues, shutdowns all non-shutdown queues if specified
++     */
++    public void halt(final boolean shutdownQueues) {
++        synchronized (this.nonShutdownQueues) {
++            this.shutdown = true;
++        }
++        if (shutdownQueues) {
++            final ArrayList<PrioritisedPoolExecutorImpl> queuesToShutdown;
++            synchronized (this.nonShutdownQueues) {
++                this.shutdown = true;
++                queuesToShutdown = new ArrayList<>(this.nonShutdownQueues);
++            }
++
++            for (final PrioritisedPoolExecutorImpl queue : queuesToShutdown) {
++                queue.shutdown();
++            }
++        }
++
++
++        for (final PrioritisedThread thread : this.threads) {
++            // can't kill queue, queue is null
++            thread.halt(false);
++        }
++    }
++
++    /**
++     * Waits until all threads in this pool have shutdown, or until the specified time has passed.
++     * @param msToWait Maximum time to wait.
++     * @return {@code false} if the maximum time passed, {@code true} otherwise.
++     */
++    public boolean join(final long msToWait) {
++        try {
++            return this.join(msToWait, false);
++        } catch (final InterruptedException ex) {
++            throw new IllegalStateException(ex);
++        }
++    }
++
++    /**
++     * Waits until all threads in this pool have shutdown, or until the specified time has passed.
++     * @param msToWait Maximum time to wait.
++     * @return {@code false} if the maximum time passed, {@code true} otherwise.
++     * @throws InterruptedException If this thread is interrupted.
++     */
++    public boolean joinInterruptable(final long msToWait) throws InterruptedException {
++        return this.join(msToWait, true);
++    }
++
++    protected final boolean join(final long msToWait, final boolean interruptable) throws InterruptedException {
++        final long nsToWait = msToWait * (1000 * 1000);
++        final long start = System.nanoTime();
++        final long deadline = start + nsToWait;
++        boolean interrupted = false;
++        try {
++            for (final PrioritisedThread thread : this.threads) {
++                for (;;) {
++                    if (!thread.isAlive()) {
++                        break;
++                    }
++                    final long current = System.nanoTime();
++                    if (current >= deadline) {
++                        return false;
++                    }
++
++                    try {
++                        thread.join(Math.max(1L, (deadline - current) / (1000 * 1000)));
++                    } catch (final InterruptedException ex) {
++                        if (interruptable) {
++                            throw ex;
++                        }
++                        interrupted = true;
++                    }
++                }
++            }
++
++            return true;
++        } finally {
++            if (interrupted) {
++                Thread.currentThread().interrupt();
++            }
++        }
++    }
++
++    public void shutdown(final boolean wait) {
++        final ArrayList<PrioritisedPoolExecutorImpl> queuesToShutdown;
++        synchronized (this.nonShutdownQueues) {
++            this.shutdown = true;
++            queuesToShutdown = new ArrayList<>(this.nonShutdownQueues);
++        }
++
++        for (final PrioritisedPoolExecutorImpl queue : queuesToShutdown) {
++            queue.shutdown();
++        }
++
++        for (final PrioritisedThread thread : this.threads) {
++            // none of these can be true or else NPE
++            thread.close(false, false);
++        }
++
++        if (wait) {
++            final ArrayList<PrioritisedPoolExecutorImpl> queues;
++            synchronized (this.activeQueues) {
++                queues = new ArrayList<>(this.activeQueues);
++            }
++            for (final PrioritisedPoolExecutorImpl queue : queues) {
++                queue.waitUntilAllExecuted();
++            }
++        }
++    }
++
++    protected static final class PrioritisedThread extends PrioritisedQueueExecutorThread {
++
++        protected final PrioritisedThreadPool pool;
++        protected final AtomicBoolean alertedHighPriority = new AtomicBoolean();
++
++        public PrioritisedThread(final PrioritisedThreadPool pool) {
++            super(null);
++            this.pool = pool;
++        }
++
++        public boolean alertHighPriorityExecutor() {
++            if (!this.notifyTasks()) {
++                if (!this.alertedHighPriority.get()) {
++                    this.alertedHighPriority.set(true);
++                }
++                return false;
++            }
++
++            return true;
++        }
++
++        private boolean isAlertedHighPriority() {
++            return this.alertedHighPriority.get() && this.alertedHighPriority.getAndSet(false);
++        }
++
++        @Override
++        protected boolean pollTasks() {
++            final PrioritisedThreadPool pool = this.pool;
++            final TreeSet<PrioritisedPoolExecutorImpl> queues = this.pool.queues;
++
++            boolean ret = false;
++            for (;;) {
++                if (this.halted) {
++                    break;
++                }
++                // try to find a queue
++                // note that if and ONLY IF the queues set is empty, this means there are no tasks for us to execute.
++                // so we can only break when it's empty
++                final PrioritisedPoolExecutorImpl queue;
++                // select queue
++                synchronized (queues) {
++                    queue = queues.pollFirst();
++                    if (queue == null) {
++                        // no tasks to execute
++                        break;
++                    }
++
++                    queue.schedulingId = ++pool.schedulingIdGenerator;
++                    // we own this queue now, so increment the executor count
++                    // do we also need to push this queue up for grabs for another executor?
++                    if (++queue.concurrentExecutors < queue.maximumExecutors) {
++                        // re-add to queues
++                        // it's very important this is done in the same synchronised block for polling, as this prevents
++                        // us from possibly later adding a queue that should not exist in the set
++                        queues.add(queue);
++                        queue.isQueued = true;
++                    } else {
++                        queue.isQueued = false;
++                    }
++                    // note: we cannot drain entries from the queue while holding this lock, as it will cause deadlock
++                    // the queue addition holds the per-queue lock first then acquires the lock we have now, but if we
++                    // try to poll now we don't hold the per queue lock but we do hold the global lock...
++                }
++
++                // parse tasks as long as we are allowed
++                final long start = System.nanoTime();
++                final long deadline = start + pool.queueMaxHoldTime;
++                do {
++                    try {
++                        if (this.halted) {
++                            break;
++                        }
++                        if (!queue.executeTask()) {
++                            // no more tasks, try next queue
++                            break;
++                        }
++                        ret = true;
++                    } catch (final ThreadDeath death) {
++                        throw death; // goodbye world...
++                    } catch (final Throwable throwable) {
++                        LOGGER.error("Exception thrown from thread '" + this.getName() + "' in queue '" + queue.toString() + "'", throwable);
++                    }
++                } while (!this.isAlertedHighPriority() && System.nanoTime() <= deadline);
++
++                synchronized (queues) {
++                    // decrement executors, we are no longer executing
++                    if (queue.isQueued) {
++                        queues.remove(queue);
++                        queue.isQueued = false;
++                    }
++                    if (--queue.concurrentExecutors == 0 && queue.scheduledPriority == null) {
++                        // reset scheduling id once the queue is empty again
++                        // this will ensure empty queues are not prioritised suddenly over active queues once tasks are
++                        // queued
++                        queue.schedulingId = 0L;
++                    }
++
++                    // ensure the executor is queued for execution again
++                    if (!queue.isHalted && queue.scheduledPriority != null) { // make sure it actually has tasks
++                        queues.add(queue);
++                        queue.isQueued = true;
++                    }
++                }
++            }
++
++            return ret;
++        }
++    }
++
++    public interface PrioritisedPoolExecutor extends PrioritisedExecutor {
++
++        /**
++         * Removes this queue from the thread pool without shutting the queue down or waiting for queued tasks to be executed
++         */
++        public void halt();
++
++        /**
++         * Returns whether this executor is scheduled to run tasks or is running tasks, otherwise it returns whether
++         * this queue is not halted and not shutdown.
++         */
++        public boolean isActive();
++    }
++
++    protected static final class PrioritisedPoolExecutorImpl extends PrioritisedThreadedTaskQueue implements PrioritisedPoolExecutor {
++
++        protected final PrioritisedThreadPool pool;
++        protected final long[] priorityCounts = new long[Priority.TOTAL_SCHEDULABLE_PRIORITIES];
++        protected long schedulingId;
++        protected int concurrentExecutors;
++        protected Priority scheduledPriority;
++
++        protected final String name;
++        protected final int maximumExecutors;
++        protected boolean isQueued;
++
++        public PrioritisedPoolExecutorImpl(final PrioritisedThreadPool pool, final String name, final int maximumExecutors) {
++            this.pool = pool;
++            this.name = name;
++            this.maximumExecutors = maximumExecutors;
++        }
++
++        public static Comparator<PrioritisedPoolExecutorImpl> comparator() {
++            return (final PrioritisedPoolExecutorImpl p1, final PrioritisedPoolExecutorImpl p2) -> {
++                if (p1 == p2) {
++                    return 0;
++                }
++
++                // prefer higher priority
++                final int priorityCompare = p1.scheduledPriority.ordinal() - p2.scheduledPriority.ordinal();
++                if (priorityCompare != 0) {
++                    return priorityCompare;
++                }
++
++                // try to spread out the executors so that each can have threads executing
++                final int executorCompare = p1.concurrentExecutors - p2.concurrentExecutors;
++                if (executorCompare != 0) {
++                    return executorCompare;
++                }
++
++                // if all else fails here we just choose whichever executor was queued first
++                return Long.compare(p1.schedulingId, p2.schedulingId);
++            };
++        }
++
++        private boolean isHalted;
++
++        @Override
++        public void halt() {
++            final PrioritisedThreadPool pool = this.pool;
++            final TreeSet<PrioritisedPoolExecutorImpl> queues = pool.queues;
++            synchronized (queues) {
++                if (this.isHalted) {
++                    return;
++                }
++                this.isHalted = true;
++                if (this.isQueued) {
++                    queues.remove(this);
++                    this.isQueued = false;
++                }
++            }
++            synchronized (pool.nonShutdownQueues) {
++                pool.nonShutdownQueues.remove(this);
++            }
++            synchronized (pool.activeQueues) {
++                pool.activeQueues.remove(this);
++            }
++        }
++
++        @Override
++        public boolean isActive() {
++            final PrioritisedThreadPool pool = this.pool;
++            final TreeSet<PrioritisedPoolExecutorImpl> queues = pool.queues;
++
++            synchronized (queues) {
++                if (this.concurrentExecutors != 0) {
++                    return true;
++                }
++                synchronized (pool.activeQueues) {
++                    if (pool.activeQueues.contains(this)) {
++                        return true;
++                    }
++                }
++            }
++
++            return false;
++        }
++
++        private long totalQueuedTasks = 0L;
++
++        @Override
++        protected void priorityChange(final PrioritisedThreadedTaskQueue.PrioritisedTask task, final Priority from, final Priority to) {
++            // Note: The superclass' queue lock is ALWAYS held when inside this method. So we do NOT need to do any additional synchronisation
++            // for accessing this queue's state.
++            final long[] priorityCounts = this.priorityCounts;
++            final boolean shutdown = this.isShutdown();
++
++            if (from == null && to == Priority.COMPLETING) {
++                throw new IllegalStateException("Cannot complete task without queueing it first");
++            }
++
++            // we should only notify for queueing of tasks, not changing priorities
++            final boolean shouldNotifyTasks = from == null;
++
++            final Priority scheduledPriority = this.scheduledPriority;
++            if (from != null) {
++                --priorityCounts[from.priority];
++            }
++            if (to != Priority.COMPLETING) {
++                ++priorityCounts[to.priority];
++            }
++            final long totalQueuedTasks;
++            if (to == Priority.COMPLETING) {
++                totalQueuedTasks = --this.totalQueuedTasks;
++            } else if (from == null) {
++                totalQueuedTasks = ++this.totalQueuedTasks;
++            } else {
++                totalQueuedTasks = this.totalQueuedTasks;
++            }
++
++            // find new highest priority
++            int highest = Math.min(to == Priority.COMPLETING ? Priority.IDLE.priority : to.priority, scheduledPriority == null ? Priority.IDLE.priority : scheduledPriority.priority);
++            int lowestPriority = priorityCounts.length; // exclusive
++            for (;highest < lowestPriority; ++highest) {
++                final long count = priorityCounts[highest];
++                if (count < 0) {
++                    throw new IllegalStateException("Priority " + highest + " has " + count + " scheduled tasks");
++                }
++
++                if (count != 0) {
++                    break;
++                }
++            }
++
++            final Priority newPriority;
++            if (highest == lowestPriority) {
++                // no tasks left
++                newPriority = null;
++            } else if (shutdown) {
++                // whichever is lower, the actual greatest priority or simply HIGHEST
++                // this is so shutdown automatically gets priority
++                newPriority = Priority.getPriority(Math.min(highest, Priority.HIGHEST.priority));
++            } else {
++                newPriority = Priority.getPriority(highest);
++            }
++
++            final int executorsWanted;
++            boolean shouldNotifyHighPriority = false;
++
++            final PrioritisedThreadPool pool = this.pool;
++            final TreeSet<PrioritisedPoolExecutorImpl> queues = pool.queues;
++
++            synchronized (queues) {
++                if (!this.isQueued) {
++                    // see if we need to be queued
++                    if (newPriority != null) {
++                        if (this.schedulingId == 0L) {
++                            this.schedulingId = ++pool.schedulingIdGenerator;
++                        }
++                        this.scheduledPriority = newPriority; // must be updated before queue add
++                        if (!this.isHalted && this.concurrentExecutors < this.maximumExecutors) {
++                            shouldNotifyHighPriority = newPriority.isHigherOrEqualPriority(Priority.HIGH);
++                            queues.add(this);
++                            this.isQueued = true;
++                        }
++                    } else {
++                        // do not queue
++                        this.scheduledPriority = null;
++                    }
++                } else {
++                    // see if we need to NOT be queued
++                    if (newPriority == null) {
++                        queues.remove(this);
++                        this.scheduledPriority = null;
++                        this.isQueued = false;
++                    } else if (scheduledPriority != newPriority) {
++                        // if our priority changed, we need to update it - which means removing and re-adding into the queue
++                        queues.remove(this);
++                        // only now can we update scheduledPriority, since we are no longer in queue
++                        this.scheduledPriority = newPriority;
++                        queues.add(this);
++                        shouldNotifyHighPriority = (scheduledPriority == null || scheduledPriority.isLowerPriority(Priority.HIGH)) && newPriority.isHigherOrEqualPriority(Priority.HIGH);
++                    }
++                }
++
++                if (this.isQueued) {
++                    executorsWanted = Math.min(this.maximumExecutors - this.concurrentExecutors, (int)totalQueuedTasks);
++                } else {
++                    executorsWanted = 0;
++                }
++            }
++
++            if (newPriority == null && shutdown) {
++                synchronized (pool.activeQueues) {
++                    pool.activeQueues.remove(this);
++                }
++            }
++
++            // Wake up the number of executors we want
++            if (executorsWanted > 0 || (shouldNotifyTasks | shouldNotifyHighPriority)) {
++                int notified = 0;
++                for (final PrioritisedThread thread : pool.threads) {
++                    if ((shouldNotifyHighPriority ? thread.alertHighPriorityExecutor() : thread.notifyTasks())
++                            && (++notified >= executorsWanted)) {
++                        break;
++                    }
++                }
++            }
++        }
++
++        @Override
++        public boolean shutdown() {
++            final boolean ret = super.shutdown();
++            if (!ret) {
++                return ret;
++            }
++
++            final PrioritisedThreadPool pool = this.pool;
++
++            // remove from active queues
++            synchronized (pool.nonShutdownQueues) {
++                pool.nonShutdownQueues.remove(this);
++            }
++
++            final TreeSet<PrioritisedPoolExecutorImpl> queues = pool.queues;
++
++            // try and shift around our priority
++            synchronized (queues) {
++                if (this.scheduledPriority == null) {
++                    // no tasks are queued, ensure we aren't in activeQueues
++                    synchronized (pool.activeQueues) {
++                        pool.activeQueues.remove(this);
++                    }
++
++                    return ret;
++                }
++
++                // try to set scheduled priority to HIGHEST so it drains faster
++
++                if (this.scheduledPriority.isHigherOrEqualPriority(Priority.HIGHEST)) {
++                    // already at target priority (highest or above)
++                    return ret;
++                }
++
++                // shift priority to HIGHEST
++
++                if (this.isQueued) {
++                    queues.remove(this);
++                    this.scheduledPriority = Priority.HIGHEST;
++                    queues.add(this);
++                } else {
++                    this.scheduledPriority = Priority.HIGHEST;
++                }
++            }
++
++            return ret;
++        }
++    }
++}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadedTaskQueue.java b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadedTaskQueue.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/executor/standard/PrioritisedThreadedTaskQueue.java
+@@ -0,0 +0,0 @@
++package ca.spottedleaf.concurrentutil.executor.standard;
++
++import java.util.ArrayDeque;
++import java.util.concurrent.atomic.AtomicLong;
++
++public class PrioritisedThreadedTaskQueue implements PrioritisedExecutor {
++
++    protected final ArrayDeque<PrioritisedTask>[] queues = new ArrayDeque[Priority.TOTAL_SCHEDULABLE_PRIORITIES]; {
++        for (int i = 0; i < Priority.TOTAL_SCHEDULABLE_PRIORITIES; ++i) {
++            this.queues[i] = new ArrayDeque<>();
++        }
++    }
++
++    // Use AtomicLong to separate from the queue field, we don't want false sharing here.
++    protected final AtomicLong totalScheduledTasks = new AtomicLong();
++    protected final AtomicLong totalCompletedTasks = new AtomicLong();
++
++    // this is here to prevent failures to queue stalling flush() calls (as the schedule calls would increment totalScheduledTasks without this check)
++    protected volatile boolean hasShutdown;
++
++    protected long taskIdGenerator = 0;
++
++    @Override
++    public PrioritisedExecutor.PrioritisedTask queueRunnable(final Runnable task, final PrioritisedExecutor.Priority priority) throws IllegalStateException, IllegalArgumentException {
++        if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
++            throw new IllegalArgumentException("Priority " + priority + " is invalid");
++        }
++        if (task == null) {
++            throw new NullPointerException("Task cannot be null");
++        }
++
++        if (this.hasShutdown) {
++            // prevent us from stalling flush() calls by incrementing scheduled tasks when we really didn't schedule something
++            throw new IllegalStateException("Queue has shutdown");
++        }
++
++        final PrioritisedTask ret;
++
++        synchronized (this.queues) {
++            if (this.hasShutdown) {
++                throw new IllegalStateException("Queue has shutdown");
++            }
++            this.getAndAddTotalScheduledTasksVolatile(1L);
++
++            ret = new PrioritisedTask(this.taskIdGenerator++, task, priority, this);
++
++            this.queues[ret.priority.priority].add(ret);
++
++            // call priority change callback (note: only after we successfully queue!)
++            this.priorityChange(ret, null, priority);
++        }
++
++        return ret;
++    }
++
++    @Override
++    public PrioritisedExecutor.PrioritisedTask createTask(final Runnable task, final Priority priority) {
++        if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
++            throw new IllegalArgumentException("Priority " + priority + " is invalid");
++        }
++        if (task == null) {
++            throw new NullPointerException("Task cannot be null");
++        }
++
++        return new PrioritisedTask(task, priority, this);
++    }
++
++    @Override
++    public long getTotalTasksScheduled() {
++        return this.totalScheduledTasks.get();
++    }
++
++    @Override
++    public long getTotalTasksExecuted() {
++        return this.totalCompletedTasks.get();
++    }
++
++    // callback method for subclasses to override
++    // from is null when a task is immediately created
++    protected void priorityChange(final PrioritisedTask task, final Priority from, final Priority to) {}
++
++    /**
++     * Polls the highest priority task currently available. {@code null} if none. This will mark the
++     * returned task as completed.
++     */
++    protected PrioritisedTask poll() {
++        return this.poll(Priority.IDLE);
++    }
++
++    protected PrioritisedTask poll(final PrioritisedExecutor.Priority minPriority) {
++        final ArrayDeque<PrioritisedTask>[] queues = this.queues;
++        synchronized (queues) {
++            final int max = minPriority.priority;
++            for (int i = 0; i <= max; ++i) {
++                final ArrayDeque<PrioritisedTask> queue = queues[i];
++                PrioritisedTask task;
++                while ((task = queue.pollFirst()) != null) {
++                    if (task.trySetCompleting(i)) {
++                        return task;
++                    }
++                }
++            }
++        }
++
++        return null;
++    }
++
++    /**
++     * Polls and executes the highest priority task currently available. Exceptions thrown during task execution will
++     * be rethrown.
++     * @return {@code true} if a task was executed, {@code false} otherwise.
++     */
++    @Override
++    public boolean executeTask() {
++        final PrioritisedTask task = this.poll();
++
++        if (task != null) {
++            task.executeInternal();
++            return true;
++        }
++
++        return false;
++    }
++
++    @Override
++    public boolean shutdown() {
++        synchronized (this.queues) {
++            if (this.hasShutdown) {
++                return false;
++            }
++            this.hasShutdown = true;
++        }
++        return true;
++    }
++
++    @Override
++    public boolean isShutdown() {
++        return this.hasShutdown;
++    }
++
++    /* totalScheduledTasks */
++
++    protected final long getTotalScheduledTasksVolatile() {
++        return this.totalScheduledTasks.get();
++    }
++
++    protected final long getAndAddTotalScheduledTasksVolatile(final long value) {
++        return this.totalScheduledTasks.getAndAdd(value);
++    }
++
++    /* totalCompletedTasks */
++
++    protected final long getTotalCompletedTasksVolatile() {
++        return this.totalCompletedTasks.get();
++    }
++
++    protected final long getAndAddTotalCompletedTasksVolatile(final long value) {
++        return this.totalCompletedTasks.getAndAdd(value);
++    }
++
++    protected static final class PrioritisedTask implements PrioritisedExecutor.PrioritisedTask {
++        protected final PrioritisedThreadedTaskQueue queue;
++        protected long id;
++        protected static final long NOT_SCHEDULED_ID = -1L;
++
++        protected Runnable runnable;
++        protected volatile PrioritisedExecutor.Priority priority;
++
++        protected PrioritisedTask(final long id, final Runnable runnable, final PrioritisedExecutor.Priority priority, final PrioritisedThreadedTaskQueue queue) {
++            if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
++                throw new IllegalArgumentException("Invalid priority " + priority);
++            }
++
++            this.priority = priority;
++            this.runnable = runnable;
++            this.queue = queue;
++            this.id = id;
++        }
++
++        protected PrioritisedTask(final Runnable runnable, final PrioritisedExecutor.Priority priority, final PrioritisedThreadedTaskQueue queue) {
++            if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
++                throw new IllegalArgumentException("Invalid priority " + priority);
++            }
++
++            this.priority = priority;
++            this.runnable = runnable;
++            this.queue = queue;
++            this.id = NOT_SCHEDULED_ID;
++        }
++
++        @Override
++        public boolean queue() {
++            if (this.queue.hasShutdown) {
++                throw new IllegalStateException("Queue has shutdown");
++            }
++
++            synchronized (this.queue.queues) {
++                if (this.queue.hasShutdown) {
++                    throw new IllegalStateException("Queue has shutdown");
++                }
++
++                final PrioritisedExecutor.Priority priority = this.priority;
++                if (priority == PrioritisedExecutor.Priority.COMPLETING) {
++                    return false;
++                }
++
++                if (this.id != NOT_SCHEDULED_ID) {
++                    return false;
++                }
++
++                this.queue.getAndAddTotalScheduledTasksVolatile(1L);
++                this.id = this.queue.taskIdGenerator++;
++                this.queue.queues[priority.priority].add(this);
++
++                this.queue.priorityChange(this, null, priority);
++
++                return true;
++            }
++        }
++
++        protected boolean trySetCompleting(final int minPriority) {
++            final PrioritisedExecutor.Priority oldPriority = this.priority;
++            if (oldPriority != PrioritisedExecutor.Priority.COMPLETING && oldPriority.isHigherOrEqualPriority(minPriority)) {
++                this.priority = PrioritisedExecutor.Priority.COMPLETING;
++                if (this.id != NOT_SCHEDULED_ID) {
++                    this.queue.priorityChange(this, oldPriority, PrioritisedExecutor.Priority.COMPLETING);
++                }
++                return true;
++            }
++
++            return false;
++        }
++
++        @Override
++        public PrioritisedExecutor.Priority getPriority() {
++            return this.priority;
++        }
++
++        @Override
++        public boolean setPriority(final PrioritisedExecutor.Priority priority) {
++            if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
++                throw new IllegalArgumentException("Invalid priority " + priority);
++            }
++            synchronized (this.queue.queues) {
++                final PrioritisedExecutor.Priority curr = this.priority;
++
++                if (curr == PrioritisedExecutor.Priority.COMPLETING) {
++                    return false;
++                }
++
++                if (curr == priority) {
++                    return true;
++                }
++
++                this.priority = priority;
++                if (this.id != NOT_SCHEDULED_ID) {
++                    this.queue.queues[priority.priority].add(this);
++
++                    // call priority change callback
++                    this.queue.priorityChange(this, curr, priority);
++                }
++            }
++
++            return true;
++        }
++
++        @Override
++        public boolean raisePriority(final PrioritisedExecutor.Priority priority) {
++            if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
++                throw new IllegalArgumentException("Invalid priority " + priority);
++            }
++
++            synchronized (this.queue.queues) {
++                final PrioritisedExecutor.Priority curr = this.priority;
++
++                if (curr == PrioritisedExecutor.Priority.COMPLETING) {
++                    return false;
++                }
++
++                if (curr.isHigherOrEqualPriority(priority)) {
++                    return true;
++                }
++
++                this.priority = priority;
++                if (this.id != NOT_SCHEDULED_ID) {
++                    this.queue.queues[priority.priority].add(this);
++
++                    // call priority change callback
++                    this.queue.priorityChange(this, curr, priority);
++                }
++            }
++
++            return true;
++        }
++
++        @Override
++        public boolean lowerPriority(final PrioritisedExecutor.Priority priority) {
++            if (!PrioritisedExecutor.Priority.isValidPriority(priority)) {
++                throw new IllegalArgumentException("Invalid priority " + priority);
++            }
++
++            synchronized (this.queue.queues) {
++                final PrioritisedExecutor.Priority curr = this.priority;
++
++                if (curr == PrioritisedExecutor.Priority.COMPLETING) {
++                    return false;
++                }
++
++                if (curr.isLowerOrEqualPriority(priority)) {
++                    return true;
++                }
++
++                this.priority = priority;
++                if (this.id != NOT_SCHEDULED_ID) {
++                    this.queue.queues[priority.priority].add(this);
++
++                    // call priority change callback
++                    this.queue.priorityChange(this, curr, priority);
++                }
++            }
++
++            return true;
++        }
++
++        @Override
++        public boolean cancel() {
++            final long id;
++            synchronized (this.queue.queues) {
++                final Priority oldPriority = this.priority;
++                if (oldPriority == PrioritisedExecutor.Priority.COMPLETING) {
++                    return false;
++                }
++
++                this.priority = PrioritisedExecutor.Priority.COMPLETING;
++                // call priority change callback
++                if ((id = this.id) != NOT_SCHEDULED_ID) {
++                    this.queue.priorityChange(this, oldPriority, PrioritisedExecutor.Priority.COMPLETING);
++                }
++            }
++            this.runnable = null;
++            if (id != NOT_SCHEDULED_ID) {
++                this.queue.getAndAddTotalCompletedTasksVolatile(1L);
++            }
++            return true;
++        }
++
++        protected void executeInternal() {
++            try {
++                final Runnable execute = this.runnable;
++                this.runnable = null;
++                execute.run();
++            } finally {
++                if (this.id != NOT_SCHEDULED_ID) {
++                    this.queue.getAndAddTotalCompletedTasksVolatile(1L);
++                }
++            }
++        }
++
++        @Override
++        public boolean execute() {
++            synchronized (this.queue.queues) {
++                final Priority oldPriority = this.priority;
++                if (oldPriority == PrioritisedExecutor.Priority.COMPLETING) {
++                    return false;
++                }
++
++                this.priority = PrioritisedExecutor.Priority.COMPLETING;
++                // call priority change callback
++                if (this.id != NOT_SCHEDULED_ID) {
++                    this.queue.priorityChange(this, oldPriority, PrioritisedExecutor.Priority.COMPLETING);
++                }
++            }
++
++            this.executeInternal();
++            return true;
++        }
++    }
++}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRHashTable.java b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRHashTable.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRHashTable.java
+@@ -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.Validate;
++import io.papermc.paper.util.IntegerUtil;
++import java.lang.invoke.VarHandle;
++import java.util.ArrayList;
++import java.util.Arrays;
++import java.util.Collection;
++import java.util.Iterator;
++import java.util.List;
++import java.util.Map;
++import java.util.NoSuchElementException;
++import java.util.Set;
++import java.util.Spliterator;
++import java.util.Spliterators;
++import java.util.function.BiConsumer;
++import java.util.function.BiFunction;
++import java.util.function.BiPredicate;
++import java.util.function.Consumer;
++import java.util.function.Function;
++import java.util.function.IntFunction;
++import java.util.function.Predicate;
++
++/**
++ * <p>
++ * Note: Not really tested, use at your own risk.
++ * </p>
++ * This map is safe for reading from multiple threads, however it is only safe to write from a single thread.
++ * {@code null} keys or values are not permitted. Writes to values in this map are guaranteed to be ordered by release semantics,
++ * however immediate visibility to other threads is not guaranteed. However, writes are guaranteed to be made visible eventually.
++ * Reads are ordered by acquire semantics.
++ * <p>
++ * Iterators cannot be modified concurrently, and its backing map cannot be modified concurrently. There is no
++ * fast-fail attempt made by iterators, thus modifying the iterator's backing map while iterating will have undefined
++ * behaviour.
++ * </p>
++ * <p>
++ * Subclasses should override {@link #clone()} to return correct instances of this class.
++ * </p>
++ * @param <K> {@inheritDoc}
++ * @param <V> {@inheritDoc}
++ */
++public class SWMRHashTable<K, V> implements Map<K, V>, Iterable<Map.Entry<K, V>> {
++
++    protected int size;
++
++    protected TableEntry<K, V>[] table;
++
++    protected final float loadFactor;
++
++    protected static final VarHandle SIZE_HANDLE = ConcurrentUtil.getVarHandle(SWMRHashTable.class, "size", int.class);
++    protected static final VarHandle TABLE_HANDLE = ConcurrentUtil.getVarHandle(SWMRHashTable.class, "table", TableEntry[].class);
++
++    /* size */
++
++    protected final int getSizePlain() {
++        return (int)SIZE_HANDLE.get(this);
++    }
++
++    protected final int getSizeOpaque() {
++        return (int)SIZE_HANDLE.getOpaque(this);
++    }
++
++    protected final int getSizeAcquire() {
++        return (int)SIZE_HANDLE.getAcquire(this);
++    }
++
++    protected final void setSizePlain(final int value) {
++        SIZE_HANDLE.set(this, value);
++    }
++
++    protected final void setSizeOpaque(final int value) {
++        SIZE_HANDLE.setOpaque(this, value);
++    }
++
++    protected final void setSizeRelease(final int value) {
++        SIZE_HANDLE.setRelease(this, value);
++    }
++
++    /* table */
++
++    protected final TableEntry<K, V>[] getTablePlain() {
++        //noinspection unchecked
++        return (TableEntry<K, V>[])TABLE_HANDLE.get(this);
++    }
++
++    protected final TableEntry<K, V>[] getTableAcquire() {
++        //noinspection unchecked
++        return (TableEntry<K, V>[])TABLE_HANDLE.getAcquire(this);
++    }
++
++    protected final void setTablePlain(final TableEntry<K, V>[] table) {
++        TABLE_HANDLE.set(this, table);
++    }
++
++    protected final void setTableRelease(final TableEntry<K, V>[] table) {
++        TABLE_HANDLE.setRelease(this, table);
++    }
++
++    protected static final int DEFAULT_CAPACITY = 16;
++    protected static final float DEFAULT_LOAD_FACTOR = 0.75f;
++    protected static final int MAXIMUM_CAPACITY = Integer.MIN_VALUE >>> 1;
++
++    /**
++     * Constructs this map with a capacity of {@code 16} and load factor of {@code 0.75f}.
++     */
++    public SWMRHashTable() {
++        this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR);
++    }
++
++    /**
++     * Constructs this map with the specified capacity and load factor of {@code 0.75f}.
++     * @param capacity specified initial capacity, > 0
++     */
++    public SWMRHashTable(final int capacity) {
++        this(capacity, DEFAULT_LOAD_FACTOR);
++    }
++
++    /**
++     * Constructs this map with the specified capacity and load factor.
++     * @param capacity specified capacity, > 0
++     * @param loadFactor specified load factor, > 0 && finite
++     */
++    public SWMRHashTable(final int capacity, final float loadFactor) {
++        final int tableSize = getCapacityFor(capacity);
++
++        if (loadFactor <= 0.0 || !Float.isFinite(loadFactor)) {
++            throw new IllegalArgumentException("Invalid load factor: " + loadFactor);
++        }
++
++        //noinspection unchecked
++        final TableEntry<K, V>[] table = new TableEntry[tableSize];
++        this.setTablePlain(table);
++
++        if (tableSize == MAXIMUM_CAPACITY) {
++            this.threshold = -1;
++        } else {
++            this.threshold = getTargetCapacity(tableSize, loadFactor);
++        }
++
++        this.loadFactor = loadFactor;
++    }
++
++    /**
++     * Constructs this map with a capacity of {@code 16} or the specified map's size, whichever is larger, and
++     * with a load factor of {@code 0.75f}.
++     * All of the specified map's entries are copied into this map.
++     * @param other The specified map.
++     */
++    public SWMRHashTable(final Map<K, V> other) {
++        this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR, other);
++    }
++
++    /**
++     * Constructs this map with a minimum capacity of the specified capacity or the specified map's size, whichever is larger, and
++     * with a load factor of {@code 0.75f}.
++     * All of the specified map's entries are copied into this map.
++     * @param capacity specified capacity, > 0
++     * @param other The specified map.
++     */
++    public SWMRHashTable(final int capacity, final Map<K, V> other) {
++        this(capacity, DEFAULT_LOAD_FACTOR, other);
++    }
++
++    /**
++     * Constructs this map with a min capacity of the specified capacity or the specified map's size, whichever is larger, and
++     * with the specified load factor.
++     * All of the specified map's entries are copied into this map.
++     * @param capacity specified capacity, > 0
++     * @param loadFactor specified load factor, > 0 && finite
++     * @param other The specified map.
++     */
++    public SWMRHashTable(final int capacity, final float loadFactor, final Map<K, V> other) {
++        this(Math.max(Validate.notNull(other, "Null map").size(), capacity), loadFactor);
++        this.putAll(other);
++    }
++
++    public final float getLoadFactor() {
++        return this.loadFactor;
++    }
++
++    protected static int getCapacityFor(final int capacity) {
++        if (capacity <= 0) {
++            throw new IllegalArgumentException("Invalid capacity: " + capacity);
++        }
++        if (capacity >= MAXIMUM_CAPACITY) {
++            return MAXIMUM_CAPACITY;
++        }
++        return IntegerUtil.roundCeilLog2(capacity);
++    }
++
++    /** Callers must still use acquire when reading the value of the entry. */
++    protected final TableEntry<K, V> getEntryForOpaque(final K key) {
++        final int hash = SWMRHashTable.getHash(key);
++        final TableEntry<K, V>[] table = this.getTableAcquire();
++
++        for (TableEntry<K, V> curr = ArrayUtil.getOpaque(table, hash & (table.length - 1)); curr != null; curr = curr.getNextOpaque()) {
++            if (hash == curr.hash && (key == curr.key || curr.key.equals(key))) {
++                return curr;
++            }
++        }
++
++        return null;
++    }
++
++    protected final TableEntry<K, V> getEntryForPlain(final K key) {
++        final int hash = SWMRHashTable.getHash(key);
++        final TableEntry<K, V>[] table = this.getTablePlain();
++
++        for (TableEntry<K, V> curr = table[hash & (table.length - 1)]; curr != null; curr = curr.getNextPlain()) {
++            if (hash == curr.hash && (key == curr.key || curr.key.equals(key))) {
++                return curr;
++            }
++        }
++
++        return null;
++    }
++
++    /* MT-Safe */
++
++    /** must be deterministic given a key */
++    private static int getHash(final Object key) {
++        int hash = key == null ? 0 : key.hashCode();
++        // inlined IntegerUtil#hash0
++        hash *= 0x36935555;
++        hash ^= hash >>> 16;
++        return hash;
++    }
++
++    static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
++    static final int spread(int h) {
++        return (h ^ (h >>> 16)) & HASH_BITS;
++    }
++
++    // rets -1 if capacity*loadFactor is too large
++    protected static int getTargetCapacity(final int capacity, final float loadFactor) {
++        final double ret = (double)capacity * (double)loadFactor;
++        if (Double.isInfinite(ret) || ret >= ((double)Integer.MAX_VALUE)) {
++            return -1;
++        }
++
++        return (int)ret;
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    @Override
++    public boolean equals(final Object obj) {
++        if (this == obj) {
++            return true;
++        }
++        /* Make no attempt to deal with concurrent modifications */
++        if (!(obj instanceof Map)) {
++            return false;
++        }
++        final Map<?, ?> other = (Map<?, ?>)obj;
++
++        if (this.size() != other.size()) {
++            return false;
++        }
++
++        final TableEntry<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()) {
++                final V value = curr.getValueAcquire();
++
++                final Object otherValue = other.get(curr.key);
++                if (otherValue == null || (value != otherValue && value.equals(otherValue))) {
++                    return false;
++                }
++            }
++        }
++
++        return true;
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    @Override
++    public int hashCode() {
++        /* Make no attempt to deal with concurrent modifications */
++        int hash = 0;
++        final TableEntry<K, V>[] table = this.getTableAcquire();
++
++        for (int i = 0, len = table.length; i < len; ++i) {
++            for (TableEntry<K, V> curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
++                hash += curr.hashCode();
++            }
++        }
++
++        return hash;
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    @Override
++    public String toString() {
++        final StringBuilder builder = new StringBuilder(64);
++        builder.append("SingleWriterMultiReaderHashMap:{");
++
++        this.forEach((final K key, final V value) -> {
++            builder.append("{key: \"").append(key).append("\", value: \"").append(value).append("\"}");
++        });
++
++        return builder.append('}').toString();
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    @Override
++    public SWMRHashTable<K, V> clone() {
++        return new SWMRHashTable<>(this.getTableAcquire().length, this.loadFactor, this);
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    @Override
++    public Iterator<Entry<K, V>> iterator() {
++        return new EntryIterator<>(this.getTableAcquire(), this);
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    @Override
++    public void forEach(final Consumer<? super 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()) {
++                action.accept(curr);
++            }
++        }
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    @Override
++    public void forEach(final BiConsumer<? super K, ? super V> action) {
++        Validate.notNull(action, "Null action");
++
++        final TableEntry<K, V>[] table = this.getTableAcquire();
++        for (int i = 0, len = table.length; i < len; ++i) {
++            for (TableEntry<K, V> curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
++                final V value = curr.getValueAcquire();
++
++                action.accept(curr.key, value);
++            }
++        }
++    }
++
++    /**
++     * Provides the specified consumer with all keys contained within this map.
++     * @param action The specified consumer.
++     */
++    public void forEachKey(final Consumer<? super K> action) {
++        Validate.notNull(action, "Null action");
++
++        final TableEntry<K, V>[] table = this.getTableAcquire();
++        for (int i = 0, len = table.length; i < len; ++i) {
++            for (TableEntry<K, V> curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
++                action.accept(curr.key);
++            }
++        }
++    }
++
++    /**
++     * Provides the specified consumer with all values contained within this map. Equivalent to {@code map.values().forEach(Consumer)}.
++     * @param action The specified consumer.
++     */
++    public void forEachValue(final Consumer<? super V> action) {
++        Validate.notNull(action, "Null action");
++
++        final TableEntry<K, V>[] table = this.getTableAcquire();
++        for (int i = 0, len = table.length; i < len; ++i) {
++            for (TableEntry<K, V> curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
++                final V value = curr.getValueAcquire();
++
++                action.accept(value);
++            }
++        }
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    @Override
++    public V get(final Object key) {
++        Validate.notNull(key, "Null key");
++
++        //noinspection unchecked
++        final TableEntry<K, V> entry = this.getEntryForOpaque((K)key);
++        return entry == null ? null : entry.getValueAcquire();
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    @Override
++    public boolean containsKey(final Object key) {
++        Validate.notNull(key, "Null key");
++
++        // note: we need to use getValueAcquire, so that the reads from this map are ordered by acquire semantics
++        return this.get(key) != null;
++    }
++
++    /**
++     * Returns {@code true} if this map contains an entry with the specified key and value at some point during this call.
++     * @param key The specified key.
++     * @param value The specified value.
++     * @return {@code true} if this map contains an entry with the specified key and value.
++     */
++    public boolean contains(final Object key, final Object value) {
++        Validate.notNull(key, "Null key");
++
++        //noinspection unchecked
++        final TableEntry<K, V> entry = this.getEntryForOpaque((K)key);
++
++        if (entry == null) {
++            return false;
++        }
++
++        final V entryVal = entry.getValueAcquire();
++        return entryVal == value || entryVal.equals(value);
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    @Override
++    public boolean containsValue(final Object value) {
++        Validate.notNull(value, "Null value");
++
++        final TableEntry<K, V>[] table = this.getTableAcquire();
++        for (int i = 0, len = table.length; i < len; ++i) {
++            for (TableEntry<K, V> curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
++                final V currVal = curr.getValueAcquire();
++                if (currVal == value || currVal.equals(value)) {
++                    return true;
++                }
++            }
++        }
++
++        return false;
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    @Override
++    public V getOrDefault(final Object key, final V defaultValue) {
++        Validate.notNull(key, "Null key");
++
++        //noinspection unchecked
++        final TableEntry<K, V> entry = this.getEntryForOpaque((K)key);
++
++        return entry == null ? defaultValue : entry.getValueAcquire();
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    @Override
++    public int size() {
++        return this.getSizeAcquire();
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    @Override
++    public boolean isEmpty() {
++        return this.getSizeAcquire() == 0;
++    }
++
++    protected Set<K> keyset;
++    protected Collection<V> values;
++    protected Set<Map.Entry<K, V>> entrySet;
++
++    @Override
++    public Set<K> keySet() {
++        return this.keyset == null ? this.keyset = new KeySet<>(this) : this.keyset;
++    }
++
++    @Override
++    public Collection<V> values() {
++        return this.values == null ? this.values = new ValueCollection<>(this) : this.values;
++    }
++
++    @Override
++    public Set<Map.Entry<K, V>> entrySet() {
++        return this.entrySet == null ? this.entrySet = new EntrySet<>(this) : this.entrySet;
++    }
++
++    /* Non-MT-Safe */
++
++    protected int threshold;
++
++    protected final void checkResize(final int minCapacity) {
++        if (minCapacity <= this.threshold || this.threshold < 0) {
++            return;
++        }
++
++        final TableEntry<K, V>[] table = this.getTablePlain();
++        int newCapacity = minCapacity >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY : IntegerUtil.roundCeilLog2(minCapacity);
++        if (newCapacity < 0) {
++            newCapacity = MAXIMUM_CAPACITY;
++        }
++        if (newCapacity <= table.length) {
++            if (newCapacity == MAXIMUM_CAPACITY) {
++                return;
++            }
++            newCapacity = table.length << 1;
++        }
++
++        //noinspection unchecked
++        final TableEntry<K, V>[] newTable = new TableEntry[newCapacity];
++        final int indexMask = newCapacity - 1;
++
++        for (int i = 0, len = table.length; i < len; ++i) {
++            for (TableEntry<K, V> entry = table[i]; entry != null; entry = entry.getNextPlain()) {
++                final int hash = entry.hash;
++                final int index = hash & indexMask;
++
++                /* we need to create a new entry since there could be reading threads */
++                final TableEntry<K, V> insert = new TableEntry<>(hash, entry.key, entry.getValuePlain());
++
++                final TableEntry<K, V> prev = newTable[index];
++
++                newTable[index] = insert;
++                insert.setNextPlain(prev);
++            }
++        }
++
++        if (newCapacity == MAXIMUM_CAPACITY) {
++            this.threshold = -1; /* No more resizing */
++        } else {
++            this.threshold = getTargetCapacity(newCapacity, this.loadFactor);
++        }
++        this.setTableRelease(newTable); /* use release to publish entries in table */
++    }
++
++    protected final int addToSize(final int num) {
++        final int newSize = this.getSizePlain() + num;
++
++        this.setSizeOpaque(newSize);
++        this.checkResize(newSize);
++
++        return newSize;
++    }
++
++    protected final int removeFromSize(final int num) {
++        final int newSize = this.getSizePlain() - num;
++
++        this.setSizeOpaque(newSize);
++
++        return newSize;
++    }
++
++    /* Cannot be used to perform downsizing */
++    protected final int removeFromSizePlain(final int num) {
++        final int newSize = this.getSizePlain() - num;
++
++        this.setSizePlain(newSize);
++
++        return newSize;
++    }
++
++    protected final V put(final K key, final V value, final boolean onlyIfAbsent) {
++        final TableEntry<K, V>[] table = this.getTablePlain();
++        final int hash = SWMRHashTable.getHash(key);
++        final int index = hash & (table.length - 1);
++
++        final TableEntry<K, V> head = table[index];
++        if (head == null) {
++            final TableEntry<K, V> insert = new TableEntry<>(hash, key, value);
++            ArrayUtil.setRelease(table, index, insert);
++            this.addToSize(1);
++            return null;
++        }
++
++        for (TableEntry<K, V> curr = head;;) {
++            if (curr.hash == hash && (key == curr.key || curr.key.equals(key))) {
++                if (onlyIfAbsent) {
++                    return curr.getValuePlain();
++                }
++
++                final V currVal = curr.getValuePlain();
++                curr.setValueRelease(value);
++                return currVal;
++            }
++
++            final TableEntry<K, V> next = curr.getNextPlain();
++            if (next != null) {
++                curr = next;
++                continue;
++            }
++
++            final TableEntry<K, V> insert = new TableEntry<>(hash, key, value);
++
++            curr.setNextRelease(insert);
++            this.addToSize(1);
++            return null;
++        }
++    }
++
++    /**
++     * Removes a key-value pair from this map if the specified predicate returns true. The specified predicate is
++     * tested with every entry in this map. Returns the number of key-value pairs removed.
++     * @param predicate The predicate to test key-value pairs against.
++     * @return The total number of key-value pairs removed from this map.
++     */
++    public int removeIf(final BiPredicate<K, V> predicate) {
++        Validate.notNull(predicate, "Null predicate");
++
++        int removed = 0;
++
++        final TableEntry<K, V>[] table = this.getTablePlain();
++
++        bin_iteration_loop:
++        for (int i = 0, len = table.length; i < len; ++i) {
++            TableEntry<K, V> curr = table[i];
++            if (curr == null) {
++                continue;
++            }
++
++            /* Handle bin nodes first */
++            while (predicate.test(curr.key, curr.getValuePlain())) {
++                ++removed;
++                this.removeFromSizePlain(1); /* required in case predicate throws an exception */
++
++                ArrayUtil.setRelease(table, i, curr = curr.getNextPlain());
++
++                if (curr == null) {
++                    continue bin_iteration_loop;
++                }
++            }
++
++            TableEntry<K, V> prev;
++
++            /* curr at this point is the bin node */
++
++            for (prev = curr, curr = curr.getNextPlain(); curr != null;) {
++                /* If we want to remove, then we should hold prev, as it will be a valid entry to link on */
++                if (predicate.test(curr.key, curr.getValuePlain())) {
++                    ++removed;
++                    this.removeFromSizePlain(1); /* required in case predicate throws an exception */
++
++                    prev.setNextRelease(curr = curr.getNextPlain());
++                } else {
++                    prev = curr;
++                    curr = curr.getNextPlain();
++                }
++            }
++        }
++
++        return removed;
++    }
++
++    /**
++     * Removes a key-value pair from this map if the specified predicate returns true. The specified predicate is
++     * tested with every entry in this map. Returns the number of key-value pairs removed.
++     * @param predicate The predicate to test key-value pairs against.
++     * @return The total number of key-value pairs removed from this map.
++     */
++    public int removeEntryIf(final Predicate<? super Entry<K, V>> predicate) {
++        Validate.notNull(predicate, "Null predicate");
++
++        int removed = 0;
++
++        final TableEntry<K, V>[] table = this.getTablePlain();
++
++        bin_iteration_loop:
++        for (int i = 0, len = table.length; i < len; ++i) {
++            TableEntry<K, V> curr = table[i];
++            if (curr == null) {
++                continue;
++            }
++
++            /* Handle bin nodes first */
++            while (predicate.test(curr)) {
++                ++removed;
++                this.removeFromSizePlain(1); /* required in case predicate throws an exception */
++
++                ArrayUtil.setRelease(table, i, curr = curr.getNextPlain());
++
++                if (curr == null) {
++                    continue bin_iteration_loop;
++                }
++            }
++
++            TableEntry<K, V> prev;
++
++            /* curr at this point is the bin node */
++
++            for (prev = curr, curr = curr.getNextPlain(); curr != null;) {
++                /* If we want to remove, then we should hold prev, as it will be a valid entry to link on */
++                if (predicate.test(curr)) {
++                    ++removed;
++                    this.removeFromSizePlain(1); /* required in case predicate throws an exception */
++
++                    prev.setNextRelease(curr = curr.getNextPlain());
++                } else {
++                    prev = curr;
++                    curr = curr.getNextPlain();
++                }
++            }
++        }
++
++        return removed;
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    @Override
++    public V put(final K key, final V value) {
++        Validate.notNull(key, "Null key");
++        Validate.notNull(value, "Null value");
++
++        return this.put(key, value, false);
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    @Override
++    public V putIfAbsent(final K key, final V value) {
++        Validate.notNull(key, "Null key");
++        Validate.notNull(value, "Null value");
++
++        return this.put(key, value, true);
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    @Override
++    public boolean remove(final Object key, final Object value) {
++        Validate.notNull(key, "Null key");
++        Validate.notNull(value, "Null value");
++
++        final TableEntry<K, V>[] table = this.getTablePlain();
++        final int hash = SWMRHashTable.getHash(key);
++        final int index = hash & (table.length - 1);
++
++        final TableEntry<K, V> head = table[index];
++        if (head == null) {
++            return false;
++        }
++
++        if (head.hash == hash && (head.key == key || head.key.equals(key))) {
++            final V currVal = head.getValuePlain();
++
++            if (currVal != value && !currVal.equals(value)) {
++                return false;
++            }
++
++            ArrayUtil.setRelease(table, index, head.getNextPlain());
++            this.removeFromSize(1);
++
++            return true;
++        }
++
++        for (TableEntry<K, V> curr = head.getNextPlain(), prev = head; curr != null; prev = curr, curr = curr.getNextPlain()) {
++            if (curr.hash == hash && (curr.key == key || curr.key.equals(key))) {
++                final V currVal = curr.getValuePlain();
++
++                if (currVal != value && !currVal.equals(value)) {
++                    return false;
++                }
++
++                prev.setNextRelease(curr.getNextPlain());
++                this.removeFromSize(1);
++
++                return true;
++            }
++        }
++
++        return false;
++    }
++
++    protected final V remove(final Object key, final int hash) {
++        final TableEntry<K, V>[] table = this.getTablePlain();
++        final int index = (table.length - 1) & hash;
++
++        final TableEntry<K, V> head = table[index];
++        if (head == null) {
++            return null;
++        }
++
++        if (hash == head.hash && (head.key == key || head.key.equals(key))) {
++            ArrayUtil.setRelease(table, index, head.getNextPlain());
++            this.removeFromSize(1);
++
++            return head.getValuePlain();
++        }
++
++        for (TableEntry<K, V> curr = head.getNextPlain(), prev = head; curr != null; prev = curr, curr = curr.getNextPlain()) {
++            if (curr.hash == hash && (key == curr.key || curr.key.equals(key))) {
++                prev.setNextRelease(curr.getNextPlain());
++                this.removeFromSize(1);
++
++                return curr.getValuePlain();
++            }
++        }
++
++        return null;
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    @Override
++    public V remove(final Object key) {
++        Validate.notNull(key, "Null key");
++
++        return this.remove(key, SWMRHashTable.getHash(key));
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    @Override
++    public boolean replace(final K key, final V oldValue, final V newValue) {
++        Validate.notNull(key, "Null key");
++        Validate.notNull(oldValue, "Null oldValue");
++        Validate.notNull(newValue, "Null newValue");
++
++        final TableEntry<K, V> entry = this.getEntryForPlain(key);
++        if (entry == null) {
++            return false;
++        }
++
++        final V currValue = entry.getValuePlain();
++        if (currValue == oldValue || currValue.equals(oldValue)) {
++            entry.setValueRelease(newValue);
++            return true;
++        }
++
++        return false;
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    @Override
++    public V replace(final K key, final V value) {
++        Validate.notNull(key, "Null key");
++        Validate.notNull(value, "Null value");
++
++        final TableEntry<K, V> entry = this.getEntryForPlain(key);
++        if (entry == null) {
++            return null;
++        }
++
++        final V prev = entry.getValuePlain();
++        entry.setValueRelease(value);
++        return prev;
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    @Override
++    public void replaceAll(final BiFunction<? super K, ? super V, ? extends V> function) {
++        Validate.notNull(function, "Null function");
++
++        final TableEntry<K, V>[] table = this.getTablePlain();
++        for (int i = 0, len = table.length; i < len; ++i) {
++            for (TableEntry<K, V> curr = table[i]; curr != null; curr = curr.getNextPlain()) {
++                final V value = curr.getValuePlain();
++
++                final V newValue = function.apply(curr.key, value);
++                if (newValue == null) {
++                    throw new NullPointerException();
++                }
++
++                curr.setValueRelease(newValue);
++            }
++        }
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    @Override
++    public void putAll(final Map<? extends K, ? extends V> map) {
++        Validate.notNull(map, "Null map");
++
++        final int size = map.size();
++        this.checkResize(Math.max(this.getSizePlain() + size/2, size)); /* preemptively resize */
++        map.forEach(this::put);
++    }
++
++    /**
++     * {@inheritDoc}
++     * <p>
++     * This call is non-atomic and the order that which entries are removed is undefined. The clear operation itself
++     * is release ordered, that is, after the clear operation is performed a release fence is performed.
++     * </p>
++     */
++    @Override
++    public void clear() {
++        Arrays.fill(this.getTablePlain(), null);
++        this.setSizeRelease(0);
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    @Override
++    public V compute(final K key, final BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
++        Validate.notNull(key, "Null key");
++        Validate.notNull(remappingFunction, "Null remappingFunction");
++
++        final int hash = SWMRHashTable.getHash(key);
++        final TableEntry<K, V>[] table = this.getTablePlain();
++        final int index = hash & (table.length - 1);
++
++        for (TableEntry<K, V> curr = table[index], prev = null;;prev = curr, curr = curr.getNextPlain()) {
++            if (curr == null) {
++                final V newVal = remappingFunction.apply(key ,null);
++
++                if (newVal == null) {
++                    return null;
++                }
++
++                final TableEntry<K, V> insert = new TableEntry<>(hash, key, newVal);
++                if (prev == null) {
++                    ArrayUtil.setRelease(table, index, insert);
++                } else {
++                    prev.setNextRelease(insert);
++                }
++
++                this.addToSize(1);
++
++                return newVal;
++            }
++
++            if (curr.hash == hash && (curr.key == key || curr.key.equals(key))) {
++                final V newVal = remappingFunction.apply(key, curr.getValuePlain());
++
++                if (newVal != null) {
++                    curr.setValueRelease(newVal);
++                    return newVal;
++                }
++
++                if (prev == null) {
++                    ArrayUtil.setRelease(table, index, curr.getNextPlain());
++                } else {
++                    prev.setNextRelease(curr.getNextPlain());
++                }
++
++                this.removeFromSize(1);
++
++                return null;
++            }
++        }
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    @Override
++    public V computeIfPresent(final K key, final BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
++        Validate.notNull(key, "Null key");
++        Validate.notNull(remappingFunction, "Null remappingFunction");
++
++        final int hash = SWMRHashTable.getHash(key);
++        final TableEntry<K, V>[] table = this.getTablePlain();
++        final int index = hash & (table.length - 1);
++
++        for (TableEntry<K, V> curr = table[index], prev = null; curr != null; prev = curr, curr = curr.getNextPlain()) {
++            if (curr.hash != hash || (curr.key != key && !curr.key.equals(key))) {
++                continue;
++            }
++
++            final V newVal = remappingFunction.apply(key, curr.getValuePlain());
++            if (newVal != null) {
++                curr.setValueRelease(newVal);
++                return newVal;
++            }
++
++            if (prev == null) {
++                ArrayUtil.setRelease(table, index, curr.getNextPlain());
++            } else {
++                prev.setNextRelease(curr.getNextPlain());
++            }
++
++            this.removeFromSize(1);
++
++            return null;
++        }
++
++        return null;
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    @Override
++    public V computeIfAbsent(final K key, final Function<? super K, ? extends V> mappingFunction) {
++        Validate.notNull(key, "Null key");
++        Validate.notNull(mappingFunction, "Null mappingFunction");
++
++        final int hash = SWMRHashTable.getHash(key);
++        final TableEntry<K, V>[] table = this.getTablePlain();
++        final int index = hash & (table.length - 1);
++
++        for (TableEntry<K, V> curr = table[index], prev = null;;prev = curr, curr = curr.getNextPlain()) {
++            if (curr != null) {
++                if (curr.hash == hash && (curr.key == key || curr.key.equals(key))) {
++                    return curr.getValuePlain();
++                }
++                continue;
++            }
++
++            final V newVal = mappingFunction.apply(key);
++
++            if (newVal == null) {
++                return null;
++            }
++
++            final TableEntry<K, V> insert = new TableEntry<>(hash, key, newVal);
++            if (prev == null) {
++                ArrayUtil.setRelease(table, index, insert);
++            } else {
++                prev.setNextRelease(insert);
++            }
++
++            this.addToSize(1);
++
++            return newVal;
++        }
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    @Override
++    public V merge(final K key, final V value, final BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
++        Validate.notNull(key, "Null key");
++        Validate.notNull(value, "Null value");
++        Validate.notNull(remappingFunction, "Null remappingFunction");
++
++        final int hash = SWMRHashTable.getHash(key);
++        final TableEntry<K, V>[] table = this.getTablePlain();
++        final int index = hash & (table.length - 1);
++
++        for (TableEntry<K, V> curr = table[index], prev = null;;prev = curr, curr = curr.getNextPlain()) {
++            if (curr == null) {
++                final TableEntry<K, V> insert = new TableEntry<>(hash, key, value);
++                if (prev == null) {
++                    ArrayUtil.setRelease(table, index, insert);
++                } else {
++                    prev.setNextRelease(insert);
++                }
++
++                this.addToSize(1);
++
++                return value;
++            }
++
++            if (curr.hash == hash && (curr.key == key || curr.key.equals(key))) {
++                final V newVal = remappingFunction.apply(curr.getValuePlain(), value);
++
++                if (newVal != null) {
++                    curr.setValueRelease(newVal);
++                    return newVal;
++                }
++
++                if (prev == null) {
++                    ArrayUtil.setRelease(table, index, curr.getNextPlain());
++                } else {
++                    prev.setNextRelease(curr.getNextPlain());
++                }
++
++                this.removeFromSize(1);
++
++                return null;
++            }
++        }
++    }
++
++    protected static final class TableEntry<K, V> implements Map.Entry<K, V> {
++
++        protected final int hash;
++        protected final K key;
++        protected V value;
++
++        protected TableEntry<K, V> next;
++
++        protected static final VarHandle VALUE_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "value", Object.class);
++        protected static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "next", TableEntry.class);
++
++        /* value */
++
++        protected final V getValuePlain() {
++            //noinspection unchecked
++            return (V)VALUE_HANDLE.get(this);
++        }
++
++        protected final V getValueAcquire() {
++            //noinspection unchecked
++            return (V)VALUE_HANDLE.getAcquire(this);
++        }
++
++        protected final void setValueRelease(final V to) {
++            VALUE_HANDLE.setRelease(this, to);
++        }
++
++        /* next */
++
++        protected final TableEntry<K, V> getNextPlain() {
++            //noinspection unchecked
++            return (TableEntry<K, V>)NEXT_HANDLE.get(this);
++        }
++
++        protected final TableEntry<K, V> getNextOpaque() {
++            //noinspection unchecked
++            return (TableEntry<K, V>)NEXT_HANDLE.getOpaque(this);
++        }
++
++        protected final void setNextPlain(final TableEntry<K, V> next) {
++            NEXT_HANDLE.set(this, next);
++        }
++
++        protected final void setNextRelease(final TableEntry<K, V> next) {
++            NEXT_HANDLE.setRelease(this, next);
++        }
++
++        protected TableEntry(final int hash, final K key, final V value) {
++            this.hash = hash;
++            this.key = key;
++            this.value = value;
++        }
++
++        /**
++         * {@inheritDoc}
++         */
++        @Override
++        public K getKey() {
++            return this.key;
++        }
++
++        /**
++         * {@inheritDoc}
++         */
++        @Override
++        public V getValue() {
++            return this.getValueAcquire();
++        }
++
++        /**
++         * {@inheritDoc}
++         */
++        @Override
++        public V setValue(final V value) {
++            if (value == null) {
++                throw new NullPointerException();
++            }
++
++            final V curr = this.getValuePlain();
++
++            this.setValueRelease(value);
++            return curr;
++        }
++
++        protected static int hash(final Object key, final Object value) {
++            return key.hashCode() ^ (value == null ? 0 : value.hashCode());
++        }
++
++        /**
++         * {@inheritDoc}
++         */
++        @Override
++        public int hashCode() {
++            return hash(this.key, this.getValueAcquire());
++        }
++
++        /**
++         * {@inheritDoc}
++         */
++        @Override
++        public boolean equals(final Object obj) {
++            if (this == obj) {
++                return true;
++            }
++
++            if (!(obj instanceof Map.Entry)) {
++                return false;
++            }
++            final Map.Entry<?, ?> other = (Map.Entry<?, ?>)obj;
++            final Object otherKey = other.getKey();
++            final Object otherValue = other.getValue();
++
++            final K thisKey = this.getKey();
++            final V thisVal = this.getValueAcquire();
++            return (thisKey == otherKey || thisKey.equals(otherKey)) &&
++                (thisVal == otherValue || thisVal.equals(otherValue));
++        }
++    }
++
++
++    protected static abstract class TableEntryIterator<K, V, T> implements Iterator<T> {
++
++        protected final TableEntry<K, V>[] table;
++        protected final SWMRHashTable<K, V> map;
++
++        /* bin which our current element resides on */
++        protected int tableIndex;
++
++        protected TableEntry<K, V> currEntry; /* curr entry, null if no more to iterate or if curr was removed or if we've just init'd */
++        protected TableEntry<K, V> nextEntry; /* may not be on the same bin as currEntry */
++
++        protected TableEntryIterator(final TableEntry<K, V>[] table, final SWMRHashTable<K, V> map) {
++            this.table = table;
++            this.map = map;
++            int tableIndex = 0;
++            for (int len = table.length; tableIndex < len; ++tableIndex) {
++                final TableEntry<K, V> entry = ArrayUtil.getOpaque(table, tableIndex);
++                if (entry != null) {
++                    this.nextEntry = entry;
++                    this.tableIndex = tableIndex + 1;
++                    return;
++                }
++            }
++            this.tableIndex = tableIndex;
++        }
++
++        @Override
++        public boolean hasNext() {
++            return this.nextEntry != null;
++        }
++
++        protected final TableEntry<K, V> advanceEntry() {
++            final TableEntry<K, V>[] table = this.table;
++            final int tableLength = table.length;
++            int tableIndex = this.tableIndex;
++            final TableEntry<K, V> curr = this.nextEntry;
++            if (curr == null) {
++                return null;
++            }
++
++            this.currEntry = curr;
++
++            // set up nextEntry
++
++            // find next in chain
++            TableEntry<K, V> next = curr.getNextOpaque();
++
++            if (next != null) {
++                this.nextEntry = next;
++                return curr;
++            }
++
++            // nothing in chain, so find next available bin
++            for (;tableIndex < tableLength; ++tableIndex) {
++                next = ArrayUtil.getOpaque(table, tableIndex);
++                if (next != null) {
++                    this.nextEntry = next;
++                    this.tableIndex = tableIndex + 1;
++                    return curr;
++                }
++            }
++
++            this.nextEntry = null;
++            this.tableIndex = tableIndex;
++            return curr;
++        }
++
++        @Override
++        public void remove() {
++            final TableEntry<K, V> curr = this.currEntry;
++            if (curr == null) {
++                throw new IllegalStateException();
++            }
++
++            this.map.remove(curr.key, curr.hash);
++
++            this.currEntry = null;
++        }
++    }
++
++    protected static final class ValueIterator<K, V> extends TableEntryIterator<K, V, V> {
++
++        protected ValueIterator(final TableEntry<K, V>[] table, final SWMRHashTable<K, V> map) {
++            super(table, map);
++        }
++
++        @Override
++        public V next() {
++            final TableEntry<K, V> entry = this.advanceEntry();
++
++            if (entry == null) {
++                throw new NoSuchElementException();
++            }
++
++            return entry.getValueAcquire();
++        }
++    }
++
++    protected static final class KeyIterator<K, V> extends TableEntryIterator<K, V, K> {
++
++        protected KeyIterator(final TableEntry<K, V>[] table, final SWMRHashTable<K, V> map) {
++            super(table, map);
++        }
++
++        @Override
++        public K next() {
++            final TableEntry<K, V> curr = this.advanceEntry();
++
++            if (curr == null) {
++                throw new NoSuchElementException();
++            }
++
++            return curr.key;
++        }
++    }
++
++    protected static final class EntryIterator<K, V> extends TableEntryIterator<K, V, Map.Entry<K, V>> {
++
++        protected EntryIterator(final TableEntry<K, V>[] table, final SWMRHashTable<K, V> map) {
++            super(table, map);
++        }
++
++        @Override
++        public Map.Entry<K, V> next() {
++            final TableEntry<K, V> curr = this.advanceEntry();
++
++            if (curr == null) {
++                throw new NoSuchElementException();
++            }
++
++            return curr;
++        }
++    }
++
++    protected static abstract class ViewCollection<K, V, T> implements Collection<T> {
++
++        protected final SWMRHashTable<K, V> map;
++
++        protected ViewCollection(final SWMRHashTable<K, V> map) {
++            this.map = map;
++        }
++
++        @Override
++        public boolean add(final T element) {
++            throw new UnsupportedOperationException();
++        }
++
++        @Override
++        public boolean addAll(final Collection<? extends T> collections) {
++            throw new UnsupportedOperationException();
++        }
++
++        @Override
++        public boolean removeAll(final Collection<?> collection) {
++            Validate.notNull(collection, "Null collection");
++
++            boolean modified = false;
++            for (final Object element : collection) {
++                modified |= this.remove(element);
++            }
++            return modified;
++        }
++
++        @Override
++        public int size() {
++            return this.map.size();
++        }
++
++        @Override
++        public boolean isEmpty() {
++            return this.size() == 0;
++        }
++
++        @Override
++        public void clear() {
++            this.map.clear();
++        }
++
++        @Override
++        public boolean containsAll(final Collection<?> collection) {
++            Validate.notNull(collection, "Null collection");
++
++            for (final Object element : collection) {
++                if (!this.contains(element)) {
++                    return false;
++                }
++            }
++
++            return true;
++        }
++
++        @Override
++        public Object[] toArray() {
++            final List<T> list = new ArrayList<>(this.size());
++
++            this.forEach(list::add);
++
++            return list.toArray();
++        }
++
++        @Override
++        public <E> E[] toArray(final E[] array) {
++            final List<T> list = new ArrayList<>(this.size());
++
++            this.forEach(list::add);
++
++            return list.toArray(array);
++        }
++
++        @Override
++        public <E> E[] toArray(final IntFunction<E[]> generator) {
++            final List<T> list = new ArrayList<>(this.size());
++
++            this.forEach(list::add);
++
++            return list.toArray(generator);
++        }
++
++        @Override
++        public int hashCode() {
++            int hash = 0;
++            for (final T element : this) {
++                hash += element == null ? 0 : element.hashCode();
++            }
++            return hash;
++        }
++
++        @Override
++        public Spliterator<T> spliterator() { // TODO implement
++            return Spliterators.spliterator(this, Spliterator.NONNULL);
++        }
++    }
++
++    protected static abstract class ViewSet<K, V, T> extends ViewCollection<K, V, T> implements Set<T> {
++
++        protected ViewSet(final SWMRHashTable<K, V> map) {
++            super(map);
++        }
++
++        @Override
++        public boolean equals(final Object obj) {
++            if (this == obj) {
++                return true;
++            }
++
++            if (!(obj instanceof Set)) {
++                return false;
++            }
++
++            final Set<?> other = (Set<?>)obj;
++            if (other.size() != this.size()) {
++                return false;
++            }
++
++            return this.containsAll(other);
++        }
++    }
++
++    protected static final class EntrySet<K, V> extends ViewSet<K, V, Map.Entry<K, V>> implements Set<Map.Entry<K, V>> {
++
++        protected EntrySet(final SWMRHashTable<K, V> map) {
++            super(map);
++        }
++
++        @Override
++        public boolean remove(final Object object) {
++            if (!(object instanceof Map.Entry<?, ?>)) {
++                return false;
++            }
++            final Map.Entry<?, ?> entry = (Map.Entry<?, ?>)object;
++
++            final Object key;
++            final Object value;
++
++            try {
++                key = entry.getKey();
++                value = entry.getValue();
++            } catch (final IllegalStateException ex) {
++                return false;
++            }
++
++            return this.map.remove(key, value);
++        }
++
++        @Override
++        public boolean removeIf(final Predicate<? super Map.Entry<K, V>> filter) {
++            Validate.notNull(filter, "Null filter");
++
++            return this.map.removeEntryIf(filter) != 0;
++        }
++
++        @Override
++        public boolean retainAll(final Collection<?> collection) {
++            Validate.notNull(collection, "Null collection");
++
++            return this.map.removeEntryIf((final Map.Entry<K, V> entry) -> {
++                return !collection.contains(entry);
++            }) != 0;
++        }
++
++        @Override
++        public Iterator<Entry<K, V>> iterator() {
++            return new EntryIterator<>(this.map.getTableAcquire(), this.map);
++        }
++
++        @Override
++        public void forEach(final Consumer<? super Entry<K, V>> action) {
++            this.map.forEach(action);
++        }
++
++        @Override
++        public boolean contains(final Object object) {
++            if (!(object instanceof Map.Entry)) {
++                return false;
++            }
++            final Map.Entry<?, ?> entry = (Map.Entry<?, ?>)object;
++
++            final Object key;
++            final Object value;
++
++            try {
++                key = entry.getKey();
++                value = entry.getValue();
++            } catch (final IllegalStateException ex) {
++                return false;
++            }
++
++            return this.map.contains(key, value);
++        }
++
++        @Override
++        public String toString() {
++            return CollectionUtil.toString(this, "SWMRHashTableEntrySet");
++        }
++    }
++
++    protected static final class KeySet<K, V> extends ViewSet<K, V, K> {
++
++        protected KeySet(final SWMRHashTable<K, V> map) {
++            super(map);
++        }
++
++        @Override
++        public Iterator<K> iterator() {
++            return new KeyIterator<>(this.map.getTableAcquire(), this.map);
++        }
++
++        @Override
++        public void forEach(final Consumer<? super K> action) {
++            Validate.notNull(action, "Null action");
++
++            this.map.forEachKey(action);
++        }
++
++        @Override
++        public boolean contains(final Object key) {
++            Validate.notNull(key, "Null key");
++
++            return this.map.containsKey(key);
++        }
++
++        @Override
++        public boolean remove(final Object key) {
++            Validate.notNull(key, "Null key");
++
++            return this.map.remove(key) != null;
++        }
++
++        @Override
++        public boolean retainAll(final Collection<?> collection) {
++            Validate.notNull(collection, "Null collection");
++
++            return this.map.removeIf((final K key, final V value) -> {
++                return !collection.contains(key);
++            }) != 0;
++        }
++
++        @Override
++        public boolean removeIf(final Predicate<? super K> filter) {
++            Validate.notNull(filter, "Null filter");
++
++            return this.map.removeIf((final K key, final V value) -> {
++                return filter.test(key);
++            }) != 0;
++        }
++
++        @Override
++        public String toString() {
++            return CollectionUtil.toString(this, "SWMRHashTableKeySet");
++        }
++    }
++
++    protected static final class ValueCollection<K, V> extends ViewSet<K, V, V> implements Collection<V> {
++
++        protected ValueCollection(final SWMRHashTable<K, V> map) {
++            super(map);
++        }
++
++        @Override
++        public Iterator<V> iterator() {
++            return new ValueIterator<>(this.map.getTableAcquire(), this.map);
++        }
++
++        @Override
++        public void forEach(final Consumer<? super V> action) {
++            Validate.notNull(action, "Null action");
++
++            this.map.forEachValue(action);
++        }
++
++        @Override
++        public boolean contains(final Object object) {
++            Validate.notNull(object, "Null object");
++
++            return this.map.containsValue(object);
++        }
++
++        @Override
++        public boolean remove(final Object object) {
++            Validate.notNull(object, "Null object");
++
++            final Iterator<V> itr = this.iterator();
++            while (itr.hasNext()) {
++                final V val = itr.next();
++                if (val == object || val.equals(object)) {
++                    itr.remove();
++                    return true;
++                }
++            }
++
++            return false;
++        }
++
++        @Override
++        public boolean removeIf(final Predicate<? super V> filter) {
++            Validate.notNull(filter, "Null filter");
++
++            return this.map.removeIf((final K key, final V value) -> {
++                return filter.test(value);
++            }) != 0;
++        }
++
++        @Override
++        public boolean retainAll(final Collection<?> collection) {
++            Validate.notNull(collection, "Null collection");
++
++            return this.map.removeIf((final K key, final V value) -> {
++                return !collection.contains(value);
++            }) != 0;
++        }
++
++        @Override
++        public String toString() {
++            return CollectionUtil.toString(this, "SWMRHashTableValues");
++        }
++    }
++}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRLong2ObjectHashTable.java b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRLong2ObjectHashTable.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/map/SWMRLong2ObjectHashTable.java
+@@ -0,0 +0,0 @@
++package ca.spottedleaf.concurrentutil.map;
++
++import ca.spottedleaf.concurrentutil.util.ArrayUtil;
++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
++import ca.spottedleaf.concurrentutil.util.Validate;
++import io.papermc.paper.util.IntegerUtil;
++import java.lang.invoke.VarHandle;
++import java.util.Arrays;
++import java.util.function.Consumer;
++import java.util.function.LongConsumer;
++
++// trimmed down version of SWMRHashTable
++public class SWMRLong2ObjectHashTable<V> {
++
++    protected int size;
++
++    protected TableEntry<V>[] table;
++
++    protected final float loadFactor;
++
++    protected static final VarHandle SIZE_HANDLE = ConcurrentUtil.getVarHandle(SWMRLong2ObjectHashTable.class, "size", int.class);
++    protected static final VarHandle TABLE_HANDLE = ConcurrentUtil.getVarHandle(SWMRLong2ObjectHashTable.class, "table", TableEntry[].class);
++
++    /* size */
++
++    protected final int getSizePlain() {
++        return (int)SIZE_HANDLE.get(this);
++    }
++
++    protected final int getSizeOpaque() {
++        return (int)SIZE_HANDLE.getOpaque(this);
++    }
++
++    protected final int getSizeAcquire() {
++        return (int)SIZE_HANDLE.getAcquire(this);
++    }
++
++    protected final void setSizePlain(final int value) {
++        SIZE_HANDLE.set(this, value);
++    }
++
++    protected final void setSizeOpaque(final int value) {
++        SIZE_HANDLE.setOpaque(this, value);
++    }
++
++    protected final void setSizeRelease(final int value) {
++        SIZE_HANDLE.setRelease(this, value);
++    }
++
++    /* table */
++
++    protected final TableEntry<V>[] getTablePlain() {
++        //noinspection unchecked
++        return (TableEntry<V>[])TABLE_HANDLE.get(this);
++    }
++
++    protected final TableEntry<V>[] getTableAcquire() {
++        //noinspection unchecked
++        return (TableEntry<V>[])TABLE_HANDLE.getAcquire(this);
++    }
++
++    protected final void setTablePlain(final TableEntry<V>[] table) {
++        TABLE_HANDLE.set(this, table);
++    }
++
++    protected final void setTableRelease(final TableEntry<V>[] table) {
++        TABLE_HANDLE.setRelease(this, table);
++    }
++
++    protected static final int DEFAULT_CAPACITY = 16;
++    protected static final float DEFAULT_LOAD_FACTOR = 0.75f;
++    protected static final int MAXIMUM_CAPACITY = Integer.MIN_VALUE >>> 1;
++
++    /**
++     * Constructs this map with a capacity of {@code 16} and load factor of {@code 0.75f}.
++     */
++    public SWMRLong2ObjectHashTable() {
++        this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR);
++    }
++
++    /**
++     * Constructs this map with the specified capacity and load factor of {@code 0.75f}.
++     * @param capacity specified initial capacity, > 0
++     */
++    public SWMRLong2ObjectHashTable(final int capacity) {
++        this(capacity, DEFAULT_LOAD_FACTOR);
++    }
++
++    /**
++     * Constructs this map with the specified capacity and load factor.
++     * @param capacity specified capacity, > 0
++     * @param loadFactor specified load factor, > 0 && finite
++     */
++    public SWMRLong2ObjectHashTable(final int capacity, final float loadFactor) {
++        final int tableSize = getCapacityFor(capacity);
++
++        if (loadFactor <= 0.0 || !Float.isFinite(loadFactor)) {
++            throw new IllegalArgumentException("Invalid load factor: " + loadFactor);
++        }
++
++        //noinspection unchecked
++        final TableEntry<V>[] table = new TableEntry[tableSize];
++        this.setTablePlain(table);
++
++        if (tableSize == MAXIMUM_CAPACITY) {
++            this.threshold = -1;
++        } else {
++            this.threshold = getTargetCapacity(tableSize, loadFactor);
++        }
++
++        this.loadFactor = loadFactor;
++    }
++
++    /**
++     * Constructs this map with a capacity of {@code 16} or the specified map's size, whichever is larger, and
++     * with a load factor of {@code 0.75f}.
++     * All of the specified map's entries are copied into this map.
++     * @param other The specified map.
++     */
++    public SWMRLong2ObjectHashTable(final SWMRLong2ObjectHashTable<V> other) {
++        this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR, other);
++    }
++
++    /**
++     * Constructs this map with a minimum capacity of the specified capacity or the specified map's size, whichever is larger, and
++     * with a load factor of {@code 0.75f}.
++     * All of the specified map's entries are copied into this map.
++     * @param capacity specified capacity, > 0
++     * @param other The specified map.
++     */
++    public SWMRLong2ObjectHashTable(final int capacity, final SWMRLong2ObjectHashTable<V> other) {
++        this(capacity, DEFAULT_LOAD_FACTOR, other);
++    }
++
++    /**
++     * Constructs this map with a min capacity of the specified capacity or the specified map's size, whichever is larger, and
++     * with the specified load factor.
++     * All of the specified map's entries are copied into this map.
++     * @param capacity specified capacity, > 0
++     * @param loadFactor specified load factor, > 0 && finite
++     * @param other The specified map.
++     */
++    public SWMRLong2ObjectHashTable(final int capacity, final float loadFactor, final SWMRLong2ObjectHashTable<V> other) {
++        this(Math.max(Validate.notNull(other, "Null map").size(), capacity), loadFactor);
++        this.putAll(other);
++    }
++
++    public final float getLoadFactor() {
++        return this.loadFactor;
++    }
++
++    protected static int getCapacityFor(final int capacity) {
++        if (capacity <= 0) {
++            throw new IllegalArgumentException("Invalid capacity: " + capacity);
++        }
++        if (capacity >= MAXIMUM_CAPACITY) {
++            return MAXIMUM_CAPACITY;
++        }
++        return IntegerUtil.roundCeilLog2(capacity);
++    }
++
++    /** Callers must still use acquire when reading the value of the entry. */
++    protected final TableEntry<V> getEntryForOpaque(final long key) {
++        final int hash = SWMRLong2ObjectHashTable.getHash(key);
++        final TableEntry<V>[] table = this.getTableAcquire();
++
++        for (TableEntry<V> curr = ArrayUtil.getOpaque(table, hash & (table.length - 1)); curr != null; curr = curr.getNextOpaque()) {
++            if (key == curr.key) {
++                return curr;
++            }
++        }
++
++        return null;
++    }
++
++    protected final TableEntry<V> getEntryForPlain(final long key) {
++        final int hash = SWMRLong2ObjectHashTable.getHash(key);
++        final TableEntry<V>[] table = this.getTablePlain();
++
++        for (TableEntry<V> curr = table[hash & (table.length - 1)]; curr != null; curr = curr.getNextPlain()) {
++            if (key == curr.key) {
++                return curr;
++            }
++        }
++
++        return null;
++    }
++
++    /* MT-Safe */
++
++    /** must be deterministic given a key */
++    protected static int getHash(final long key) {
++        return (int)it.unimi.dsi.fastutil.HashCommon.mix(key);
++    }
++
++    // rets -1 if capacity*loadFactor is too large
++    protected static int getTargetCapacity(final int capacity, final float loadFactor) {
++        final double ret = (double)capacity * (double)loadFactor;
++        if (Double.isInfinite(ret) || ret >= ((double)Integer.MAX_VALUE)) {
++            return -1;
++        }
++
++        return (int)ret;
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    @Override
++    public boolean equals(final Object obj) {
++        if (this == obj) {
++            return true;
++        }
++        /* Make no attempt to deal with concurrent modifications */
++        if (!(obj instanceof SWMRLong2ObjectHashTable)) {
++            return false;
++        }
++        final SWMRLong2ObjectHashTable<?> other = (SWMRLong2ObjectHashTable<?>)obj;
++
++        if (this.size() != other.size()) {
++            return false;
++        }
++
++        final TableEntry<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()) {
++                final V value = curr.getValueAcquire();
++
++                final Object otherValue = other.get(curr.key);
++                if (otherValue == null || (value != otherValue && value.equals(otherValue))) {
++                    return false;
++                }
++            }
++        }
++
++        return true;
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    @Override
++    public int hashCode() {
++        /* Make no attempt to deal with concurrent modifications */
++        int hash = 0;
++        final TableEntry<V>[] table = this.getTableAcquire();
++
++        for (int i = 0, len = table.length; i < len; ++i) {
++            for (TableEntry<V> curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
++                hash += curr.hashCode();
++            }
++        }
++
++        return hash;
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    @Override
++    public String toString() {
++        final StringBuilder builder = new StringBuilder(64);
++        builder.append("SingleWriterMultiReaderHashMap:{");
++
++        this.forEach((final long key, final V value) -> {
++            builder.append("{key: \"").append(key).append("\", value: \"").append(value).append("\"}");
++        });
++
++        return builder.append('}').toString();
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    @Override
++    public SWMRLong2ObjectHashTable<V> clone() {
++        return new SWMRLong2ObjectHashTable<>(this.getTableAcquire().length, this.loadFactor, this);
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    public void forEach(final Consumer<? super SWMRLong2ObjectHashTable.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()) {
++                action.accept(curr);
++            }
++        }
++    }
++
++    @FunctionalInterface
++    public static interface BiLongObjectConsumer<V> {
++        public void accept(final long key, final V value);
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    public void forEach(final BiLongObjectConsumer<? super V> action) {
++        Validate.notNull(action, "Null action");
++
++        final TableEntry<V>[] table = this.getTableAcquire();
++        for (int i = 0, len = table.length; i < len; ++i) {
++            for (TableEntry<V> curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
++                final V value = curr.getValueAcquire();
++
++                action.accept(curr.key, value);
++            }
++        }
++    }
++
++    /**
++     * Provides the specified consumer with all keys contained within this map.
++     * @param action The specified consumer.
++     */
++    public void forEachKey(final LongConsumer action) {
++        Validate.notNull(action, "Null action");
++
++        final TableEntry<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()) {
++                action.accept(curr.key);
++            }
++        }
++    }
++
++    /**
++     * Provides the specified consumer with all values contained within this map. Equivalent to {@code map.values().forEach(Consumer)}.
++     * @param action The specified consumer.
++     */
++    public void forEachValue(final Consumer<? super V> action) {
++        Validate.notNull(action, "Null action");
++
++        final TableEntry<V>[] table = this.getTableAcquire();
++        for (int i = 0, len = table.length; i < len; ++i) {
++            for (TableEntry<V> curr = ArrayUtil.getOpaque(table, i); curr != null; curr = curr.getNextOpaque()) {
++                final V value = curr.getValueAcquire();
++
++                action.accept(value);
++            }
++        }
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    public V get(final long key) {
++        final TableEntry<V> entry = this.getEntryForOpaque(key);
++        return entry == null ? null : entry.getValueAcquire();
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    public boolean containsKey(final long key) {
++        // note: we need to use getValueAcquire, so that the reads from this map are ordered by acquire semantics
++        return this.get(key) != null;
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    public V getOrDefault(final long key, final V defaultValue) {
++        final TableEntry<V> entry = this.getEntryForOpaque(key);
++
++        return entry == null ? defaultValue : entry.getValueAcquire();
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    public int size() {
++        return this.getSizeAcquire();
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    public boolean isEmpty() {
++        return this.getSizeAcquire() == 0;
++    }
++
++    /* Non-MT-Safe */
++
++    protected int threshold;
++
++    protected final void checkResize(final int minCapacity) {
++        if (minCapacity <= this.threshold || this.threshold < 0) {
++            return;
++        }
++
++        final TableEntry<V>[] table = this.getTablePlain();
++        int newCapacity = minCapacity >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY : IntegerUtil.roundCeilLog2(minCapacity);
++        if (newCapacity < 0) {
++            newCapacity = MAXIMUM_CAPACITY;
++        }
++        if (newCapacity <= table.length) {
++            if (newCapacity == MAXIMUM_CAPACITY) {
++                return;
++            }
++            newCapacity = table.length << 1;
++        }
++
++        //noinspection unchecked
++        final TableEntry<V>[] newTable = new TableEntry[newCapacity];
++        final int indexMask = newCapacity - 1;
++
++        for (int i = 0, len = table.length; i < len; ++i) {
++            for (TableEntry<V> entry = table[i]; entry != null; entry = entry.getNextPlain()) {
++                final long key = entry.key;
++                final int hash = SWMRLong2ObjectHashTable.getHash(key);
++                final int index = hash & indexMask;
++
++                /* we need to create a new entry since there could be reading threads */
++                final TableEntry<V> insert = new TableEntry<>(key, entry.getValuePlain());
++
++                final TableEntry<V> prev = newTable[index];
++
++                newTable[index] = insert;
++                insert.setNextPlain(prev);
++            }
++        }
++
++        if (newCapacity == MAXIMUM_CAPACITY) {
++            this.threshold = -1; /* No more resizing */
++        } else {
++            this.threshold = getTargetCapacity(newCapacity, this.loadFactor);
++        }
++        this.setTableRelease(newTable); /* use release to publish entries in table */
++    }
++
++    protected final int addToSize(final int num) {
++        final int newSize = this.getSizePlain() + num;
++
++        this.setSizeOpaque(newSize);
++        this.checkResize(newSize);
++
++        return newSize;
++    }
++
++    protected final int removeFromSize(final int num) {
++        final int newSize = this.getSizePlain() - num;
++
++        this.setSizeOpaque(newSize);
++
++        return newSize;
++    }
++
++    protected final V put(final long key, final V value, final boolean onlyIfAbsent) {
++        final TableEntry<V>[] table = this.getTablePlain();
++        final int hash = SWMRLong2ObjectHashTable.getHash(key);
++        final int index = hash & (table.length - 1);
++
++        final TableEntry<V> head = table[index];
++        if (head == null) {
++            final TableEntry<V> insert = new TableEntry<>(key, value);
++            ArrayUtil.setRelease(table, index, insert);
++            this.addToSize(1);
++            return null;
++        }
++
++        for (TableEntry<V> curr = head;;) {
++            if (key == curr.key) {
++                if (onlyIfAbsent) {
++                    return curr.getValuePlain();
++                }
++
++                final V currVal = curr.getValuePlain();
++                curr.setValueRelease(value);
++                return currVal;
++            }
++
++            final TableEntry<V> next = curr.getNextPlain();
++            if (next != null) {
++                curr = next;
++                continue;
++            }
++
++            final TableEntry<V> insert = new TableEntry<>(key, value);
++
++            curr.setNextRelease(insert);
++            this.addToSize(1);
++            return null;
++        }
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    public V put(final long key, final V value) {
++        Validate.notNull(value, "Null value");
++
++        return this.put(key, value, false);
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    public V putIfAbsent(final long key, final V value) {
++        Validate.notNull(value, "Null value");
++
++        return this.put(key, value, true);
++    }
++
++    protected final V remove(final long key, final int hash) {
++        final TableEntry<V>[] table = this.getTablePlain();
++        final int index = (table.length - 1) & hash;
++
++        final TableEntry<V> head = table[index];
++        if (head == null) {
++            return null;
++        }
++
++        if (head.key == key) {
++            ArrayUtil.setRelease(table, index, head.getNextPlain());
++            this.removeFromSize(1);
++
++            return head.getValuePlain();
++        }
++
++        for (TableEntry<V> curr = head.getNextPlain(), prev = head; curr != null; prev = curr, curr = curr.getNextPlain()) {
++            if (key == curr.key) {
++                prev.setNextRelease(curr.getNextPlain());
++                this.removeFromSize(1);
++
++                return curr.getValuePlain();
++            }
++        }
++
++        return null;
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    public V remove(final long key) {
++        return this.remove(key, SWMRLong2ObjectHashTable.getHash(key));
++    }
++
++    /**
++     * {@inheritDoc}
++     */
++    public void putAll(final SWMRLong2ObjectHashTable<? extends V> map) {
++        Validate.notNull(map, "Null map");
++
++        final int size = map.size();
++        this.checkResize(Math.max(this.getSizePlain() + size/2, size)); /* preemptively resize */
++        map.forEach(this::put);
++    }
++
++    /**
++     * {@inheritDoc}
++     * <p>
++     * This call is non-atomic and the order that which entries are removed is undefined. The clear operation itself
++     * is release ordered, that is, after the clear operation is performed a release fence is performed.
++     * </p>
++     */
++    public void clear() {
++        Arrays.fill(this.getTablePlain(), null);
++        this.setSizeRelease(0);
++    }
++
++    public static final class TableEntry<V> {
++
++        protected final long key;
++        protected V value;
++
++        protected TableEntry<V> next;
++
++        protected static final VarHandle VALUE_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "value", Object.class);
++        protected static final VarHandle NEXT_HANDLE = ConcurrentUtil.getVarHandle(TableEntry.class, "next", TableEntry.class);
++
++        /* value */
++
++        protected final V getValuePlain() {
++            //noinspection unchecked
++            return (V)VALUE_HANDLE.get(this);
++        }
++
++        protected final V getValueAcquire() {
++            //noinspection unchecked
++            return (V)VALUE_HANDLE.getAcquire(this);
++        }
++
++        protected final void setValueRelease(final V to) {
++            VALUE_HANDLE.setRelease(this, to);
++        }
++
++        /* next */
++
++        protected final TableEntry<V> getNextPlain() {
++            //noinspection unchecked
++            return (TableEntry<V>)NEXT_HANDLE.get(this);
++        }
++
++        protected final TableEntry<V> getNextOpaque() {
++            //noinspection unchecked
++            return (TableEntry<V>)NEXT_HANDLE.getOpaque(this);
++        }
++
++        protected final void setNextPlain(final TableEntry<V> next) {
++            NEXT_HANDLE.set(this, next);
++        }
++
++        protected final void setNextRelease(final TableEntry<V> next) {
++            NEXT_HANDLE.setRelease(this, next);
++        }
++
++        protected TableEntry(final long key, final V value) {
++            this.key = key;
++            this.value = value;
++        }
++
++        public long getKey() {
++            return this.key;
++        }
++
++        public V getValue() {
++            return this.getValueAcquire();
++        }
++
++        /**
++         * {@inheritDoc}
++         */
++        public V setValue(final V value) {
++            if (value == null) {
++                throw new NullPointerException();
++            }
++
++            final V curr = this.getValuePlain();
++
++            this.setValueRelease(value);
++            return curr;
++        }
++
++        protected static int hash(final long key, final Object value) {
++            return SWMRLong2ObjectHashTable.getHash(key) ^ (value == null ? 0 : value.hashCode());
++        }
++
++        /**
++         * {@inheritDoc}
++         */
++        @Override
++        public int hashCode() {
++            return hash(this.key, this.getValueAcquire());
++        }
++
++        /**
++         * {@inheritDoc}
++         */
++        @Override
++        public boolean equals(final Object obj) {
++            if (this == obj) {
++                return true;
++            }
++
++            if (!(obj instanceof TableEntry<?>)) {
++                return false;
++            }
++            final TableEntry<?> other = (TableEntry<?>)obj;
++            final long otherKey = other.getKey();
++            final long thisKey = this.getKey();
++            final Object otherValue = other.getValueAcquire();
++            final V thisVal = this.getValueAcquire();
++            return (thisKey == otherKey) && (thisVal == otherValue || thisVal.equals(otherValue));
++        }
++    }
++}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/ArrayUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/ArrayUtil.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/util/ArrayUtil.java
+@@ -0,0 +0,0 @@
++package ca.spottedleaf.concurrentutil.util;
++
++import java.lang.invoke.VarHandle;
++
++public final class ArrayUtil {
++
++    public static final VarHandle BOOLEAN_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(boolean[].class);
++
++    public static final VarHandle BYTE_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(byte[].class);
++
++    public static final VarHandle SHORT_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(short[].class);
++
++    public static final VarHandle INT_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(int[].class);
++
++    public static final VarHandle LONG_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(long[].class);
++
++    public static final VarHandle OBJECT_ARRAY_HANDLE = ConcurrentUtil.getArrayHandle(Object[].class);
++
++    private ArrayUtil() {
++        throw new RuntimeException();
++    }
++
++    /* byte array */
++
++    public static byte getPlain(final byte[] array, final int index) {
++        return (byte)BYTE_ARRAY_HANDLE.get(array, index);
++    }
++
++    public static byte getOpaque(final byte[] array, final int index) {
++        return (byte)BYTE_ARRAY_HANDLE.getOpaque(array, index);
++    }
++
++    public static byte getAcquire(final byte[] array, final int index) {
++        return (byte)BYTE_ARRAY_HANDLE.getAcquire(array, index);
++    }
++
++    public static byte getVolatile(final byte[] array, final int index) {
++        return (byte)BYTE_ARRAY_HANDLE.getVolatile(array, index);
++    }
++
++    public static void setPlain(final byte[] array, final int index, final byte value) {
++        BYTE_ARRAY_HANDLE.set(array, index, value);
++    }
++
++    public static void setOpaque(final byte[] array, final int index, final byte value) {
++        BYTE_ARRAY_HANDLE.setOpaque(array, index, value);
++    }
++
++    public static void setRelease(final byte[] array, final int index, final byte value) {
++        BYTE_ARRAY_HANDLE.setRelease(array, index, value);
++    }
++
++    public static void setVolatile(final byte[] array, final int index, final byte value) {
++        BYTE_ARRAY_HANDLE.setVolatile(array, index, value);
++    }
++
++    public static void setVolatileContended(final byte[] array, final int index, final byte param) {
++        int failures = 0;
++
++        for (byte curr = getVolatile(array, index);;++failures) {
++            for (int i = 0; i < failures; ++i) {
++                ConcurrentUtil.backoff();
++            }
++
++            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
++                return;
++            }
++        }
++    }
++
++    public static byte compareAndExchangeVolatile(final byte[] array, final int index, final byte expect, final byte update) {
++        return (byte)BYTE_ARRAY_HANDLE.compareAndExchange(array, index, expect, update);
++    }
++
++    public static byte getAndAddVolatile(final byte[] array, final int index, final byte param) {
++        return (byte)BYTE_ARRAY_HANDLE.getAndAdd(array, index, param);
++    }
++
++    public static byte getAndAndVolatile(final byte[] array, final int index, final byte param) {
++        return (byte)BYTE_ARRAY_HANDLE.getAndBitwiseAnd(array, index, param);
++    }
++
++    public static byte getAndOrVolatile(final byte[] array, final int index, final byte param) {
++        return (byte)BYTE_ARRAY_HANDLE.getAndBitwiseOr(array, index, param);
++    }
++
++    public static byte getAndXorVolatile(final byte[] array, final int index, final byte param) {
++        return (byte)BYTE_ARRAY_HANDLE.getAndBitwiseXor(array, index, param);
++    }
++
++    public static byte getAndSetVolatile(final byte[] array, final int index, final byte param) {
++        return (byte)BYTE_ARRAY_HANDLE.getAndSet(array, index, param);
++    }
++
++    public static byte compareAndExchangeVolatileContended(final byte[] array, final int index, final byte expect, final byte update) {
++        return (byte)BYTE_ARRAY_HANDLE.compareAndExchange(array, index, expect, update);
++    }
++
++    public static byte getAndAddVolatileContended(final byte[] array, final int index, final byte param) {
++        int failures = 0;
++
++        for (byte curr = getVolatile(array, index);;++failures) {
++            for (int i = 0; i < failures; ++i) {
++                ConcurrentUtil.backoff();
++            }
++
++            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (byte) (curr + param)))) {
++                return curr;
++            }
++        }
++    }
++
++    public static byte getAndAndVolatileContended(final byte[] array, final int index, final byte param) {
++        int failures = 0;
++
++        for (byte curr = getVolatile(array, index);;++failures) {
++            for (int i = 0; i < failures; ++i) {
++                ConcurrentUtil.backoff();
++            }
++
++            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (byte) (curr & param)))) {
++                return curr;
++            }
++        }
++    }
++
++    public static byte getAndOrVolatileContended(final byte[] array, final int index, final byte param) {
++        int failures = 0;
++
++        for (byte curr = getVolatile(array, index);;++failures) {
++            for (int i = 0; i < failures; ++i) {
++                ConcurrentUtil.backoff();
++            }
++
++            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (byte) (curr | param)))) {
++                return curr;
++            }
++        }
++    }
++
++    public static byte getAndXorVolatileContended(final byte[] array, final int index, final byte param) {
++        int failures = 0;
++
++        for (byte curr = getVolatile(array, index);;++failures) {
++            for (int i = 0; i < failures; ++i) {
++                ConcurrentUtil.backoff();
++            }
++
++            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (byte) (curr ^ param)))) {
++                return curr;
++            }
++        }
++    }
++
++    public static byte getAndSetVolatileContended(final byte[] array, final int index, final byte param) {
++        int failures = 0;
++
++        for (byte curr = getVolatile(array, index);;++failures) {
++            for (int i = 0; i < failures; ++i) {
++                ConcurrentUtil.backoff();
++            }
++
++            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
++                return curr;
++            }
++        }
++    }
++
++    /* short array */
++
++    public static short getPlain(final short[] array, final int index) {
++        return (short)SHORT_ARRAY_HANDLE.get(array, index);
++    }
++
++    public static short getOpaque(final short[] array, final int index) {
++        return (short)SHORT_ARRAY_HANDLE.getOpaque(array, index);
++    }
++
++    public static short getAcquire(final short[] array, final int index) {
++        return (short)SHORT_ARRAY_HANDLE.getAcquire(array, index);
++    }
++
++    public static short getVolatile(final short[] array, final int index) {
++        return (short)SHORT_ARRAY_HANDLE.getVolatile(array, index);
++    }
++
++    public static void setPlain(final short[] array, final int index, final short value) {
++        SHORT_ARRAY_HANDLE.set(array, index, value);
++    }
++
++    public static void setOpaque(final short[] array, final int index, final short value) {
++        SHORT_ARRAY_HANDLE.setOpaque(array, index, value);
++    }
++
++    public static void setRelease(final short[] array, final int index, final short value) {
++        SHORT_ARRAY_HANDLE.setRelease(array, index, value);
++    }
++
++    public static void setVolatile(final short[] array, final int index, final short value) {
++        SHORT_ARRAY_HANDLE.setVolatile(array, index, value);
++    }
++
++    public static void setVolatileContended(final short[] array, final int index, final short param) {
++        int failures = 0;
++
++        for (short curr = getVolatile(array, index);;++failures) {
++            for (int i = 0; i < failures; ++i) {
++                ConcurrentUtil.backoff();
++            }
++
++            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
++                return;
++            }
++        }
++    }
++
++    public static short compareAndExchangeVolatile(final short[] array, final int index, final short expect, final short update) {
++        return (short)SHORT_ARRAY_HANDLE.compareAndExchange(array, index, expect, update);
++    }
++
++    public static short getAndAddVolatile(final short[] array, final int index, final short param) {
++        return (short)SHORT_ARRAY_HANDLE.getAndAdd(array, index, param);
++    }
++
++    public static short getAndAndVolatile(final short[] array, final int index, final short param) {
++        return (short)SHORT_ARRAY_HANDLE.getAndBitwiseAnd(array, index, param);
++    }
++
++    public static short getAndOrVolatile(final short[] array, final int index, final short param) {
++        return (short)SHORT_ARRAY_HANDLE.getAndBitwiseOr(array, index, param);
++    }
++
++    public static short getAndXorVolatile(final short[] array, final int index, final short param) {
++        return (short)SHORT_ARRAY_HANDLE.getAndBitwiseXor(array, index, param);
++    }
++
++    public static short getAndSetVolatile(final short[] array, final int index, final short param) {
++        return (short)SHORT_ARRAY_HANDLE.getAndSet(array, index, param);
++    }
++
++    public static short compareAndExchangeVolatileContended(final short[] array, final int index, final short expect, final short update) {
++        return (short)SHORT_ARRAY_HANDLE.compareAndExchange(array, index, expect, update);
++    }
++
++    public static short getAndAddVolatileContended(final short[] array, final int index, final short param) {
++        int failures = 0;
++
++        for (short curr = getVolatile(array, index);;++failures) {
++            for (int i = 0; i < failures; ++i) {
++                ConcurrentUtil.backoff();
++            }
++
++            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (short) (curr + param)))) {
++                return curr;
++            }
++        }
++    }
++
++    public static short getAndAndVolatileContended(final short[] array, final int index, final short param) {
++        int failures = 0;
++
++        for (short curr = getVolatile(array, index);;++failures) {
++            for (int i = 0; i < failures; ++i) {
++                ConcurrentUtil.backoff();
++            }
++
++            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (short) (curr & param)))) {
++                return curr;
++            }
++        }
++    }
++
++    public static short getAndOrVolatileContended(final short[] array, final int index, final short param) {
++        int failures = 0;
++
++        for (short curr = getVolatile(array, index);;++failures) {
++            for (int i = 0; i < failures; ++i) {
++                ConcurrentUtil.backoff();
++            }
++
++            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (short) (curr | param)))) {
++                return curr;
++            }
++        }
++    }
++
++    public static short getAndXorVolatileContended(final short[] array, final int index, final short param) {
++        int failures = 0;
++
++        for (short curr = getVolatile(array, index);;++failures) {
++            for (int i = 0; i < failures; ++i) {
++                ConcurrentUtil.backoff();
++            }
++
++            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (short) (curr ^ param)))) {
++                return curr;
++            }
++        }
++    }
++
++    public static short getAndSetVolatileContended(final short[] array, final int index, final short param) {
++        int failures = 0;
++
++        for (short curr = getVolatile(array, index);;++failures) {
++            for (int i = 0; i < failures; ++i) {
++                ConcurrentUtil.backoff();
++            }
++
++            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
++                return curr;
++            }
++        }
++    }
++
++    /* int array */
++
++    public static int getPlain(final int[] array, final int index) {
++        return (int)INT_ARRAY_HANDLE.get(array, index);
++    }
++
++    public static int getOpaque(final int[] array, final int index) {
++        return (int)INT_ARRAY_HANDLE.getOpaque(array, index);
++    }
++
++    public static int getAcquire(final int[] array, final int index) {
++        return (int)INT_ARRAY_HANDLE.getAcquire(array, index);
++    }
++
++    public static int getVolatile(final int[] array, final int index) {
++        return (int)INT_ARRAY_HANDLE.getVolatile(array, index);
++    }
++
++    public static void setPlain(final int[] array, final int index, final int value) {
++        INT_ARRAY_HANDLE.set(array, index, value);
++    }
++
++    public static void setOpaque(final int[] array, final int index, final int value) {
++        INT_ARRAY_HANDLE.setOpaque(array, index, value);
++    }
++
++    public static void setRelease(final int[] array, final int index, final int value) {
++        INT_ARRAY_HANDLE.setRelease(array, index, value);
++    }
++
++    public static void setVolatile(final int[] array, final int index, final int value) {
++        INT_ARRAY_HANDLE.setVolatile(array, index, value);
++    }
++
++    public static void setVolatileContended(final int[] array, final int index, final int param) {
++        int failures = 0;
++
++        for (int curr = getVolatile(array, index);;++failures) {
++            for (int i = 0; i < failures; ++i) {
++                ConcurrentUtil.backoff();
++            }
++
++            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
++                return;
++            }
++        }
++    }
++
++    public static int compareAndExchangeVolatile(final int[] array, final int index, final int expect, final int update) {
++        return (int)INT_ARRAY_HANDLE.compareAndExchange(array, index, expect, update);
++    }
++
++    public static int getAndAddVolatile(final int[] array, final int index, final int param) {
++        return (int)INT_ARRAY_HANDLE.getAndAdd(array, index, param);
++    }
++
++    public static int getAndAndVolatile(final int[] array, final int index, final int param) {
++        return (int)INT_ARRAY_HANDLE.getAndBitwiseAnd(array, index, param);
++    }
++
++    public static int getAndOrVolatile(final int[] array, final int index, final int param) {
++        return (int)INT_ARRAY_HANDLE.getAndBitwiseOr(array, index, param);
++    }
++
++    public static int getAndXorVolatile(final int[] array, final int index, final int param) {
++        return (int)INT_ARRAY_HANDLE.getAndBitwiseXor(array, index, param);
++    }
++
++    public static int getAndSetVolatile(final int[] array, final int index, final int param) {
++        return (int)INT_ARRAY_HANDLE.getAndSet(array, index, param);
++    }
++
++    public static int compareAndExchangeVolatileContended(final int[] array, final int index, final int expect, final int update) {
++        return (int)INT_ARRAY_HANDLE.compareAndExchange(array, index, expect, update);
++    }
++
++    public static int getAndAddVolatileContended(final int[] array, final int index, final int param) {
++        int failures = 0;
++
++        for (int curr = getVolatile(array, index);;++failures) {
++            for (int i = 0; i < failures; ++i) {
++                ConcurrentUtil.backoff();
++            }
++
++            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (int) (curr + param)))) {
++                return curr;
++            }
++        }
++    }
++
++    public static int getAndAndVolatileContended(final int[] array, final int index, final int param) {
++        int failures = 0;
++
++        for (int curr = getVolatile(array, index);;++failures) {
++            for (int i = 0; i < failures; ++i) {
++                ConcurrentUtil.backoff();
++            }
++
++            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (int) (curr & param)))) {
++                return curr;
++            }
++        }
++    }
++
++    public static int getAndOrVolatileContended(final int[] array, final int index, final int param) {
++        int failures = 0;
++
++        for (int curr = getVolatile(array, index);;++failures) {
++            for (int i = 0; i < failures; ++i) {
++                ConcurrentUtil.backoff();
++            }
++
++            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (int) (curr | param)))) {
++                return curr;
++            }
++        }
++    }
++
++    public static int getAndXorVolatileContended(final int[] array, final int index, final int param) {
++        int failures = 0;
++
++        for (int curr = getVolatile(array, index);;++failures) {
++            for (int i = 0; i < failures; ++i) {
++                ConcurrentUtil.backoff();
++            }
++
++            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (int) (curr ^ param)))) {
++                return curr;
++            }
++        }
++    }
++
++    public static int getAndSetVolatileContended(final int[] array, final int index, final int param) {
++        int failures = 0;
++
++        for (int curr = getVolatile(array, index);;++failures) {
++            for (int i = 0; i < failures; ++i) {
++                ConcurrentUtil.backoff();
++            }
++
++            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
++                return curr;
++            }
++        }
++    }
++
++    /* long array */
++
++    public static long getPlain(final long[] array, final int index) {
++        return (long)LONG_ARRAY_HANDLE.get(array, index);
++    }
++
++    public static long getOpaque(final long[] array, final int index) {
++        return (long)LONG_ARRAY_HANDLE.getOpaque(array, index);
++    }
++
++    public static long getAcquire(final long[] array, final int index) {
++        return (long)LONG_ARRAY_HANDLE.getAcquire(array, index);
++    }
++
++    public static long getVolatile(final long[] array, final int index) {
++        return (long)LONG_ARRAY_HANDLE.getVolatile(array, index);
++    }
++
++    public static void setPlain(final long[] array, final int index, final long value) {
++        LONG_ARRAY_HANDLE.set(array, index, value);
++    }
++
++    public static void setOpaque(final long[] array, final int index, final long value) {
++        LONG_ARRAY_HANDLE.setOpaque(array, index, value);
++    }
++
++    public static void setRelease(final long[] array, final int index, final long value) {
++        LONG_ARRAY_HANDLE.setRelease(array, index, value);
++    }
++
++    public static void setVolatile(final long[] array, final int index, final long value) {
++        LONG_ARRAY_HANDLE.setVolatile(array, index, value);
++    }
++
++    public static void setVolatileContended(final long[] array, final int index, final long param) {
++        int failures = 0;
++
++        for (long curr = getVolatile(array, index);;++failures) {
++            for (int i = 0; i < failures; ++i) {
++                ConcurrentUtil.backoff();
++            }
++
++            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
++                return;
++            }
++        }
++    }
++
++    public static long compareAndExchangeVolatile(final long[] array, final int index, final long expect, final long update) {
++        return (long)LONG_ARRAY_HANDLE.compareAndExchange(array, index, expect, update);
++    }
++
++    public static long getAndAddVolatile(final long[] array, final int index, final long param) {
++        return (long)LONG_ARRAY_HANDLE.getAndAdd(array, index, param);
++    }
++
++    public static long getAndAndVolatile(final long[] array, final int index, final long param) {
++        return (long)LONG_ARRAY_HANDLE.getAndBitwiseAnd(array, index, param);
++    }
++
++    public static long getAndOrVolatile(final long[] array, final int index, final long param) {
++        return (long)LONG_ARRAY_HANDLE.getAndBitwiseOr(array, index, param);
++    }
++
++    public static long getAndXorVolatile(final long[] array, final int index, final long param) {
++        return (long)LONG_ARRAY_HANDLE.getAndBitwiseXor(array, index, param);
++    }
++
++    public static long getAndSetVolatile(final long[] array, final int index, final long param) {
++        return (long)LONG_ARRAY_HANDLE.getAndSet(array, index, param);
++    }
++
++    public static long compareAndExchangeVolatileContended(final long[] array, final int index, final long expect, final long update) {
++        return (long)LONG_ARRAY_HANDLE.compareAndExchange(array, index, expect, update);
++    }
++
++    public static long getAndAddVolatileContended(final long[] array, final int index, final long param) {
++        int failures = 0;
++
++        for (long curr = getVolatile(array, index);;++failures) {
++            for (int i = 0; i < failures; ++i) {
++                ConcurrentUtil.backoff();
++            }
++
++            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (long) (curr + param)))) {
++                return curr;
++            }
++        }
++    }
++
++    public static long getAndAndVolatileContended(final long[] array, final int index, final long param) {
++        int failures = 0;
++
++        for (long curr = getVolatile(array, index);;++failures) {
++            for (int i = 0; i < failures; ++i) {
++                ConcurrentUtil.backoff();
++            }
++
++            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (long) (curr & param)))) {
++                return curr;
++            }
++        }
++    }
++
++    public static long getAndOrVolatileContended(final long[] array, final int index, final long param) {
++        int failures = 0;
++
++        for (long curr = getVolatile(array, index);;++failures) {
++            for (int i = 0; i < failures; ++i) {
++                ConcurrentUtil.backoff();
++            }
++
++            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (long) (curr | param)))) {
++                return curr;
++            }
++        }
++    }
++
++    public static long getAndXorVolatileContended(final long[] array, final int index, final long param) {
++        int failures = 0;
++
++        for (long curr = getVolatile(array, index);;++failures) {
++            for (int i = 0; i < failures; ++i) {
++                ConcurrentUtil.backoff();
++            }
++
++            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (long) (curr ^ param)))) {
++                return curr;
++            }
++        }
++    }
++
++    public static long getAndSetVolatileContended(final long[] array, final int index, final long param) {
++        int failures = 0;
++
++        for (long curr = getVolatile(array, index);;++failures) {
++            for (int i = 0; i < failures; ++i) {
++                ConcurrentUtil.backoff();
++            }
++
++            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
++                return curr;
++            }
++        }
++    }
++
++    /* boolean array */
++
++    public static boolean getPlain(final boolean[] array, final int index) {
++        return (boolean)BOOLEAN_ARRAY_HANDLE.get(array, index);
++    }
++
++    public static boolean getOpaque(final boolean[] array, final int index) {
++        return (boolean)BOOLEAN_ARRAY_HANDLE.getOpaque(array, index);
++    }
++
++    public static boolean getAcquire(final boolean[] array, final int index) {
++        return (boolean)BOOLEAN_ARRAY_HANDLE.getAcquire(array, index);
++    }
++
++    public static boolean getVolatile(final boolean[] array, final int index) {
++        return (boolean)BOOLEAN_ARRAY_HANDLE.getVolatile(array, index);
++    }
++
++    public static void setPlain(final boolean[] array, final int index, final boolean value) {
++        BOOLEAN_ARRAY_HANDLE.set(array, index, value);
++    }
++
++    public static void setOpaque(final boolean[] array, final int index, final boolean value) {
++        BOOLEAN_ARRAY_HANDLE.setOpaque(array, index, value);
++    }
++
++    public static void setRelease(final boolean[] array, final int index, final boolean value) {
++        BOOLEAN_ARRAY_HANDLE.setRelease(array, index, value);
++    }
++
++    public static void setVolatile(final boolean[] array, final int index, final boolean value) {
++        BOOLEAN_ARRAY_HANDLE.setVolatile(array, index, value);
++    }
++
++    public static void setVolatileContended(final boolean[] array, final int index, final boolean param) {
++        int failures = 0;
++
++        for (boolean curr = getVolatile(array, index);;++failures) {
++            for (int i = 0; i < failures; ++i) {
++                ConcurrentUtil.backoff();
++            }
++
++            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
++                return;
++            }
++        }
++    }
++
++    public static boolean compareAndExchangeVolatile(final boolean[] array, final int index, final boolean expect, final boolean update) {
++        return (boolean)BOOLEAN_ARRAY_HANDLE.compareAndExchange(array, index, expect, update);
++    }
++
++    public static boolean getAndOrVolatile(final boolean[] array, final int index, final boolean param) {
++        return (boolean)BOOLEAN_ARRAY_HANDLE.getAndBitwiseOr(array, index, param);
++    }
++
++    public static boolean getAndXorVolatile(final boolean[] array, final int index, final boolean param) {
++        return (boolean)BOOLEAN_ARRAY_HANDLE.getAndBitwiseXor(array, index, param);
++    }
++
++    public static boolean getAndSetVolatile(final boolean[] array, final int index, final boolean param) {
++        return (boolean)BOOLEAN_ARRAY_HANDLE.getAndSet(array, index, param);
++    }
++
++    public static boolean compareAndExchangeVolatileContended(final boolean[] array, final int index, final boolean expect, final boolean update) {
++        return (boolean)BOOLEAN_ARRAY_HANDLE.compareAndExchange(array, index, expect, update);
++    }
++
++    public static boolean getAndAndVolatileContended(final boolean[] array, final int index, final boolean param) {
++        int failures = 0;
++
++        for (boolean curr = getVolatile(array, index);;++failures) {
++            for (int i = 0; i < failures; ++i) {
++                ConcurrentUtil.backoff();
++            }
++
++            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (boolean) (curr & param)))) {
++                return curr;
++            }
++        }
++    }
++
++    public static boolean getAndOrVolatileContended(final boolean[] array, final int index, final boolean param) {
++        int failures = 0;
++
++        for (boolean curr = getVolatile(array, index);;++failures) {
++            for (int i = 0; i < failures; ++i) {
++                ConcurrentUtil.backoff();
++            }
++
++            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (boolean) (curr | param)))) {
++                return curr;
++            }
++        }
++    }
++
++    public static boolean getAndXorVolatileContended(final boolean[] array, final int index, final boolean param) {
++        int failures = 0;
++
++        for (boolean curr = getVolatile(array, index);;++failures) {
++            for (int i = 0; i < failures; ++i) {
++                ConcurrentUtil.backoff();
++            }
++
++            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, (boolean) (curr ^ param)))) {
++                return curr;
++            }
++        }
++    }
++
++    public static boolean getAndSetVolatileContended(final boolean[] array, final int index, final boolean param) {
++        int failures = 0;
++
++        for (boolean curr = getVolatile(array, index);;++failures) {
++            for (int i = 0; i < failures; ++i) {
++                ConcurrentUtil.backoff();
++            }
++
++            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
++                return curr;
++            }
++        }
++    }
++
++    @SuppressWarnings("unchecked")
++    public static <T> T getPlain(final T[] array, final int index) {
++        final Object ret = OBJECT_ARRAY_HANDLE.get((Object[])array, index);
++        return (T)ret;
++    }
++
++    @SuppressWarnings("unchecked")
++    public static <T> T getOpaque(final T[] array, final int index) {
++        final Object ret = OBJECT_ARRAY_HANDLE.getOpaque((Object[])array, index);
++        return (T)ret;
++    }
++
++    @SuppressWarnings("unchecked")
++    public static <T> T getAcquire(final T[] array, final int index) {
++        final Object ret = OBJECT_ARRAY_HANDLE.getAcquire((Object[])array, index);
++        return (T)ret;
++    }
++
++    @SuppressWarnings("unchecked")
++    public static <T> T getVolatile(final T[] array, final int index) {
++        final Object ret = OBJECT_ARRAY_HANDLE.getVolatile((Object[])array, index);
++        return (T)ret;
++    }
++
++    public static <T> void setPlain(final T[] array, final int index, final T value) {
++        OBJECT_ARRAY_HANDLE.set((Object[])array, index, (Object)value);
++    }
++
++    public static <T> void setOpaque(final T[] array, final int index, final T value) {
++        OBJECT_ARRAY_HANDLE.setOpaque((Object[])array, index, (Object)value);
++    }
++
++    public static <T> void setRelease(final T[] array, final int index, final T value) {
++        OBJECT_ARRAY_HANDLE.setRelease((Object[])array, index, (Object)value);
++    }
++
++    public static <T> void setVolatile(final T[] array, final int index, final T value) {
++        OBJECT_ARRAY_HANDLE.setVolatile((Object[])array, index, (Object)value);
++    }
++
++    public static <T> void setVolatileContended(final T[] array, final int index, final T param) {
++        int failures = 0;
++
++        for (T curr = getVolatile(array, index);;++failures) {
++            for (int i = 0; i < failures; ++i) {
++                ConcurrentUtil.backoff();
++            }
++
++            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
++                return;
++            }
++        }
++    }
++
++    @SuppressWarnings("unchecked")
++    public static <T> T compareAndExchangeVolatile(final T[] array, final int index, final T expect, final T update) {
++        final Object ret = OBJECT_ARRAY_HANDLE.compareAndExchange((Object[])array, index, (Object)expect, (Object)update);
++        return (T)ret;
++    }
++
++    @SuppressWarnings("unchecked")
++    public static <T> T getAndSetVolatile(final T[] array, final int index, final T param) {
++        final Object ret = BYTE_ARRAY_HANDLE.getAndSet((Object[])array, index, (Object)param);
++        return (T)ret;
++    }
++
++    @SuppressWarnings("unchecked")
++    public static <T> T compareAndExchangeVolatileContended(final T[] array, final int index, final T expect, final T update) {
++        final Object ret = OBJECT_ARRAY_HANDLE.compareAndExchange((Object[])array, index, (Object)expect, (Object)update);
++        return (T)ret;
++    }
++
++    public static <T> T getAndSetVolatileContended(final T[] array, final int index, final T param) {
++        int failures = 0;
++
++        for (T curr = getVolatile(array, index);;++failures) {
++            for (int i = 0; i < failures; ++i) {
++                ConcurrentUtil.backoff();
++            }
++
++            if (curr == (curr = compareAndExchangeVolatileContended(array, index, curr, param))) {
++                return curr;
++            }
++        }
++    }
++}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/CollectionUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/CollectionUtil.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/util/CollectionUtil.java
+@@ -0,0 +0,0 @@
++package ca.spottedleaf.concurrentutil.util;
++
++import java.util.Collection;
++
++public final class CollectionUtil {
++
++    public static String toString(final Collection<?> collection, final String name) {
++        return CollectionUtil.toString(collection, name, new StringBuilder(name.length() + 128)).toString();
++    }
++
++    public static StringBuilder toString(final Collection<?> collection, final String name, final StringBuilder builder) {
++        builder.append(name).append("{elements={");
++
++        boolean first = true;
++
++        for (final Object element : collection) {
++            if (!first) {
++                builder.append(", ");
++            }
++            first = false;
++
++            builder.append('"').append(element).append('"');
++        }
++
++        return builder.append("}}");
++    }
++
++    private CollectionUtil() {
++        throw new RuntimeException();
++    }
++}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/ConcurrentUtil.java b/src/main/java/ca/spottedleaf/concurrentutil/util/ConcurrentUtil.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/util/ConcurrentUtil.java
+@@ -0,0 +0,0 @@
++package ca.spottedleaf.concurrentutil.util;
++
++import java.lang.invoke.MethodHandles;
++import java.lang.invoke.VarHandle;
++import java.util.concurrent.locks.LockSupport;
++
++public final class ConcurrentUtil {
++
++    public static String genericToString(final Object object) {
++        return object == null ? "null" : object.getClass().getName() + ":" + object.hashCode() + ":" + object.toString();
++    }
++
++    public static void rethrow(Throwable exception) {
++        rethrow0(exception);
++    }
++
++    private static <T extends Throwable> void rethrow0(Throwable thr) throws T {
++        throw (T)thr;
++    }
++
++    public static VarHandle getVarHandle(final Class<?> lookIn, final String fieldName, final Class<?> fieldType) {
++        try {
++            return MethodHandles.privateLookupIn(lookIn, MethodHandles.lookup()).findVarHandle(lookIn, fieldName, fieldType);
++        } catch (final Exception ex) {
++            throw new RuntimeException(ex); // unreachable
++        }
++    }
++
++    public static VarHandle getStaticVarHandle(final Class<?> lookIn, final String fieldName, final Class<?> fieldType) {
++        try {
++            return MethodHandles.privateLookupIn(lookIn, MethodHandles.lookup()).findStaticVarHandle(lookIn, fieldName, fieldType);
++        } catch (final Exception ex) {
++            throw new RuntimeException(ex); // unreachable
++        }
++    }
++
++    /**
++     * Non-exponential backoff algorithm to use in lightly contended areas.
++     * @see ConcurrentUtil#exponentiallyBackoffSimple(long)
++     * @see ConcurrentUtil#exponentiallyBackoffComplex(long)
++     */
++    public static void backoff() {
++        Thread.onSpinWait();
++    }
++
++    /**
++     * Backoff algorithm to use for a short held lock (i.e compareAndExchange operation). Generally this should not be
++     * used when a thread can block another thread. Instead, use {@link ConcurrentUtil#exponentiallyBackoffComplex(long)}.
++     * @param counter The current counter.
++     * @return The counter plus 1.
++     * @see ConcurrentUtil#backoff()
++     * @see ConcurrentUtil#exponentiallyBackoffComplex(long)
++     */
++    public static long exponentiallyBackoffSimple(final long counter) {
++        for (long i = 0; i < counter; ++i) {
++            backoff();
++        }
++        return counter + 1L;
++    }
++
++    /**
++     * Backoff algorithm to use for a lock that can block other threads (i.e if another thread contending with this thread
++     * can be thrown off the scheduler). This lock should not be used for simple locks such as compareAndExchange.
++     * @param counter The current counter.
++     * @return The next (if any) step in the backoff logic.
++     * @see ConcurrentUtil#backoff()
++     * @see ConcurrentUtil#exponentiallyBackoffSimple(long)
++     */
++    public static long exponentiallyBackoffComplex(final long counter) {
++        // TODO experimentally determine counters
++        if (counter < 100L) {
++            return exponentiallyBackoffSimple(counter);
++        }
++        if (counter < 1_200L) {
++            Thread.yield();
++            LockSupport.parkNanos(1_000L);
++            return counter + 1L;
++        }
++        // scale 0.1ms (100us) per failure
++        Thread.yield();
++        LockSupport.parkNanos(100_000L * counter);
++        return counter + 1;
++    }
++
++    /**
++     * Simple exponential backoff that will linearly increase the time per failure, according to the scale.
++     * @param counter The current failure counter.
++     * @param scale Time per failure, in ns.
++     * @param max The maximum time to wait for, in ns.
++     * @return The next counter.
++     */
++    public static long linearLongBackoff(long counter, final long scale, long max) {
++        counter = Math.min(Long.MAX_VALUE, counter + 1); // prevent overflow
++        max = Math.max(0, max);
++
++        if (scale <= 0L) {
++            return counter;
++        }
++
++        long time = scale * counter;
++
++        if (time > max || time / scale != counter) {
++            time = max;
++        }
++
++        boolean interrupted = Thread.interrupted();
++        if (time > 1_000_000L) { // 1ms
++            Thread.yield();
++        }
++        LockSupport.parkNanos(time);
++        if (interrupted) {
++            Thread.currentThread().interrupt();
++        }
++        return counter;
++    }
++
++    /**
++     * Simple exponential backoff that will linearly increase the time per failure, according to the scale.
++     * @param counter The current failure counter.
++     * @param scale Time per failure, in ns.
++     * @param max The maximum time to wait for, in ns.
++     * @param deadline The deadline in ns. Deadline time source: {@link System#nanoTime()}.
++     * @return The next counter.
++     */
++    public static long linearLongBackoffDeadline(long counter, final long scale, long max, long deadline) {
++        counter = Math.min(Long.MAX_VALUE, counter + 1); // prevent overflow
++        max = Math.max(0, max);
++
++        if (scale <= 0L) {
++            return counter;
++        }
++
++        long time = scale * counter;
++
++        // check overflow
++        if (time / scale != counter) {
++            // overflew
++            --counter;
++            time = max;
++        } else if (time > max) {
++            time = max;
++        }
++
++        final long currTime = System.nanoTime();
++        final long diff = deadline - currTime;
++        if (diff <= 0) {
++            return counter;
++        }
++        if (diff <= 1_500_000L) { // 1.5ms
++            time = 100_000L; // 100us
++        } else if (time > 1_000_000L) { // 1ms
++            Thread.yield();
++        }
++
++        boolean interrupted = Thread.interrupted();
++        LockSupport.parkNanos(time);
++        if (interrupted) {
++            Thread.currentThread().interrupt();
++        }
++        return counter;
++    }
++
++    public static VarHandle getArrayHandle(final Class<?> type) {
++        return MethodHandles.arrayElementVarHandle(type);
++    }
++}
+diff --git a/src/main/java/ca/spottedleaf/concurrentutil/util/Validate.java b/src/main/java/ca/spottedleaf/concurrentutil/util/Validate.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/ca/spottedleaf/concurrentutil/util/Validate.java
+@@ -0,0 +0,0 @@
++package ca.spottedleaf.concurrentutil.util;
++
++public final class Validate {
++
++    public static <T> T notNull(final T obj) {
++        if (obj == null) {
++            throw new NullPointerException();
++        }
++        return obj;
++    }
++
++    public static <T> T notNull(final T obj, final String msgIfNull) {
++        if (obj == null) {
++            throw new NullPointerException(msgIfNull);
++        }
++        return obj;
++    }
++
++    public static void arrayBounds(final int off, final int len, final int arrayLength, final String msgPrefix) {
++        if (off < 0 || len < 0 || (arrayLength - off) < len) {
++            throw new ArrayIndexOutOfBoundsException(msgPrefix + ": off: " + off + ", len: " + len + ", array length: " + arrayLength);
++        }
++    }
++
++    private Validate() {
++        throw new RuntimeException();
++    }
++}
diff --git a/patches/server/Do-not-allow-ticket-level-changes-while-unloading-pl.patch b/patches/server/Do-not-allow-ticket-level-changes-while-unloading-pl.patch
index ed009f0a5c..a27ee26750 100644
--- a/patches/server/Do-not-allow-ticket-level-changes-while-unloading-pl.patch
+++ b/patches/server/Do-not-allow-ticket-level-changes-while-unloading-pl.patch
@@ -18,7 +18,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    boolean unloadingPlayerChunk = false; // Paper - do not allow ticket level changes while unloading chunks
      public ChunkMap(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor executor, BlockableEventLoop<Runnable> mainThreadExecutor, LightChunkGetter chunkProvider, ChunkGenerator chunkGenerator, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier<DimensionDataStorage> persistentStateManagerFactory, int viewDistance, boolean dsync) {
          super(session.getDimensionPath(world.dimension()).resolve("region"), dataFixer, dsync);
-         this.visibleChunkMap = this.updatingChunkMap.clone();
+         // Paper - don't copy
 @@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
  
      @Nullable
@@ -41,9 +41,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
                  boolean removed;
                  if ((removed = this.pendingUnloads.remove(pos, holder)) && ichunkaccess != null) {
 @@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
-                     this.regionManagers.get(index).removeChunk(holder.pos.x, holder.pos.z);
-                 }
-             } // Paper end
+                 } else if (removed) { // Paper start
+                     net.minecraft.server.ChunkSystem.onChunkHolderDelete(this.level, holder);
+                 } // Paper end
 +                } finally { this.unloadingPlayerChunk = unloadingBefore; } // Paper - do not allow ticket level changes while unloading chunks
  
              }
diff --git a/patches/server/Do-not-copy-visible-chunks.patch b/patches/server/Do-not-copy-visible-chunks.patch
index 2399a4adf1..3167cd480b 100644
--- a/patches/server/Do-not-copy-visible-chunks.patch
+++ b/patches/server/Do-not-copy-visible-chunks.patch
@@ -8,32 +8,39 @@ tickDistanceManager call can take up quite a bit in
 the function. I saw approximately 1/3rd of the function
 on the copy.
 
-diff --git a/src/main/java/io/papermc/paper/command/subcommands/ChunkDebugCommand.java b/src/main/java/io/papermc/paper/command/subcommands/ChunkDebugCommand.java
+diff --git a/src/main/java/net/minecraft/server/ChunkSystem.java b/src/main/java/net/minecraft/server/ChunkSystem.java
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/io/papermc/paper/command/subcommands/ChunkDebugCommand.java
-+++ b/src/main/java/io/papermc/paper/command/subcommands/ChunkDebugCommand.java
-@@ -0,0 +0,0 @@ public final class ChunkDebugCommand implements PaperSubcommand {
-             int ticking = 0;
-             int entityTicking = 0;
+--- a/src/main/java/net/minecraft/server/ChunkSystem.java
++++ b/src/main/java/net/minecraft/server/ChunkSystem.java
+@@ -0,0 +0,0 @@ public final class ChunkSystem {
+     }
  
--            for (final ChunkHolder chunk : world.getChunkSource().chunkMap.updatingChunkMap.values()) {
-+            for (final ChunkHolder chunk : world.getChunkSource().chunkMap.updatingChunks.getUpdatingMap().values()) { // Paper - change updating chunks map
-                 if (chunk.getFullChunkNowUnchecked() == null) {
-                     continue;
-                 }
-diff --git a/src/main/java/net/minecraft/server/MCUtil.java b/src/main/java/net/minecraft/server/MCUtil.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/MCUtil.java
-+++ b/src/main/java/net/minecraft/server/MCUtil.java
-@@ -0,0 +0,0 @@ public final class MCUtil {
+     public static List<ChunkHolder> getVisibleChunkHolders(final ServerLevel level) {
+-        return new ArrayList<>(level.chunkSource.chunkMap.visibleChunkMap.values());
++        if (Bukkit.isPrimaryThread()) {
++            return level.chunkSource.chunkMap.updatingChunks.getVisibleValuesCopy();
++        }
++        synchronized (level.chunkSource.chunkMap.updatingChunks) {
++            return level.chunkSource.chunkMap.updatingChunks.getVisibleValuesCopy();
++        }
+     }
  
-             ServerLevel world = ((org.bukkit.craftbukkit.CraftWorld)bukkitWorld).getHandle();
-             ChunkMap chunkMap = world.getChunkSource().chunkMap;
--            Long2ObjectLinkedOpenHashMap<ChunkHolder> visibleChunks = chunkMap.visibleChunkMap;
-+            Long2ObjectLinkedOpenHashMap<ChunkHolder> visibleChunks = chunkMap.updatingChunks.getVisibleMap(); // Paper
-             DistanceManager chunkMapDistance = chunkMap.distanceManager;
-             List<ChunkHolder> allChunks = new ArrayList<>(visibleChunks.values());
-             List<ServerPlayer> players = world.players;
+     public static List<ChunkHolder> getUpdatingChunkHolders(final ServerLevel level) {
+-        return new ArrayList<>(level.chunkSource.chunkMap.updatingChunkMap.values());
++        return level.chunkSource.chunkMap.updatingChunks.getUpdatingValuesCopy();
+     }
+ 
+     public static int getVisibleChunkHolderCount(final ServerLevel level) {
+-        return level.chunkSource.chunkMap.visibleChunkMap.size();
++        return level.chunkSource.chunkMap.updatingChunks.getVisibleMap().size();
+     }
+ 
+     public static int getUpdatingChunkHolderCount(final ServerLevel level) {
+-        return level.chunkSource.chunkMap.updatingChunkMap.size();
++        return level.chunkSource.chunkMap.updatingChunks.getUpdatingMap().size();
+     }
+ 
+     public static boolean hasAnyChunkHolders(final ServerLevel level) {
 diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
 --- a/src/main/java/net/minecraft/server/level/ChunkMap.java
@@ -53,7 +60,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      public final LongSet entitiesInLevel;
      public final ServerLevel level;
 @@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
-     boolean unloadingPlayerChunk = false; // Paper - do not allow ticket level changes while unloading chunks
+ 
      public ChunkMap(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor executor, BlockableEventLoop<Runnable> mainThreadExecutor, LightChunkGetter chunkProvider, ChunkGenerator chunkGenerator, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier<DimensionDataStorage> persistentStateManagerFactory, int viewDistance, boolean dsync) {
          super(session.getDimensionPath(world.dimension()).resolve("region"), dataFixer, dsync);
 -        this.visibleChunkMap = this.updatingChunkMap.clone();
@@ -82,21 +89,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
      protected IntSupplier getChunkQueueLevel(long pos) {
 @@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
-         };
- 
-         stringbuilder.append("Updating:").append(System.lineSeparator());
--        this.updatingChunkMap.values().forEach(consumer);
-+        this.updatingChunks.getUpdatingValuesCopy().forEach(consumer); // Paper
-         stringbuilder.append("Visible:").append(System.lineSeparator());
--        this.visibleChunkMap.values().forEach(consumer);
-+        this.updatingChunks.getVisibleValuesCopy().forEach(consumer); // Paper
-         CrashReport crashreport = CrashReport.forThrowable(exception, "Chunk loading");
-         CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Chunk loading");
- 
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
-                     // Paper end
-                 }
- 
+                 // Paper start
+                 holder.onChunkAdd();
+                 // Paper end
 -                this.updatingChunkMap.put(pos, holder);
 +                this.updatingChunks.queueUpdate(pos, holder); // Paper - Don't copy
                  this.modified = true;
@@ -104,32 +99,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
 @@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
  
-     protected void saveAllChunks(boolean flush) {
-         if (flush) {
--            List<ChunkHolder> list = (List) this.visibleChunkMap.values().stream().filter(ChunkHolder::wasAccessibleSinceLastSave).peek(ChunkHolder::refreshAccessibility).collect(Collectors.toList());
-+            List<ChunkHolder> list = (List) this.updatingChunks.getVisibleValuesCopy().stream().filter(ChunkHolder::wasAccessibleSinceLastSave).peek(ChunkHolder::refreshAccessibility).collect(Collectors.toList()); // Paper
-             MutableBoolean mutableboolean = new MutableBoolean();
- 
-             do {
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
-             //this.flushWorker(); // Paper - nuke IOWorker
-             this.level.asyncChunkTaskManager.flush(); // Paper - flush to preserve behavior compat with pre-async behaviour
-         } else {
--            this.visibleChunkMap.values().forEach(this::saveChunkIfNeeded);
-+            this.updatingChunks.getVisibleValuesCopy().forEach(this::saveChunkIfNeeded); // Paper
-         }
- 
-     }
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
-     }
- 
-     public boolean hasWork() {
--        return this.lightEngine.hasLightWork() || !this.pendingUnloads.isEmpty() || !this.updatingChunkMap.isEmpty() || this.poiManager.hasWork() || !this.toDrop.isEmpty() || !this.unloadQueue.isEmpty() || this.queueSorter.hasWork() || this.distanceManager.hasTickets();
-+        return this.lightEngine.hasLightWork() || !this.pendingUnloads.isEmpty() || !this.updatingChunks.getUpdatingValuesCopy().isEmpty() || this.poiManager.hasWork() || !this.toDrop.isEmpty() || !this.unloadQueue.isEmpty() || this.queueSorter.hasWork() || this.distanceManager.hasTickets(); // Paper
-     }
- 
-     private void processUnloads(BooleanSupplier shouldKeepTicking) {
-         LongIterator longiterator = this.toDrop.iterator();
          for (int i = 0; longiterator.hasNext() && (shouldKeepTicking.getAsBoolean() || i < 200 || this.toDrop.size() > 2000); longiterator.remove()) {
              long j = longiterator.nextLong();
 -            ChunkHolder playerchunk = (ChunkHolder) this.updatingChunkMap.remove(j);
@@ -151,88 +120,3 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
              this.modified = false;
              return true;
          }
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
- 
-             this.viewDistance = j;
-             this.distanceManager.updatePlayerTickets(this.viewDistance + 1);
--            ObjectIterator objectiterator = this.updatingChunkMap.values().iterator();
-+            Iterator objectiterator = this.updatingChunks.getVisibleValuesCopy().iterator(); // Paper
- 
-             while (objectiterator.hasNext()) {
-                 ChunkHolder playerchunk = (ChunkHolder) objectiterator.next();
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
-     }
- 
-     public int size() {
--        return this.visibleChunkMap.size();
-+        return this.updatingChunks.getVisibleMap().size(); // Paper - Don't copy
-     }
- 
-     public DistanceManager getDistanceManager() {
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
-     }
- 
-     protected Iterable<ChunkHolder> getChunks() {
--        return Iterables.unmodifiableIterable(this.visibleChunkMap.values());
-+        return Iterables.unmodifiableIterable(this.updatingChunks.getVisibleValuesCopy()); // Paper
-     }
- 
-     void dumpChunks(Writer writer) throws IOException {
-         CsvOutput csvwriter = CsvOutput.builder().addColumn("x").addColumn("z").addColumn("level").addColumn("in_memory").addColumn("status").addColumn("full_status").addColumn("accessible_ready").addColumn("ticking_ready").addColumn("entity_ticking_ready").addColumn("ticket").addColumn("spawning").addColumn("block_entity_count").addColumn("ticking_ticket").addColumn("ticking_level").addColumn("block_ticks").addColumn("fluid_ticks").build(writer);
-         TickingTracker tickingtracker = this.distanceManager.tickingTracker();
--        ObjectBidirectionalIterator objectbidirectionaliterator = this.visibleChunkMap.long2ObjectEntrySet().iterator();
-+        ObjectBidirectionalIterator objectbidirectionaliterator = this.updatingChunks.getVisibleMap().clone().long2ObjectEntrySet().fastIterator(); // Paper
- 
-         while (objectbidirectionaliterator.hasNext()) {
-             Entry<ChunkHolder> entry = (Entry) objectbidirectionaliterator.next();
-diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
-+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
-@@ -0,0 +0,0 @@ public class CraftWorld extends CraftRegionAccessor implements World {
-     @Override
-     public int getTileEntityCount() {
-         // We don't use the full world tile entity list, so we must iterate chunks
--        Long2ObjectLinkedOpenHashMap<ChunkHolder> chunks = world.getChunkSource().chunkMap.visibleChunkMap;
-+        Long2ObjectLinkedOpenHashMap<ChunkHolder> chunks = world.getChunkSource().chunkMap.updatingChunks.getVisibleMap(); // Paper - change updating chunks map
-         int size = 0;
-         for (ChunkHolder playerchunk : chunks.values()) {
-             net.minecraft.world.level.chunk.LevelChunk chunk = playerchunk.getTickingChunk();
-@@ -0,0 +0,0 @@ public class CraftWorld extends CraftRegionAccessor implements World {
-     public int getChunkCount() {
-         int ret = 0;
- 
--        for (ChunkHolder chunkHolder : world.getChunkSource().chunkMap.visibleChunkMap.values()) {
-+        for (ChunkHolder chunkHolder : world.getChunkSource().chunkMap.updatingChunks.getVisibleMap().values()) { // Paper - change updating chunks map
-             if (chunkHolder.getTickingChunk() != null) {
-                 ++ret;
-             }
-@@ -0,0 +0,0 @@ public class CraftWorld extends CraftRegionAccessor implements World {
- 
-     @Override
-     public Chunk[] getLoadedChunks() {
--        Long2ObjectLinkedOpenHashMap<ChunkHolder> chunks = this.world.getChunkSource().chunkMap.visibleChunkMap;
-+        // Paper start
-+        if (Thread.currentThread() != world.getLevel().thread) {
-+            // Paper start - change updating chunks map
-+            Long2ObjectLinkedOpenHashMap<ChunkHolder> chunks;
-+            synchronized (world.getChunkSource().chunkMap.updatingChunks) {
-+                chunks = world.getChunkSource().chunkMap.updatingChunks.getVisibleMap().clone();
-+            }
-+            return chunks.values().stream().map(ChunkHolder::getFullChunkNow).filter(Objects::nonNull).map(net.minecraft.world.level.chunk.LevelChunk::getBukkitChunk).toArray(Chunk[]::new);
-+            // Paper end - change updating chunks map
-+        }
-+        // Paper end
-+        Long2ObjectLinkedOpenHashMap<ChunkHolder> chunks = this.world.getChunkSource().chunkMap.updatingChunks.getVisibleMap(); // Paper - change updating chunks map
-         return chunks.values().stream().map(ChunkHolder::getFullChunkNow).filter(Objects::nonNull).map(net.minecraft.world.level.chunk.LevelChunk::getBukkitChunk).toArray(Chunk[]::new);
-     }
- 
-@@ -0,0 +0,0 @@ public class CraftWorld extends CraftRegionAccessor implements World {
- 
-     @Override
-     public boolean refreshChunk(int x, int z) {
--        ChunkHolder playerChunk = this.world.getChunkSource().chunkMap.visibleChunkMap.get(ChunkPos.asLong(x, z));
-+        ChunkHolder playerChunk = this.world.getChunkSource().chunkMap.updatingChunks.getVisibleMap().get(ChunkPos.asLong(x, z));
-         if (playerChunk == null) return false;
- 
-         playerChunk.getTickingChunkFuture().thenAccept(either -> {
diff --git a/patches/server/Do-not-overload-I-O-threads-with-chunk-data-while-fl.patch b/patches/server/Do-not-overload-I-O-threads-with-chunk-data-while-fl.patch
deleted file mode 100644
index a30e922275..0000000000
--- a/patches/server/Do-not-overload-I-O-threads-with-chunk-data-while-fl.patch
+++ /dev/null
@@ -1,42 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Spottedleaf <Spottedleaf@users.noreply.github.com>
-Date: Sat, 16 Oct 2021 01:36:00 -0700
-Subject: [PATCH] Do not overload I/O threads with chunk data while flush
- saving
-
-If the chunk count is high, then the memory used by the
-chunks adds up and could cause problems. By flushing
-every so many chunks, the server will not become
-stressed for memory. It will also not increase the total
-time to save, as flush saving performs a full flush at
-the end anyways.
-
-diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/server/level/ChunkMap.java
-+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
-     // Paper end
- 
-     protected void saveAllChunks(boolean flush) {
-+        // Paper start - do not overload I/O threads with too much work when saving
-+        int[] saved = new int[1];
-+        int maxAsyncSaves = 50;
-+        Runnable onChunkSave = () -> {
-+            if (++saved[0] >= maxAsyncSaves) {
-+                saved[0] = 0;
-+                com.destroystokyo.paper.io.PaperFileIOThread.Holder.INSTANCE.flush();
-+            }
-+        };
-+        // Paper end - do not overload I/O threads with too much work when saving
-         if (flush) {
-             List<ChunkHolder> list = (List) this.updatingChunks.getVisibleValuesCopy().stream().filter(ChunkHolder::wasAccessibleSinceLastSave).peek(ChunkHolder::refreshAccessibility).collect(Collectors.toList()); // Paper
-             MutableBoolean mutableboolean = new MutableBoolean();
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
-                 }).filter((ichunkaccess) -> {
-                     return ichunkaccess instanceof ImposterProtoChunk || ichunkaccess instanceof LevelChunk;
-                 }).filter(this::save).forEach((ichunkaccess) -> {
-+                    onChunkSave.run(); // Paper - do not overload I/O threads with too much work when saving
-                     mutableboolean.setTrue();
-                 });
-             } while (mutableboolean.isTrue());
diff --git a/patches/server/Duplicate-UUID-Resolve-Option.patch b/patches/server/Duplicate-UUID-Resolve-Option.patch
index 63e9f25c39..a11fbc6ada 100644
--- a/patches/server/Duplicate-UUID-Resolve-Option.patch
+++ b/patches/server/Duplicate-UUID-Resolve-Option.patch
@@ -32,6 +32,21 @@ But for those who are ok with leaving this inconsistent behavior, you may use WA
 
 It is recommended you regenerate the entities, as these were legit entities, and deserve your love.
 
+diff --git a/src/main/java/net/minecraft/server/ChunkSystem.java b/src/main/java/net/minecraft/server/ChunkSystem.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/ChunkSystem.java
++++ b/src/main/java/net/minecraft/server/ChunkSystem.java
+@@ -0,0 +0,0 @@ public final class ChunkSystem {
+     }
+ 
+     public static void onEntityPreAdd(final ServerLevel level, final Entity entity) {
+-
++        if (net.minecraft.server.level.ChunkMap.checkDupeUUID(level, entity)) {
++            return;
++        }
+     }
+ 
+     public static void onChunkHolderCreate(final ServerLevel level, final ChunkHolder holder) {
 diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
 --- a/src/main/java/net/minecraft/server/level/ChunkMap.java
@@ -49,22 +64,27 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
 +    // Paper start
-+    private static void checkDupeUUID(ServerLevel level, Entity entity) {
++    // rets true if to prevent the entity from being added
++    public static boolean checkDupeUUID(ServerLevel level, Entity entity) {
 +        io.papermc.paper.configuration.WorldConfiguration.Entities.Spawning.DuplicateUUID.DuplicateUUIDMode mode = level.paperConfig().entities.spawning.duplicateUuid.mode;
 +        if (mode != io.papermc.paper.configuration.WorldConfiguration.Entities.Spawning.DuplicateUUID.DuplicateUUIDMode.WARN
 +            && mode != io.papermc.paper.configuration.WorldConfiguration.Entities.Spawning.DuplicateUUID.DuplicateUUIDMode.DELETE
 +            && mode != io.papermc.paper.configuration.WorldConfiguration.Entities.Spawning.DuplicateUUID.DuplicateUUIDMode.SAFE_REGEN) {
-+            return;
++            return false;
 +        }
 +        Entity other = level.getEntity(entity.getUUID());
 +
++        if (other == null || other == entity) {
++            return false;
++        }
++
 +        if (mode == io.papermc.paper.configuration.WorldConfiguration.Entities.Spawning.DuplicateUUID.DuplicateUUIDMode.SAFE_REGEN && other != null && !other.isRemoved()
 +            && Objects.equals(other.getEncodeId(), entity.getEncodeId())
 +            && entity.getBukkitEntity().getLocation().distance(other.getBukkitEntity().getLocation()) < level.paperConfig().entities.spawning.duplicateUuid.safeRegenDeleteRange
 +        ) {
 +            if (ServerLevel.DEBUG_ENTITIES) LOGGER.warn("[DUPE-UUID] Duplicate UUID found used by " + other + ", deleted entity " + entity + " because it was near the duplicate and likely an actual duplicate. See https://github.com/PaperMC/Paper/issues/1223 for discussion on what this is about.");
 +            entity.discard();
-+            return;
++            return true;
 +        }
 +        if (other != null && !other.isRemoved()) {
 +            switch (mode) {
@@ -76,13 +96,14 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                case DELETE: {
 +                    if (ServerLevel.DEBUG_ENTITIES) LOGGER.warn("[DUPE-UUID] Duplicate UUID found used by " + other + ", deleted entity " + entity + ". See https://github.com/PaperMC/Paper/issues/1223 for discussion on what this is about.");
 +                    entity.discard();
-+                    break;
++                    return true;
 +                }
 +                default:
 +                    if (ServerLevel.DEBUG_ENTITIES) LOGGER.warn("[DUPE-UUID] Duplicate UUID found used by " + other + ", doing nothing to " + entity + ". See https://github.com/PaperMC/Paper/issues/1223 for discussion on what this is about.");
 +                    break;
 +            }
 +        }
++        return false;
 +    }
 +    // Paper end
      public CompletableFuture<Either<LevelChunk, ChunkHolder.ChunkLoadingFailure>> prepareTickingChunk(ChunkHolder holder) {
diff --git a/patches/server/Entity-Origin-API.patch b/patches/server/Entity-Origin-API.patch
index f0809e1e0a..e99cef95d1 100644
--- a/patches/server/Entity-Origin-API.patch
+++ b/patches/server/Entity-Origin-API.patch
@@ -136,11 +136,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 --- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
 +++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
 @@ -0,0 +0,0 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity {
-         return this.spigot;
+ 
+         return ret;
      }
-     // Spigot end
 +
-+    // Paper start
 +    @Override
 +    public Location getOrigin() {
 +        Vector originVector = this.getHandle().getOriginVector();
@@ -155,5 +154,5 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        //noinspection ConstantConditions
 +        return originVector.toLocation(world);
 +    }
-+    // Paper end
+     // Paper end
  }
diff --git a/patches/server/Implement-Chunk-Priority-Urgency-System-for-Chunks.patch b/patches/server/Implement-Chunk-Priority-Urgency-System-for-Chunks.patch
index f2d512c2f5..0ab88c8a97 100644
--- a/patches/server/Implement-Chunk-Priority-Urgency-System-for-Chunks.patch
+++ b/patches/server/Implement-Chunk-Priority-Urgency-System-for-Chunks.patch
@@ -67,6 +67,90 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
          }
      }
  
+diff --git a/src/main/java/net/minecraft/server/ChunkSystem.java b/src/main/java/net/minecraft/server/ChunkSystem.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/ChunkSystem.java
++++ b/src/main/java/net/minecraft/server/ChunkSystem.java
+@@ -0,0 +0,0 @@ public final class ChunkSystem {
+ 
+     static final TicketType<Long> CHUNK_LOAD = TicketType.create("chunk_load", Long::compareTo);
+ 
++    // Paper start - priority
++    private static int getPriorityBoost(final PrioritisedExecutor.Priority priority) {
++        if (priority.isLowerOrEqualPriority(PrioritisedExecutor.Priority.NORMAL)) {
++            return 0;
++        }
++
++        int dist = PrioritisedExecutor.Priority.BLOCKING.ordinal() - PrioritisedExecutor.Priority.NORMAL.ordinal();
++
++
++        return (net.minecraft.server.level.DistanceManager.URGENT_PRIORITY * (priority.ordinal() - PrioritisedExecutor.Priority.NORMAL.ordinal())) / dist;
++    }
++    // Paper end - priority
++
+     private static long chunkLoadCounter = 0L;
+     public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final ChunkStatus toStatus,
+                                          final boolean addTicket, final PrioritisedExecutor.Priority priority, final Consumer<ChunkAccess> onComplete) {
+@@ -0,0 +0,0 @@ public final class ChunkSystem {
+         final int minLevel = 33 + ChunkStatus.getDistance(toStatus);
+         final Long chunkReference = addTicket ? Long.valueOf(++chunkLoadCounter) : null;
+         final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ);
++        final int priorityBoost = getPriorityBoost(priority);
+ 
+         if (addTicket) {
+             level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
+         }
+         level.chunkSource.runDistanceManagerUpdates();
+ 
++        if (priorityBoost == net.minecraft.server.level.DistanceManager.URGENT_PRIORITY) {
++            level.chunkSource.markUrgent(chunkPos);
++        } else if (priorityBoost != 0) {
++            level.chunkSource.markHighPriority(chunkPos, priorityBoost);
++        }
++
+         final Consumer<ChunkAccess> loadCallback = (final ChunkAccess chunk) -> {
+             try {
+                 if (onComplete != null) {
+@@ -0,0 +0,0 @@ public final class ChunkSystem {
+                     level.chunkSource.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, minLevel, chunkPos);
+                     level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
+                 }
++                if (priorityBoost == net.minecraft.server.level.DistanceManager.URGENT_PRIORITY) {
++                    level.chunkSource.clearUrgent(chunkPos);
++                } else if (priorityBoost != 0) {
++                    level.chunkSource.clearPriorityTickets(chunkPos);
++                }
+             }
+         };
+ 
+@@ -0,0 +0,0 @@ public final class ChunkSystem {
+         final int radius = toStatus.ordinal() - 1;
+         final Long chunkReference = addTicket ? Long.valueOf(++chunkLoadCounter) : null;
+         final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ);
++        final int priorityBoost = getPriorityBoost(priority);
+ 
+         if (addTicket) {
+             level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
+         }
+         level.chunkSource.runDistanceManagerUpdates();
+ 
++        if (priorityBoost != 0) {
++            level.chunkSource.markAreaHighPriority(chunkPos, priorityBoost, radius);
++        }
++
+         final Consumer<LevelChunk> loadCallback = (final LevelChunk chunk) -> {
+             try {
+                 if (onComplete != null) {
+@@ -0,0 +0,0 @@ public final class ChunkSystem {
+                     level.chunkSource.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, minLevel, chunkPos);
+                     level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
+                 }
++                if (priorityBoost != 0) {
++                    level.chunkSource.clearAreaPriorityTickets(chunkPos, radius);
++                }
+             }
+         };
+ 
 diff --git a/src/main/java/net/minecraft/server/MCUtil.java b/src/main/java/net/minecraft/server/MCUtil.java
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
 --- a/src/main/java/net/minecraft/server/MCUtil.java
@@ -150,11 +234,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                io.papermc.paper.util.TickThread.ensureTickThread("Async full chunk future completion"); // Paper
                  final Optional<LevelChunk> left = either.left();
                  if (left.isPresent() && ChunkHolder.this.fullChunkCreateCount == expectCreateCount) {
-                     // note: Here is a very good place to add callbacks to logic waiting on this.
                      LevelChunk fullChunk = either.left().get();
                      ChunkHolder.this.isFullChunkReady = true;
-                     fullChunk.playerChunk = ChunkHolder.this;
-+                    this.chunkMap.distanceManager.clearPriorityTickets(pos);
+                     net.minecraft.server.ChunkSystem.onChunkBorder(fullChunk, this);
++                    this.chunkMap.distanceManager.clearPriorityTickets(pos); // Paper - chunk priority
                  }
              });
              this.updateChunkToSave(this.fullChunkFuture, "full");
@@ -173,7 +256,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                io.papermc.paper.util.TickThread.ensureTickThread("Async full chunk future completion"); // Paper
                  either.ifLeft(chunk -> {
                      ChunkHolder.this.isEntityTickingReady = true;
-                     // Paper start - entity ticking chunk set
+                     net.minecraft.server.ChunkSystem.onChunkEntityTicking(chunk, this);
 @@ -0,0 +0,0 @@ public class ChunkHolder {
              this.demoteFullChunk(chunkStorage, playerchunk_state1);
          }
@@ -422,9 +505,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +    // Paper end
 +
-     // Paper start
-     public void updatePlayerMobTypeMap(Entity entity) {
-         if (!this.level.paperConfig().entities.spawning.perPlayerMobSpawns) {
+     private static double euclideanDistanceSquared(ChunkPos pos, Entity entity) {
+         double d0 = (double) SectionPos.sectionToBlockCoord(pos.x, 8);
+         double d1 = (double) SectionPos.sectionToBlockCoord(pos.z, 8);
 @@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
          List<ChunkHolder> list1 = new ArrayList();
          int j = centerChunk.x;
@@ -542,7 +625,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
 @@ -0,0 +0,0 @@ public abstract class DistanceManager {
      public boolean runAllUpdates(ChunkMap chunkStorage) {
-         //this.f.a(); // Paper - no longer used
+         this.naturalSpawnChunkCounter.runAllUpdates();
          this.tickingTicketsTracker.runAllUpdates();
 +        org.spigotmc.AsyncCatcher.catchOp("DistanceManagerTick"); // Paper
          this.playerTicketManager.runAllUpdates();
@@ -737,6 +820,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    public void clearPriorityTickets(ChunkPos coords) {
 +        this.distanceManager.clearPriorityTickets(coords);
++    }
++
++    public void clearUrgent(ChunkPos coords) {
++        this.distanceManager.clearUrgent(coords);
 +    }
      // Paper end - async chunk io
  
@@ -808,8 +895,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      private int lastSentFood = -99999999;
      private boolean lastFoodSaturationZero = true;
 @@ -0,0 +0,0 @@ public class ServerPlayer extends Player {
+         this.bukkitPickUpLoot = true;
          this.maxHealthCache = this.getMaxHealth();
-         this.cachedSingleMobDistanceMap = new com.destroystokyo.paper.util.PooledHashSets.PooledObjectLinkedOpenHashSet<>(this); // Paper
      }
 +    // Paper start - Chunk priority
 +    public BlockPos getPointInFront(double inFront) {
@@ -1120,20 +1207,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
      public void placeNewPlayer(Connection connection, ServerPlayer player) {
 +        player.isRealPlayer = true; // Paper - Chunk priority
-         ServerPlayer prev = pendingPlayers.put(player.getUUID(), player);// Paper
-         if (prev != null) {
-             disconnectPendingPlayer(prev);
-@@ -0,0 +0,0 @@ public abstract class PlayerList {
-         net.minecraft.server.level.ChunkMap playerChunkMap = worldserver1.getChunkSource().chunkMap;
-         net.minecraft.server.level.DistanceManager distanceManager = playerChunkMap.distanceManager;
-         distanceManager.addTicket(net.minecraft.server.level.TicketType.LOGIN, pos, 31, pos.toLong());
--        worldserver1.getChunkSource().runDistanceManagerUpdates();
--        worldserver1.getChunkSource().getChunkAtAsynchronously(chunkX, chunkZ, true, true).thenApply(chunk -> {
-+        worldserver1.getChunkSource().markAreaHighPriority(pos, 28, 3); // Paper - Chunk priority
-+        worldserver1.getChunkSource().getChunkAtAsynchronously(chunkX, chunkZ, true, false).thenApply(chunk -> { // Paper - Chunk priority
-             net.minecraft.server.level.ChunkHolder updatingChunk = playerChunkMap.getUpdatingChunkIfPresent(pos.toLong());
-             if (updatingChunk != null) {
-                 return updatingChunk.getEntityTickingChunkFuture();
+         GameProfile gameprofile = player.getGameProfile();
+         GameProfileCache usercache = this.server.getProfileCache();
+         Optional<GameProfile> optional = usercache.get(gameprofile.getId());
 diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
 --- a/src/main/java/net/minecraft/world/entity/Entity.java
@@ -1176,41 +1252,3 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
          org.bukkit.Server server = this.level.getCraftServer();
          org.bukkit.event.world.ChunkUnloadEvent unloadEvent = new org.bukkit.event.world.ChunkUnloadEvent(this.bukkitChunk, this.isUnsaved());
          server.getPluginManager().callEvent(unloadEvent);
-diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
-+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
-@@ -0,0 +0,0 @@ public class CraftWorld extends CraftRegionAccessor implements World {
-             return future;
-         }
- 
-+        // Paper start - Chunk priority
-+        if (!urgent) {
-+            // If not urgent, at least use a slightly boosted priority
-+            world.getChunkSource().markHighPriority(new ChunkPos(x, z), 1);
-+        }
-+        // Paper end
-         return this.world.getChunkSource().getChunkAtAsynchronously(x, z, gen, urgent).thenComposeAsync((either) -> {
-             net.minecraft.world.level.chunk.LevelChunk chunk = (net.minecraft.world.level.chunk.LevelChunk) either.left().orElse(null);
-             if (chunk != null) addTicket(x, z); // Paper
-diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
-+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
-@@ -0,0 +0,0 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
-         throw new UnsupportedOperationException("Cannot set rotation of players. Consider teleporting instead.");
-     }
- 
-+    // Paper start - Chunk priority
-+    @Override
-+    public java.util.concurrent.CompletableFuture<Boolean> teleportAsync(Location loc, @javax.annotation.Nonnull PlayerTeleportEvent.TeleportCause cause) {
-+        ((CraftWorld)loc.getWorld()).getHandle().getChunkSource().markAreaHighPriority(
-+            new net.minecraft.world.level.ChunkPos(net.minecraft.util.Mth.floor(loc.getX()) >> 4,
-+            net.minecraft.util.Mth.floor(loc.getZ()) >> 4), 28, 3); // Load area high priority
-+        return super.teleportAsync(loc, cause);
-+    }
-+    // Paper end
-+
-     @Override
-     public boolean teleport(Location location, PlayerTeleportEvent.TeleportCause cause) {
-         Preconditions.checkArgument(location != null, "location");
diff --git a/patches/server/Load-Chunks-for-Login-Asynchronously.patch b/patches/server/Load-Chunks-for-Login-Asynchronously.patch
index 71c0079d9b..c7fcd376b1 100644
--- a/patches/server/Load-Chunks-for-Login-Asynchronously.patch
+++ b/patches/server/Load-Chunks-for-Login-Asynchronously.patch
@@ -115,17 +115,19 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      // private final Map<UUID, ServerStatisticManager> stats;
      // private final Map<UUID, AdvancementDataPlayer> advancements;
 @@ -0,0 +0,0 @@ public abstract class PlayerList {
-     }
- 
      public void placeNewPlayer(Connection connection, ServerPlayer player) {
-+        ServerPlayer prev = pendingPlayers.put(player.getUUID(), player);// Paper
+         player.isRealPlayer = true; // Paper - Chunk priority
+         player.loginTime = System.currentTimeMillis(); // Paper
++        // Paper start
++        ServerPlayer prev = pendingPlayers.put(player.getUUID(), player);
 +        if (prev != null) {
 +            disconnectPendingPlayer(prev);
 +        }
-+        player.networkManager = connection; // Paper
-         player.loginTime = System.currentTimeMillis(); // Paper
++        player.networkManager = connection;
++        // Paper end
          GameProfile gameprofile = player.getGameProfile();
          GameProfileCache usercache = this.server.getProfileCache();
+         Optional<GameProfile> optional = usercache.get(gameprofile.getId());
 @@ -0,0 +0,0 @@ public abstract class PlayerList {
          if (nbttagcompound != null && nbttagcompound.contains("bukkit")) {
              CompoundTag bukkit = nbttagcompound.getCompound("bukkit");
@@ -164,31 +166,26 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        final net.minecraft.world.level.ChunkPos pos = new net.minecraft.world.level.ChunkPos(chunkX, chunkZ);
 +        net.minecraft.server.level.ChunkMap playerChunkMap = worldserver1.getChunkSource().chunkMap;
 +        net.minecraft.server.level.DistanceManager distanceManager = playerChunkMap.distanceManager;
-+        distanceManager.addTicket(net.minecraft.server.level.TicketType.LOGIN, pos, 31, pos.toLong());
-+        worldserver1.getChunkSource().runDistanceManagerUpdates();
-+        worldserver1.getChunkSource().getChunkAtAsynchronously(chunkX, chunkZ, true, true).thenApply(chunk -> {
-+            net.minecraft.server.level.ChunkHolder updatingChunk = playerChunkMap.getUpdatingChunkIfPresent(pos.toLong());
-+            if (updatingChunk != null) {
-+                return updatingChunk.getEntityTickingChunkFuture();
-+            } else {
-+                return java.util.concurrent.CompletableFuture.completedFuture(chunk);
-+            }
-+        }).thenAccept(chunk -> {
-+            MinecraftServer.getServer().scheduleOnMain(() -> {
-+                try {
-+                    if (!playerconnection.connection.isConnected()) {
-+                        return;
++        net.minecraft.server.ChunkSystem.scheduleTickingState(
++            worldserver1, chunkX, chunkZ, net.minecraft.server.level.ChunkHolder.FullChunkStatus.ENTITY_TICKING, true,
++            ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHEST,
++            (chunk) -> {
++                MinecraftServer.getServer().scheduleOnMain(() -> {
++                    try {
++                        if (!playerconnection.connection.isConnected()) {
++                            return;
++                        }
++                        PlayerList.this.postChunkLoadJoin(
++                            player, finalWorldserver, connection, playerconnection,
++                            nbttagcompound, s1, lastKnownName
++                        );
++                        distanceManager.addTicket(net.minecraft.server.level.TicketType.LOGIN, pos, 31, pos.toLong());
++                    } finally {
++                        finalWorldserver.pendingLogin.remove(player);
 +                    }
-+                    PlayerList.this.postChunkLoadJoin(
-+                        player, finalWorldserver, connection, playerconnection,
-+                        nbttagcompound, s1, lastKnownName
-+                    );
-+                    distanceManager.addTicket(net.minecraft.server.level.TicketType.LOGIN, pos, 31, pos.toLong());
-+                } finally {
-+                    finalWorldserver.pendingLogin.remove(player);
-+                }
-+            });
-+        });
++                });
++            }
++        );
 +    }
 +
 +    public ServerPlayer getActivePlayer(UUID uuid) {
diff --git a/patches/server/MC-Utils.patch b/patches/server/MC-Utils.patch
index 3900efb0e0..9dd8c8c4aa 100644
--- a/patches/server/MC-Utils.patch
+++ b/patches/server/MC-Utils.patch
@@ -4528,6 +4528,281 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
                      if (packet.isSkippable()) {
                          throw new SkipPacketException(var10);
                      } else {
+diff --git a/src/main/java/net/minecraft/server/ChunkSystem.java b/src/main/java/net/minecraft/server/ChunkSystem.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/net/minecraft/server/ChunkSystem.java
+@@ -0,0 +0,0 @@
++package net.minecraft.server;
++
++import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor;
++import com.destroystokyo.paper.util.SneakyThrow;
++import com.mojang.datafixers.util.Either;
++import com.mojang.logging.LogUtils;
++import io.papermc.paper.util.CoordinateUtils;
++import net.minecraft.server.level.ChunkHolder;
++import net.minecraft.server.level.ChunkMap;
++import net.minecraft.server.level.ServerLevel;
++import net.minecraft.server.level.TicketType;
++import net.minecraft.world.entity.Entity;
++import net.minecraft.world.level.ChunkPos;
++import net.minecraft.world.level.chunk.ChunkAccess;
++import net.minecraft.world.level.chunk.ChunkStatus;
++import net.minecraft.world.level.chunk.LevelChunk;
++import org.bukkit.Bukkit;
++import org.slf4j.Logger;
++import java.util.ArrayList;
++import java.util.List;
++import java.util.concurrent.CompletableFuture;
++import java.util.function.Consumer;
++
++public final class ChunkSystem {
++
++    private static final Logger LOGGER = LogUtils.getLogger();
++
++    public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run) {
++        scheduleChunkTask(level, chunkX, chunkZ, run, PrioritisedExecutor.Priority.NORMAL);
++    }
++
++    public static void scheduleChunkTask(final ServerLevel level, final int chunkX, final int chunkZ, final Runnable run, final PrioritisedExecutor.Priority priority) {
++        level.chunkSource.mainThreadProcessor.execute(run);
++    }
++
++    public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final boolean gen,
++                                         final ChunkStatus toStatus, final boolean addTicket, final PrioritisedExecutor.Priority priority,
++                                         final Consumer<ChunkAccess> onComplete) {
++        if (gen) {
++            scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
++            return;
++        }
++        scheduleChunkLoad(level, chunkX, chunkZ, ChunkStatus.EMPTY, addTicket, priority, (final ChunkAccess chunk) -> {
++            if (chunk == null) {
++                onComplete.accept(null);
++            } else {
++                if (chunk.getStatus().isOrAfter(toStatus)) {
++                    scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
++                } else {
++                    onComplete.accept(null);
++                }
++            }
++        });
++    }
++
++    static final TicketType<Long> CHUNK_LOAD = TicketType.create("chunk_load", Long::compareTo);
++
++    private static long chunkLoadCounter = 0L;
++    public static void scheduleChunkLoad(final ServerLevel level, final int chunkX, final int chunkZ, final ChunkStatus toStatus,
++                                         final boolean addTicket, final PrioritisedExecutor.Priority priority, final Consumer<ChunkAccess> onComplete) {
++        if (!Bukkit.isPrimaryThread()) {
++            scheduleChunkTask(level, chunkX, chunkZ, () -> {
++                scheduleChunkLoad(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
++            }, priority);
++            return;
++        }
++
++        final int minLevel = 33 + ChunkStatus.getDistance(toStatus);
++        final Long chunkReference = addTicket ? Long.valueOf(++chunkLoadCounter) : null;
++        final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ);
++
++        if (addTicket) {
++            level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
++        }
++        level.chunkSource.runDistanceManagerUpdates();
++
++        final Consumer<ChunkAccess> loadCallback = (final ChunkAccess chunk) -> {
++            try {
++                if (onComplete != null) {
++                    onComplete.accept(chunk);
++                }
++            } catch (final ThreadDeath death) {
++                throw death;
++            } catch (final Throwable thr) {
++                LOGGER.error("Exception handling chunk load callback", thr);
++                SneakyThrow.sneaky(thr);
++            } finally {
++                if (addTicket) {
++                    level.chunkSource.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, minLevel, chunkPos);
++                    level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
++                }
++            }
++        };
++
++        final ChunkHolder holder = level.chunkSource.chunkMap.getUpdatingChunkIfPresent(CoordinateUtils.getChunkKey(chunkX, chunkZ));
++
++        if (holder == null || holder.getTicketLevel() > minLevel) {
++            loadCallback.accept(null);
++            return;
++        }
++
++        final CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> loadFuture = holder.getOrScheduleFuture(toStatus, level.chunkSource.chunkMap);
++
++        if (loadFuture.isDone()) {
++            loadCallback.accept(loadFuture.join().left().orElse(null));
++            return;
++        }
++
++        loadFuture.whenCompleteAsync((final Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure> either, final Throwable thr) -> {
++            if (thr != null) {
++                loadCallback.accept(null);
++                return;
++            }
++            loadCallback.accept(either.left().orElse(null));
++        }, (final Runnable r) -> {
++            scheduleChunkTask(level, chunkX, chunkZ, r, PrioritisedExecutor.Priority.HIGHEST);
++        });
++    }
++
++    public static void scheduleTickingState(final ServerLevel level, final int chunkX, final int chunkZ,
++                                            final ChunkHolder.FullChunkStatus toStatus, final boolean addTicket,
++                                            final PrioritisedExecutor.Priority priority, final Consumer<LevelChunk> onComplete) {
++        if (toStatus == ChunkHolder.FullChunkStatus.INACCESSIBLE) {
++            throw new IllegalArgumentException("Cannot wait for INACCESSIBLE status");
++        }
++
++        if (!Bukkit.isPrimaryThread()) {
++            scheduleChunkTask(level, chunkX, chunkZ, () -> {
++                scheduleTickingState(level, chunkX, chunkZ, toStatus, addTicket, priority, onComplete);
++            }, priority);
++            return;
++        }
++
++        final int minLevel = 33 - (toStatus.ordinal() - 1);
++        final int radius = toStatus.ordinal() - 1;
++        final Long chunkReference = addTicket ? Long.valueOf(++chunkLoadCounter) : null;
++        final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ);
++
++        if (addTicket) {
++            level.chunkSource.addTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
++        }
++        level.chunkSource.runDistanceManagerUpdates();
++
++        final Consumer<LevelChunk> loadCallback = (final LevelChunk chunk) -> {
++            try {
++                if (onComplete != null) {
++                    onComplete.accept(chunk);
++                }
++            } catch (final ThreadDeath death) {
++                throw death;
++            } catch (final Throwable thr) {
++                LOGGER.error("Exception handling chunk load callback", thr);
++                SneakyThrow.sneaky(thr);
++            } finally {
++                if (addTicket) {
++                    level.chunkSource.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, minLevel, chunkPos);
++                    level.chunkSource.removeTicketAtLevel(CHUNK_LOAD, chunkPos, minLevel, chunkReference);
++                }
++            }
++        };
++
++        final ChunkHolder holder = level.chunkSource.chunkMap.getUpdatingChunkIfPresent(CoordinateUtils.getChunkKey(chunkX, chunkZ));
++
++        if (holder == null || holder.getTicketLevel() > minLevel) {
++            loadCallback.accept(null);
++            return;
++        }
++
++        final CompletableFuture<Either<LevelChunk, ChunkHolder.ChunkLoadingFailure>> tickingState;
++        switch (toStatus) {
++            case BORDER: {
++                tickingState = holder.getFullChunkFuture();
++                break;
++            }
++            case TICKING: {
++                tickingState = holder.getTickingChunkFuture();
++                break;
++            }
++            case ENTITY_TICKING: {
++                tickingState = holder.getEntityTickingChunkFuture();
++                break;
++            }
++            default: {
++                throw new IllegalStateException("Cannot reach here");
++            }
++        }
++
++        if (tickingState.isDone()) {
++            loadCallback.accept(tickingState.join().left().orElse(null));
++            return;
++        }
++
++        tickingState.whenCompleteAsync((final Either<LevelChunk, ChunkHolder.ChunkLoadingFailure> either, final Throwable thr) -> {
++            if (thr != null) {
++                loadCallback.accept(null);
++                return;
++            }
++            loadCallback.accept(either.left().orElse(null));
++        }, (final Runnable r) -> {
++            scheduleChunkTask(level, chunkX, chunkZ, r, PrioritisedExecutor.Priority.HIGHEST);
++        });
++    }
++
++    public static List<ChunkHolder> getVisibleChunkHolders(final ServerLevel level) {
++        return new ArrayList<>(level.chunkSource.chunkMap.visibleChunkMap.values());
++    }
++
++    public static List<ChunkHolder> getUpdatingChunkHolders(final ServerLevel level) {
++        return new ArrayList<>(level.chunkSource.chunkMap.updatingChunkMap.values());
++    }
++
++    public static int getVisibleChunkHolderCount(final ServerLevel level) {
++        return level.chunkSource.chunkMap.visibleChunkMap.size();
++    }
++
++    public static int getUpdatingChunkHolderCount(final ServerLevel level) {
++        return level.chunkSource.chunkMap.updatingChunkMap.size();
++    }
++
++    public static boolean hasAnyChunkHolders(final ServerLevel level) {
++        return getUpdatingChunkHolderCount(level) != 0;
++    }
++
++    public static void onEntityPreAdd(final ServerLevel level, final Entity entity) {
++
++    }
++
++    public static void onChunkHolderCreate(final ServerLevel level, final ChunkHolder holder) {
++        final ChunkMap chunkMap = level.chunkSource.chunkMap;
++        for (int index = 0, len = chunkMap.regionManagers.size(); index < len; ++index) {
++            chunkMap.regionManagers.get(index).addChunk(holder.pos.x, holder.pos.z);
++        }
++    }
++
++    public static void onChunkHolderDelete(final ServerLevel level, final ChunkHolder holder) {
++        final ChunkMap chunkMap = level.chunkSource.chunkMap;
++        for (int index = 0, len = chunkMap.regionManagers.size(); index < len; ++index) {
++            chunkMap.regionManagers.get(index).removeChunk(holder.pos.x, holder.pos.z);
++        }
++    }
++
++    public static void onChunkBorder(LevelChunk chunk, ChunkHolder holder) {
++        chunk.playerChunk = holder;
++    }
++
++    public static void onChunkNotBorder(LevelChunk chunk, ChunkHolder holder) {
++
++    }
++
++    public static void onChunkTicking(LevelChunk chunk, ChunkHolder holder) {
++        chunk.level.getChunkSource().tickingChunks.add(chunk);
++    }
++
++    public static void onChunkNotTicking(LevelChunk chunk, ChunkHolder holder) {
++        chunk.level.getChunkSource().tickingChunks.remove(chunk);
++    }
++
++    public static void onChunkEntityTicking(LevelChunk chunk, ChunkHolder holder) {
++        chunk.level.getChunkSource().entityTickingChunks.add(chunk);
++    }
++
++    public static void onChunkNotEntityTicking(LevelChunk chunk, ChunkHolder holder) {
++        chunk.level.getChunkSource().entityTickingChunks.remove(chunk);
++    }
++
++    private ChunkSystem() {
++        throw new RuntimeException();
++    }
++}
 diff --git a/src/main/java/net/minecraft/server/MCUtil.java b/src/main/java/net/minecraft/server/MCUtil.java
 new file mode 100644
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
@@ -4845,7 +5120,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +     * @return
 +     */
 +    public static void ensureMain(String reason, Runnable run) {
-+        if (AsyncCatcher.enabled && Thread.currentThread() != MinecraftServer.getServer().serverThread) {
++        if (!isMainThread()) {
 +            if (reason != null) {
 +                new IllegalStateException("Asynchronous " + reason + "!").printStackTrace();
 +            }
@@ -4870,7 +5145,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +     * @return
 +     */
 +    public static <T> T ensureMain(String reason, Supplier<T> run) {
-+        if (AsyncCatcher.enabled && Thread.currentThread() != MinecraftServer.getServer().serverThread) {
++        if (!isMainThread()) {
 +            if (reason != null) {
 +                new IllegalStateException("Asynchronous " + reason + "! Blocking thread until it returns ").printStackTrace();
 +            }
@@ -5118,6 +5393,16 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      private CompletableFuture<Void> pendingFullStateConfirmation;
  
 +    private final ChunkMap chunkMap; // Paper
++
++    // Paper start
++    public void onChunkAdd() {
++
++    }
++
++    public void onChunkRemove() {
++
++    }
++    // Paper end
 +
      public ChunkHolder(ChunkPos pos, int level, LevelHeightAccessor world, LevelLightEngine lightingProvider, ChunkHolder.LevelChangeListener levelUpdateListener, ChunkHolder.PlayerProvider playersWatchingChunkProvider) {
          this.futures = new AtomicReferenceArray(ChunkHolder.CHUNK_STATUSES.size());
@@ -5236,16 +5521,20 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            this.fullChunkFuture.thenAccept(either -> {
 +                final Optional<LevelChunk> left = either.left();
 +                if (left.isPresent() && ChunkHolder.this.fullChunkCreateCount == expectCreateCount) {
-+                    // note: Here is a very good place to add callbacks to logic waiting on this.
 +                    LevelChunk fullChunk = either.left().get();
 +                    ChunkHolder.this.isFullChunkReady = true;
-+                    fullChunk.playerChunk = ChunkHolder.this;
++                    net.minecraft.server.ChunkSystem.onChunkBorder(fullChunk, this);
 +                }
 +            });
              this.updateChunkToSave(this.fullChunkFuture, "full");
          }
  
          if (flag2 && !flag3) {
++            // Paper start
++            if (this.isFullChunkReady) {
++                net.minecraft.server.ChunkSystem.onChunkNotBorder(this.fullChunkFuture.join().left().get(), this); // Paper
++            }
++            // Paper end
              this.fullChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK);
              this.fullChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
 +            ++this.fullChunkCreateCount; // Paper - cache ticking ready status
@@ -5262,9 +5551,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                either.ifLeft(chunk -> {
 +                    // note: Here is a very good place to add callbacks to logic waiting on this.
 +                    ChunkHolder.this.isTickingReady = true;
-+                    // Paper start - ticking chunk set
-+                    ChunkHolder.this.chunkMap.level.getChunkSource().tickingChunks.add(chunk);
-+                    // Paper end - ticking chunk set
++                    net.minecraft.server.ChunkSystem.onChunkTicking(chunk, this);
 +                });
 +            });
 +            // Paper end
@@ -5273,17 +5560,15 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
          if (flag4 && !flag5) {
 -            this.tickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK);
++            // Paper start
++            if (this.isTickingReady) {
++                net.minecraft.server.ChunkSystem.onChunkNotTicking(this.tickingChunkFuture.join().left().get(), this); // Paper
++            }
++            // Paper end
 +            this.tickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); this.isTickingReady = false; // Paper - cache chunk ticking stage
              this.tickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
-+            // Paper start - ticking chunk set
-+            LevelChunk chunkIfCached = this.getFullChunkNowUnchecked();
-+            if (chunkIfCached != null) {
-+                this.chunkMap.level.getChunkSource().tickingChunks.remove(chunkIfCached);
-+            }
-+            // Paper end - ticking chunk set
          }
  
-         boolean flag6 = playerchunk_state.isOrAfter(ChunkHolder.FullChunkStatus.ENTITY_TICKING);
 @@ -0,0 +0,0 @@ public class ChunkHolder {
  
              this.entityTickingChunkFuture = chunkStorage.prepareEntityTickingChunk(this.pos);
@@ -5292,9 +5577,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            this.entityTickingChunkFuture.thenAccept(either -> {
 +                either.ifLeft(chunk -> {
 +                    ChunkHolder.this.isEntityTickingReady = true;
-+                    // Paper start - entity ticking chunk set
-+                    ChunkHolder.this.chunkMap.level.getChunkSource().entityTickingChunks.add(chunk);
-+                    // Paper end - entity ticking chunk set
++                    net.minecraft.server.ChunkSystem.onChunkEntityTicking(chunk, this);
 +                });
 +            });
 +            // Paper end
@@ -5303,17 +5586,15 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
          if (flag6 && !flag7) {
 -            this.entityTickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK);
++            // Paper start
++            if (this.isEntityTickingReady) {
++                net.minecraft.server.ChunkSystem.onChunkNotEntityTicking(this.entityTickingChunkFuture.join().left().get(), this);
++            }
++            // Paper end
 +            this.entityTickingChunkFuture.complete(ChunkHolder.UNLOADED_LEVEL_CHUNK); this.isEntityTickingReady = false; // Paper - cache chunk ticking stage
              this.entityTickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
-+            // Paper start - entity ticking chunk set
-+            LevelChunk chunkIfCached = this.getFullChunkNowUnchecked();
-+            if (chunkIfCached != null) {
-+                this.chunkMap.level.getChunkSource().entityTickingChunks.remove(chunkIfCached);
-+            }
-+            // Paper end - entity ticking chunk set
          }
  
-         if (!playerchunk_state1.isOrAfter(playerchunk_state)) {
 @@ -0,0 +0,0 @@ public class ChunkHolder {
              }
          };
@@ -5428,18 +5709,77 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      private CompletableFuture<Either<List<ChunkAccess>, ChunkHolder.ChunkLoadingFailure>> getChunkRangeFuture(ChunkPos centerChunk, int margin, IntFunction<ChunkStatus> distanceToStatus) {
          List<CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>>> list = new ArrayList();
          List<ChunkHolder> list1 = new ArrayList();
+@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+         };
+ 
+         stringbuilder.append("Updating:").append(System.lineSeparator());
+-        this.updatingChunkMap.values().forEach(consumer);
++        net.minecraft.server.ChunkSystem.getUpdatingChunkHolders(this.level).forEach(consumer); // Paper
+         stringbuilder.append("Visible:").append(System.lineSeparator());
+-        this.visibleChunkMap.values().forEach(consumer);
++        net.minecraft.server.ChunkSystem.getVisibleChunkHolders(this.level).forEach(consumer); // Paper
+         CrashReport crashreport = CrashReport.forThrowable(exception, "Chunk loading");
+         CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Chunk loading");
+ 
 @@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
                      holder.setTicketLevel(level);
                  } else {
                      holder = new ChunkHolder(new ChunkPos(pos), level, this.level, this.lightEngine, this.queueSorter, this);
 +                    // Paper start
-+                    for (int index = 0, len = this.regionManagers.size(); index < len; ++index) {
-+                        this.regionManagers.get(index).addChunk(holder.pos.x, holder.pos.z);
-+                    }
++                    net.minecraft.server.ChunkSystem.onChunkHolderCreate(this.level, holder);
 +                    // Paper end
                  }
  
++                // Paper start
++                holder.onChunkAdd();
++                // Paper end
                  this.updatingChunkMap.put(pos, holder);
+                 this.modified = true;
+             }
+@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ 
+     protected void saveAllChunks(boolean flush) {
+         if (flush) {
+-            List<ChunkHolder> list = (List) this.visibleChunkMap.values().stream().filter(ChunkHolder::wasAccessibleSinceLastSave).peek(ChunkHolder::refreshAccessibility).collect(Collectors.toList());
++            List<ChunkHolder> list = (List) net.minecraft.server.ChunkSystem.getVisibleChunkHolders(this.level).stream().filter(ChunkHolder::wasAccessibleSinceLastSave).peek(ChunkHolder::refreshAccessibility).collect(Collectors.toList()); // Paper
+             MutableBoolean mutableboolean = new MutableBoolean();
+ 
+             do {
+@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+             });
+             this.flushWorker();
+         } else {
+-            this.visibleChunkMap.values().forEach(this::saveChunkIfNeeded);
++            net.minecraft.server.ChunkSystem.getVisibleChunkHolders(this.level).forEach(this::saveChunkIfNeeded);
+         }
+ 
+     }
+@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+     }
+ 
+     public boolean hasWork() {
+-        return this.lightEngine.hasLightWork() || !this.pendingUnloads.isEmpty() || !this.updatingChunkMap.isEmpty() || this.poiManager.hasWork() || !this.toDrop.isEmpty() || !this.unloadQueue.isEmpty() || this.queueSorter.hasWork() || this.distanceManager.hasTickets();
++        return this.lightEngine.hasLightWork() || !this.pendingUnloads.isEmpty() || net.minecraft.server.ChunkSystem.hasAnyChunkHolders(this.level) || this.poiManager.hasWork() || !this.toDrop.isEmpty() || !this.unloadQueue.isEmpty() || this.queueSorter.hasWork() || this.distanceManager.hasTickets(); // Paper
+     }
+ 
+     private void processUnloads(BooleanSupplier shouldKeepTicking) {
+@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+             ChunkHolder playerchunk = (ChunkHolder) this.updatingChunkMap.remove(j);
+ 
+             if (playerchunk != null) {
++                playerchunk.onChunkRemove(); // Paper
+                 this.pendingUnloads.put(j, playerchunk);
+                 this.modified = true;
+                 ++i;
+@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+         }
+ 
+         int l = 0;
+-        ObjectIterator objectiterator = this.visibleChunkMap.values().iterator();
++        Iterator objectiterator = net.minecraft.server.ChunkSystem.getVisibleChunkHolders(this.level).iterator(); // Paper
+ 
+         while (l < 20 && shouldKeepTicking.getAsBoolean() && objectiterator.hasNext()) {
+             if (this.saveChunkIfNeeded((ChunkHolder) objectiterator.next())) {
 @@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
              if (completablefuture1 != completablefuture) {
                  this.scheduleUnload(pos, holder);
@@ -5448,9 +5788,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                // Paper start
 +                boolean removed;
 +                if ((removed = this.pendingUnloads.remove(pos, holder)) && ichunkaccess != null) {
-+                    for (int index = 0, len = this.regionManagers.size(); index < len; ++index) {
-+                        this.regionManagers.get(index).removeChunk(holder.pos.x, holder.pos.z);
-+                    }
++                    net.minecraft.server.ChunkSystem.onChunkHolderDelete(this.level, holder);
 +                    // Paper end
                      if (ichunkaccess instanceof LevelChunk) {
                          ((LevelChunk) ichunkaccess).setLoaded(false);
@@ -5459,15 +5797,57 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
                      this.lightEngine.tryScheduleUpdate();
                      this.progressListener.onStatusChange(ichunkaccess.getPos(), (ChunkStatus) null);
                      this.chunkSaveCooldowns.remove(ichunkaccess.getPos().toLong());
+-                }
 +                } else if (removed) { // Paper start
-+                for (int index = 0, len = this.regionManagers.size(); index < len; ++index) {
-+                    this.regionManagers.get(index).removeChunk(holder.pos.x, holder.pos.z);
-                 }
-+            } // Paper end
++                    net.minecraft.server.ChunkSystem.onChunkHolderDelete(this.level, holder);
++                } // Paper end
  
              }
          };
 @@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ 
+             this.viewDistance = j;
+             this.distanceManager.updatePlayerTickets(this.viewDistance + 1);
+-            ObjectIterator objectiterator = this.updatingChunkMap.values().iterator();
++            Iterator objectiterator = net.minecraft.server.ChunkSystem.getUpdatingChunkHolders(this.level).iterator(); // Paper
+ 
+             while (objectiterator.hasNext()) {
+                 ChunkHolder playerchunk = (ChunkHolder) objectiterator.next();
+@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+     }
+ 
+     public int size() {
+-        return this.visibleChunkMap.size();
++        return net.minecraft.server.ChunkSystem.getVisibleChunkHolderCount(this.level); // Paper
+     }
+ 
+     public DistanceManager getDistanceManager() {
+@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+     }
+ 
+     protected Iterable<ChunkHolder> getChunks() {
+-        return Iterables.unmodifiableIterable(this.visibleChunkMap.values());
++        return Iterables.unmodifiableIterable(net.minecraft.server.ChunkSystem.getVisibleChunkHolders(this.level)); // Paper
+     }
+ 
+     void dumpChunks(Writer writer) throws IOException {
+         CsvOutput csvwriter = CsvOutput.builder().addColumn("x").addColumn("z").addColumn("level").addColumn("in_memory").addColumn("status").addColumn("full_status").addColumn("accessible_ready").addColumn("ticking_ready").addColumn("entity_ticking_ready").addColumn("ticket").addColumn("spawning").addColumn("block_entity_count").addColumn("ticking_ticket").addColumn("ticking_level").addColumn("block_ticks").addColumn("fluid_ticks").build(writer);
+         TickingTracker tickingtracker = this.distanceManager.tickingTracker();
+-        ObjectBidirectionalIterator objectbidirectionaliterator = this.visibleChunkMap.long2ObjectEntrySet().iterator();
++        Iterator<ChunkHolder> objectbidirectionaliterator = net.minecraft.server.ChunkSystem.getVisibleChunkHolders(this.level).iterator(); // Paper
+ 
+         while (objectbidirectionaliterator.hasNext()) {
+-            Entry<ChunkHolder> entry = (Entry) objectbidirectionaliterator.next();
+-            long i = entry.getLongKey();
++            ChunkHolder playerchunk = objectbidirectionaliterator.next(); // Paper
++            long i = playerchunk.pos.toLong(); // Paper
+             ChunkPos chunkcoordintpair = new ChunkPos(i);
+-            ChunkHolder playerchunk = (ChunkHolder) entry.getValue();
++            // Paper
+             Optional<ChunkAccess> optional = Optional.ofNullable(playerchunk.getLastAvailable());
+             Optional<LevelChunk> optional1 = optional.flatMap((ichunkaccess) -> {
+                 return ichunkaccess instanceof LevelChunk ? Optional.of((LevelChunk) ichunkaccess) : Optional.empty();
+@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
              if (!flag1) {
                  this.distanceManager.addPlayer(SectionPos.of((EntityAccess) player), player);
              }
@@ -5597,7 +5977,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +        LevelChunk cachedChunk = this.lastLoadedChunks[cacheKey];
 +        if (cachedChunk != null && cachedChunk.locX == x & cachedChunk.locZ == z) {
-+            return this.lastLoadedChunks[cacheKey];
++            return cachedChunk;
 +        }
 +
 +        long chunkKey = ChunkPos.asLong(x, z);
@@ -5623,80 +6003,24 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    long chunkFutureAwaitCounter; // Paper - private -> package private
 +
 +    public void getEntityTickingChunkAsync(int x, int z, java.util.function.Consumer<LevelChunk> onLoad) {
-+        if (Thread.currentThread() != this.mainThread) {
-+            this.mainThreadProcessor.execute(() -> {
-+                ServerChunkCache.this.getEntityTickingChunkAsync(x, z, onLoad);
-+            });
-+            return;
-+        }
-+        this.getChunkFutureAsynchronously(x, z, 31, ChunkHolder::getEntityTickingChunkFuture, onLoad);
++        net.minecraft.server.ChunkSystem.scheduleTickingState(
++            this.level, x, z, ChunkHolder.FullChunkStatus.ENTITY_TICKING, true,
++            ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL, onLoad
++        );
 +    }
 +
 +    public void getTickingChunkAsync(int x, int z, java.util.function.Consumer<LevelChunk> onLoad) {
-+        if (Thread.currentThread() != this.mainThread) {
-+            this.mainThreadProcessor.execute(() -> {
-+                ServerChunkCache.this.getTickingChunkAsync(x, z, onLoad);
-+            });
-+            return;
-+        }
-+        this.getChunkFutureAsynchronously(x, z, 32, ChunkHolder::getTickingChunkFuture, onLoad);
++        net.minecraft.server.ChunkSystem.scheduleTickingState(
++            this.level, x, z, ChunkHolder.FullChunkStatus.TICKING, true,
++            ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL, onLoad
++        );
 +    }
 +
 +    public void getFullChunkAsync(int x, int z, java.util.function.Consumer<LevelChunk> onLoad) {
-+        if (Thread.currentThread() != this.mainThread) {
-+            this.mainThreadProcessor.execute(() -> {
-+                ServerChunkCache.this.getFullChunkAsync(x, z, onLoad);
-+            });
-+            return;
-+        }
-+        this.getChunkFutureAsynchronously(x, z, 33, ChunkHolder::getFullChunkFuture, onLoad);
-+    }
-+
-+    private void getChunkFutureAsynchronously(int x, int z, int ticketLevel, java.util.function.Function<ChunkHolder, CompletableFuture<Either<LevelChunk, ChunkHolder.ChunkLoadingFailure>>> futureGet, java.util.function.Consumer<LevelChunk> onLoad) {
-+        if (Thread.currentThread() != this.mainThread) {
-+            throw new IllegalStateException();
-+        }
-+        ChunkPos chunkPos = new ChunkPos(x, z);
-+        Long identifier = this.chunkFutureAwaitCounter++;
-+        this.distanceManager.addTicket(TicketType.FUTURE_AWAIT, chunkPos, ticketLevel, identifier);
-+        this.runDistanceManagerUpdates();
-+
-+        ChunkHolder chunk = this.chunkMap.getUpdatingChunkIfPresent(chunkPos.toLong());
-+
-+        if (chunk == null) {
-+            throw new IllegalStateException("Expected playerchunk " + chunkPos + " in world '" + this.level.getWorld().getName() + "'");
-+        }
-+
-+        CompletableFuture<Either<LevelChunk, ChunkHolder.ChunkLoadingFailure>> future = futureGet.apply(chunk);
-+
-+        future.whenCompleteAsync((either, throwable) -> {
-+            try {
-+                if (throwable != null) {
-+                    if (throwable instanceof ThreadDeath) {
-+                        throw (ThreadDeath)throwable;
-+                    }
-+                    net.minecraft.server.MinecraftServer.LOGGER.error("Failed to complete future await for chunk " + chunkPos.toString() + " in world '" + ServerChunkCache.this.level.getWorld().getName() + "'", throwable);
-+                } else if (either.right().isPresent()) {
-+                    net.minecraft.server.MinecraftServer.LOGGER.error("Failed to complete future await for chunk " + chunkPos.toString() + " in world '" + ServerChunkCache.this.level.getWorld().getName() + "': " + either.right().get().toString());
-+                }
-+
-+                try {
-+                    if (onLoad != null) {
-+                        onLoad.accept(either == null ? null : either.left().orElse(null)); // indicate failure to the callback.
-+                    }
-+                } catch (Throwable thr) {
-+                    if (thr instanceof ThreadDeath) {
-+                        throw (ThreadDeath)thr;
-+                    }
-+                    net.minecraft.server.MinecraftServer.LOGGER.error("Load callback for future await failed " + chunkPos.toString() + " in world '" + ServerChunkCache.this.level.getWorld().getName() + "'", thr);
-+                    return;
-+                }
-+            } finally {
-+                // due to odd behaviour with CB unload implementation we need to have these AFTER the load callback.
-+                ServerChunkCache.this.distanceManager.addTicket(TicketType.UNKNOWN, chunkPos, ticketLevel, chunkPos);
-+                ServerChunkCache.this.distanceManager.removeTicket(TicketType.FUTURE_AWAIT, chunkPos, ticketLevel, identifier);
-+            }
-+        }, this.mainThreadProcessor);
++        net.minecraft.server.ChunkSystem.scheduleTickingState(
++            this.level, x, z, ChunkHolder.FullChunkStatus.BORDER, true,
++            ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL, onLoad
++        );
 +    }
 +    // Paper end
 +
@@ -5739,67 +6063,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +    }
 +
-+    void getChunkAtAsynchronously(int chunkX, int chunkZ, int ticketLevel,
-+                                  java.util.function.Consumer<ChunkAccess> consumer) {
-+        this.getChunkAtAsynchronously(chunkX, chunkZ, ticketLevel, (ChunkHolder chunkHolder) -> {
-+            if (ticketLevel <= 33) {
-+                return (CompletableFuture)chunkHolder.getFullChunkFuture();
-+            } else {
-+                return chunkHolder.getOrScheduleFuture(ChunkHolder.getStatus(ticketLevel), ServerChunkCache.this.chunkMap);
-+            }
-+        }, consumer);
-+    }
-+
-+    void getChunkAtAsynchronously(int chunkX, int chunkZ, int ticketLevel,
-+                                  java.util.function.Function<ChunkHolder, CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>>> function,
-+                                  java.util.function.Consumer<ChunkAccess> consumer) {
-+        if (Thread.currentThread() != this.mainThread) {
-+            throw new IllegalStateException();
-+        }
-+
-+        ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ);
-+        Long identifier = Long.valueOf(this.chunkFutureAwaitCounter++);
-+        this.addTicketAtLevel(TicketType.FUTURE_AWAIT, chunkPos, ticketLevel, identifier);
-+        this.runDistanceManagerUpdates();
-+
-+        ChunkHolder chunk = this.chunkMap.getUpdatingChunkIfPresent(chunkPos.toLong());
-+
-+        if (chunk == null) {
-+            throw new IllegalStateException("Expected playerchunk " + chunkPos + " in world '" + this.level.getWorld().getName() + "'");
-+        }
-+
-+        CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> future = function.apply(chunk);
-+
-+        future.whenCompleteAsync((either, throwable) -> {
-+            try {
-+                if (throwable != null) {
-+                    if (throwable instanceof ThreadDeath) {
-+                        throw (ThreadDeath)throwable;
-+                    }
-+                    LOGGER.error("Failed to complete future await for chunk " + chunkPos.toString() + " in world '" + ServerChunkCache.this.level.getWorld().getName() + "'", throwable);
-+                } else if (either.right().isPresent()) {
-+                    LOGGER.error("Failed to complete future await for chunk " + chunkPos.toString() + " in world '" + ServerChunkCache.this.level.getWorld().getName() + "': " + either.right().get().toString());
-+                }
-+
-+                try {
-+                    if (consumer != null) {
-+                        consumer.accept(either == null ? null : either.left().orElse(null)); // indicate failure to the callback.
-+                    }
-+                } catch (Throwable thr) {
-+                    if (thr instanceof ThreadDeath) {
-+                        throw (ThreadDeath)thr;
-+                    }
-+                    LOGGER.error("Load callback for future await failed " + chunkPos.toString() + " in world '" + ServerChunkCache.this.level.getWorld().getName() + "'", thr);
-+                    return;
-+                }
-+            } finally {
-+                // due to odd behaviour with CB unload implementation we need to have these AFTER the load callback.
-+                ServerChunkCache.this.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, ticketLevel, chunkPos);
-+                ServerChunkCache.this.removeTicketAtLevel(TicketType.FUTURE_AWAIT, chunkPos, ticketLevel, identifier);
-+            }
-+        }, this.mainThreadProcessor);
-+    }
-+
 +    public <T> void addTicketAtLevel(TicketType<T> ticketType, ChunkPos chunkPos, int ticketLevel, T identifier) {
 +        this.distanceManager.addTicket(ticketType, chunkPos, ticketLevel, identifier);
 +    }
@@ -5808,74 +6071,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        this.distanceManager.removeTicket(ticketType, chunkPos, ticketLevel, identifier);
 +    }
 +
-+    void chunkLoadAccept(int chunkX, int chunkZ, ChunkAccess chunk, java.util.function.Consumer<ChunkAccess> consumer) {
-+        try {
-+            consumer.accept(chunk);
-+        } catch (Throwable throwable) {
-+            if (throwable instanceof ThreadDeath) {
-+                throw (ThreadDeath)throwable;
-+            }
-+            LOGGER.error("Load callback for chunk " + chunkX + "," + chunkZ + " in world '" + this.level.getWorld().getName() + "' threw an exception", throwable);
-+        }
-+    }
-+
-+    public final void getChunkAtAsynchronously(int chunkX, int chunkZ, ChunkStatus status, boolean gen, boolean allowSubTicketLevel, java.util.function.Consumer<ChunkAccess> onLoad) {
-+        // try to fire sync
-+        int chunkStatusTicketLevel = 33 + ChunkStatus.getDistance(status);
-+        ChunkHolder playerChunk = this.chunkMap.getUpdatingChunkIfPresent(io.papermc.paper.util.CoordinateUtils.getChunkKey(chunkX, chunkZ));
-+        if (playerChunk != null) {
-+            ChunkStatus holderStatus = playerChunk.getChunkHolderStatus();
-+            ChunkAccess immediate = playerChunk.getAvailableChunkNow();
-+            if (immediate != null) {
-+                if (allowSubTicketLevel ? immediate.getStatus().isOrAfter(status) : (playerChunk.getTicketLevel() <= chunkStatusTicketLevel && holderStatus != null && holderStatus.isOrAfter(status))) {
-+                    this.chunkLoadAccept(chunkX, chunkZ, immediate, onLoad);
-+                    return;
-+                } else {
-+                    if (gen || (!allowSubTicketLevel && immediate.getStatus().isOrAfter(status))) {
-+                        this.getChunkAtAsynchronously(chunkX, chunkZ, chunkStatusTicketLevel, onLoad);
-+                        return;
-+                    } else {
-+                        this.chunkLoadAccept(chunkX, chunkZ, null, onLoad);
-+                        return;
-+                    }
-+                }
-+            }
-+        }
-+
-+        // need to fire async
-+
-+        if (gen && !allowSubTicketLevel) {
-+            this.getChunkAtAsynchronously(chunkX, chunkZ, chunkStatusTicketLevel, onLoad);
-+            return;
-+        }
-+
-+        this.getChunkAtAsynchronously(chunkX, chunkZ, net.minecraft.server.MCUtil.getTicketLevelFor(ChunkStatus.EMPTY), (ChunkAccess chunk) -> {
-+            if (chunk == null) {
-+                throw new IllegalStateException("Chunk cannot be null");
-+            }
-+
-+            if (!chunk.getStatus().isOrAfter(status)) {
-+                if (gen) {
-+                    this.getChunkAtAsynchronously(chunkX, chunkZ, chunkStatusTicketLevel, onLoad);
-+                    return;
-+                } else {
-+                    ServerChunkCache.this.chunkLoadAccept(chunkX, chunkZ, null, onLoad);
-+                    return;
-+                }
-+            } else {
-+                if (allowSubTicketLevel) {
-+                    ServerChunkCache.this.chunkLoadAccept(chunkX, chunkZ, chunk, onLoad);
-+                    return;
-+                } else {
-+                    this.getChunkAtAsynchronously(chunkX, chunkZ, chunkStatusTicketLevel, onLoad);
-+                    return;
-+                }
-+            }
-+        });
-+    }
-+
-+    final io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<LevelChunk> tickingChunks = new io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<>(4096, 0.75f, 4096, 0.15, true);
-+    final io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<LevelChunk> entityTickingChunks = new io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<>(4096, 0.75f, 4096, 0.15, true);
++    public final io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<LevelChunk> tickingChunks = new io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<>(4096, 0.75f, 4096, 0.15, true);
++    public final io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<LevelChunk> entityTickingChunks = new io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet<>(4096, 0.75f, 4096, 0.15, true);
 +    // Paper end
  
      public ServerChunkCache(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor workerExecutor, ChunkGenerator chunkGenerator, int viewDistance, int simulationDistance, boolean dsync, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier<DimensionDataStorage> persistentStateManagerFactory) {
@@ -5989,11 +6186,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return true;
 +    }
 +
-+    public final void loadChunksForMoveAsync(AABB axisalignedbb, double toX, double toZ,
++    public final void loadChunksForMoveAsync(AABB axisalignedbb, ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority,
 +                                             java.util.function.Consumer<List<net.minecraft.world.level.chunk.ChunkAccess>> onLoad) {
 +        if (Thread.currentThread() != this.thread) {
 +            this.getChunkSource().mainThreadProcessor.execute(() -> {
-+                this.loadChunksForMoveAsync(axisalignedbb, toX, toZ, onLoad);
++                this.loadChunksForMoveAsync(axisalignedbb, priority, onLoad);
 +            });
 +            return;
 +        }
@@ -6043,7 +6240,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +        for (int cx = minChunkX; cx <= maxChunkX; ++cx) {
 +            for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) {
-+                chunkProvider.getChunkAtAsynchronously(cx, cz, net.minecraft.world.level.chunk.ChunkStatus.FULL, true, false, consumer);
++                net.minecraft.server.ChunkSystem.scheduleChunkLoad(
++                    this, cx, cz, net.minecraft.world.level.chunk.ChunkStatus.FULL, true, priority, consumer
++                );
 +            }
 +        }
 +    }
@@ -6790,6 +6989,128 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      @Override
      public BlockState getBlockState(BlockPos pos) {
          int i = pos.getY();
+diff --git a/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java b/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java
++++ b/src/main/java/net/minecraft/world/level/entity/PersistentEntitySectionManager.java
+@@ -0,0 +0,0 @@ public class PersistentEntitySectionManager<T extends EntityAccess> implements A
+     }
+ 
+     private boolean addEntity(T entity, boolean existing) {
++        // Paper start - chunk system hooks
++        if (existing) {
++            // I don't want to know why this is a generic type.
++            Entity entityCasted = (Entity)entity;
++            boolean wasRemoved = entityCasted.isRemoved();
++            net.minecraft.server.ChunkSystem.onEntityPreAdd((net.minecraft.server.level.ServerLevel)entityCasted.level, entityCasted);
++            if (!wasRemoved && entityCasted.isRemoved()) {
++                // removed by callback
++                return false;
++            }
++        }
++        // Paper end - chunk system hooks
+         if (!this.addEntityUuid(entity)) {
+             return false;
+         } else {
+diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
++++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
+@@ -0,0 +0,0 @@ public class CraftWorld extends CraftRegionAccessor implements World {
+ 
+     @Override
+     public Chunk[] getLoadedChunks() {
+-        Long2ObjectLinkedOpenHashMap<ChunkHolder> chunks = this.world.getChunkSource().chunkMap.visibleChunkMap;
+-        return chunks.values().stream().map(ChunkHolder::getFullChunkNow).filter(Objects::nonNull).map(net.minecraft.world.level.chunk.LevelChunk::getBukkitChunk).toArray(Chunk[]::new);
++        List<ChunkHolder> chunks = net.minecraft.server.ChunkSystem.getVisibleChunkHolders(this.world); // Paper
++        return chunks.stream().map(ChunkHolder::getFullChunkNow).filter(Objects::nonNull).map(net.minecraft.world.level.chunk.LevelChunk::getBukkitChunk).toArray(Chunk[]::new);
+     }
+ 
+     @Override
+@@ -0,0 +0,0 @@ public class CraftWorld extends CraftRegionAccessor implements World {
+ 
+     @Override
+     public boolean refreshChunk(int x, int z) {
+-        ChunkHolder playerChunk = this.world.getChunkSource().chunkMap.visibleChunkMap.get(ChunkPos.asLong(x, z));
++        ChunkHolder playerChunk = this.world.getChunkSource().chunkMap.getVisibleChunkIfPresent(ChunkPos.asLong(x, z));
+         if (playerChunk == null) return false;
+ 
+         playerChunk.getTickingChunkFuture().thenAccept(either -> {
+@@ -0,0 +0,0 @@ public class CraftWorld extends CraftRegionAccessor implements World {
+         return this.spigot;
+     }
+     // Spigot end
++    // Paper start
++    public java.util.concurrent.CompletableFuture<Chunk> getChunkAtAsync(int x, int z, boolean gen, boolean urgent) {
++        if (Bukkit.isPrimaryThread()) {
++            net.minecraft.world.level.chunk.LevelChunk immediate = this.world.getChunkSource().getChunkAtIfLoadedImmediately(x, z);
++            if (immediate != null) {
++                return java.util.concurrent.CompletableFuture.completedFuture(immediate.getBukkitChunk());
++            }
++        }
++
++        ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority priority;
++        if (urgent) {
++            priority = ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHER;
++        } else {
++            priority = ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL;
++        }
++
++        java.util.concurrent.CompletableFuture<Chunk> ret = new java.util.concurrent.CompletableFuture<>();
++
++        net.minecraft.server.ChunkSystem.scheduleChunkLoad(this.getHandle(), x, z, gen, ChunkStatus.FULL, true, priority, (c) -> {
++            net.minecraft.server.MinecraftServer.getServer().scheduleOnMain(() -> {
++                net.minecraft.world.level.chunk.LevelChunk chunk = (net.minecraft.world.level.chunk.LevelChunk)c;
++                ret.complete(chunk == null ? null : chunk.getBukkitChunk());
++            });
++        });
++
++        return ret;
++    }
++    // Paper end
+ }
+diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java
+@@ -0,0 +0,0 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity {
+         return this.spigot;
+     }
+     // Spigot end
++
++    // Paper start
++    @Override
++    public java.util.concurrent.CompletableFuture<Boolean> teleportAsync(Location location, TeleportCause cause) {
++        Preconditions.checkArgument(location != null, "location");
++        location.checkFinite();
++        Location locationClone = location.clone(); // clone so we don't need to worry about mutations after this call.
++
++        net.minecraft.server.level.ServerLevel world = ((CraftWorld)locationClone.getWorld()).getHandle();
++        java.util.concurrent.CompletableFuture<Boolean> ret = new java.util.concurrent.CompletableFuture<>();
++
++        world.loadChunksForMoveAsync(getHandle().getBoundingBoxAt(locationClone.getX(), locationClone.getY(), locationClone.getZ()),
++            this instanceof CraftPlayer ? ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHER : ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL, (list) -> {
++            net.minecraft.server.level.ServerChunkCache chunkProviderServer = world.getChunkSource();
++            for (net.minecraft.world.level.chunk.ChunkAccess chunk : list) {
++                chunkProviderServer.addTicketAtLevel(net.minecraft.server.level.TicketType.POST_TELEPORT, chunk.getPos(), 33, CraftEntity.this.getEntityId());
++            }
++            net.minecraft.server.MinecraftServer.getServer().scheduleOnMain(() -> {
++                try {
++                    ret.complete(CraftEntity.this.teleport(locationClone, cause) ? Boolean.TRUE : Boolean.FALSE);
++                } catch (Throwable throwable) {
++                    if (throwable instanceof ThreadDeath) {
++                        throw (ThreadDeath)throwable;
++                    }
++                    net.minecraft.server.MinecraftServer.LOGGER.error("Failed to teleport entity " + CraftEntity.this, throwable);
++                    ret.completeExceptionally(throwable);
++                }
++            });
++        });
++
++        return ret;
++    }
++    // Paper end
+ }
 diff --git a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
 --- a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java
diff --git a/patches/server/Make-targetSize-more-aggressive-in-the-chunk-unload-.patch b/patches/server/Make-targetSize-more-aggressive-in-the-chunk-unload-.patch
index 84529f36f8..0c91964dac 100644
--- a/patches/server/Make-targetSize-more-aggressive-in-the-chunk-unload-.patch
+++ b/patches/server/Make-targetSize-more-aggressive-in-the-chunk-unload-.patch
@@ -24,7 +24,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 -
          for (int i = 0; longiterator.hasNext() && (shouldKeepTicking.getAsBoolean() || i < 200 || this.toDrop.size() > 2000); longiterator.remove()) {
              long j = longiterator.nextLong();
-             ChunkHolder playerchunk = (ChunkHolder) this.updatingChunkMap.remove(j);
+             ChunkHolder playerchunk = this.updatingChunks.queueRemove(j); // Paper - Don't copy
 @@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
              }
          }
diff --git a/patches/server/More-Teleport-API.patch b/patches/server/More-Teleport-API.patch
index 81e4f6d120..01090594cf 100644
--- a/patches/server/More-Teleport-API.patch
+++ b/patches/server/More-Teleport-API.patch
@@ -88,9 +88,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        // Paper end
      }
  
-     // Paper start - Chunk priority
-@@ -0,0 +0,0 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
- 
      @Override
      public boolean teleport(Location location, PlayerTeleportEvent.TeleportCause cause) {
 +        // Paper start - Teleport API
diff --git a/patches/server/Not-implemeneted.patch b/patches/server/Not-implemeneted.patch
index cf3d0385f2..70449c12f0 100644
--- a/patches/server/Not-implemeneted.patch
+++ b/patches/server/Not-implemeneted.patch
@@ -14,6 +14,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +package io.papermc.paper.util;
 +
 +import net.minecraft.server.MinecraftServer;
++import net.minecraft.server.level.ServerLevel;
 +import net.minecraft.world.entity.Entity;
 +import org.bukkit.Bukkit;
 +import java.util.concurrent.atomic.AtomicInteger;
@@ -42,15 +43,15 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +    }
 +
-+    public static void ensureTickThread(final int chunkX, final int chunkZ, final String reason) {
-+        if (!isTickThreadFor(chunkX, chunkZ)) {
++    public static void ensureTickThread(final ServerLevel world, final int chunkX, final int chunkZ, final String reason) {
++        if (!isTickThreadFor(world, chunkX, chunkZ)) {
 +            MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
 +            throw new IllegalStateException(reason);
 +        }
 +    }
 +
 +    public static void ensureTickThread(final Entity entity, final String reason) {
-+        if (!isTickThreadFor(entity.chunkPosition().x, entity.chunkPosition().z)) {
++        if (!isTickThreadFor(entity)) {
 +            MinecraftServer.LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
 +            throw new IllegalStateException(reason);
 +        }
@@ -81,7 +82,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return Bukkit.isPrimaryThread();
 +    }
 +
-+    public static boolean isTickThreadFor(final int chunkX, final int chunkZ) {
++    public static boolean isTickThreadFor(final ServerLevel world, final int chunkX, final int chunkZ) {
 +        return Bukkit.isPrimaryThread();
 +    }
 +
diff --git a/patches/server/Only-write-chunk-data-to-disk-if-it-serializes-witho.patch b/patches/server/Only-write-chunk-data-to-disk-if-it-serializes-witho.patch
index 7e0c992d66..c1082a5a44 100644
--- a/patches/server/Only-write-chunk-data-to-disk-if-it-serializes-witho.patch
+++ b/patches/server/Only-write-chunk-data-to-disk-if-it-serializes-witho.patch
@@ -79,8 +79,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 -            }
 +            // Paper - move into try block to only write if successfully serialized
          }
- 
-             // Paper start
+         // Paper start
+         return;
 @@ -0,0 +0,0 @@ public class RegionFileStorage implements AutoCloseable {
          }
  
diff --git a/patches/server/Optimise-chunk-tick-iteration.patch b/patches/server/Optimise-chunk-tick-iteration.patch
index c7327f4d53..dc71d08e9a 100644
--- a/patches/server/Optimise-chunk-tick-iteration.patch
+++ b/patches/server/Optimise-chunk-tick-iteration.patch
@@ -10,9 +10,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 --- a/src/main/java/net/minecraft/server/level/ChunkHolder.java
 +++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java
 @@ -0,0 +0,0 @@ public class ChunkHolder {
-         long key = net.minecraft.server.MCUtil.getCoordinateKey(this.pos);
          this.playersInMobSpawnRange = this.chunkMap.playerMobSpawnMap.getObjectsInRange(key);
          this.playersInChunkTickRange = this.chunkMap.playerChunkTickRangeMap.getObjectsInRange(key);
+         // Paper end - optimise anyPlayerCloseEnoughForSpawning
 +        // Paper start - optimise chunk tick iteration
 +        if (this.needsBroadcastChanges()) {
 +            this.chunkMap.needsChangeBroadcasting.add(this);
@@ -20,17 +20,19 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        // Paper end - optimise chunk tick iteration
      }
  
-     void onChunkRemove() {
+     public void onChunkRemove() {
+@@ -0,0 +0,0 @@ public class ChunkHolder {
          this.playersInMobSpawnRange = null;
          this.playersInChunkTickRange = null;
+         // Paper end - optimise anyPlayerCloseEnoughForSpawning
 +        // Paper start - optimise chunk tick iteration
 +        if (this.needsBroadcastChanges()) {
 +            this.chunkMap.needsChangeBroadcasting.remove(this);
 +        }
 +        // Paper end - optimise chunk tick iteration
      }
-     // Paper end - optimise anyPlayerCloseEnoughForSpawning
-     long lastAutoSaveTime; // Paper - incremental autosave
+     // Paper end
+ 
 @@ -0,0 +0,0 @@ public class ChunkHolder {
  
              if (i < 0 || i >= this.changedBlocksPerSection.length) return; // CraftBukkit - SPIGOT-6086, SPIGOT-6296
@@ -158,13 +160,13 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
                  }
 +                // Paper start - optimise chunk tick iteration
 +                }
-             }
++            }
 +
 +            } finally {
 +                if (iterator1 instanceof io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet.Iterator safeIterator) {
 +                    safeIterator.finishedIterating();
 +                }
-+            }
+             }
 +            // Paper end - optimise chunk tick iteration
              this.level.timings.chunkTicks.stopTiming(); // Paper
              gameprofilerfiller.popPush("customSpawners");
diff --git a/patches/server/Optimise-nearby-player-lookups.patch b/patches/server/Optimise-nearby-player-lookups.patch
index c02f444590..a947575ea8 100644
--- a/patches/server/Optimise-nearby-player-lookups.patch
+++ b/patches/server/Optimise-nearby-player-lookups.patch
@@ -24,7 +24,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        // Paper end - optimise checkDespawn
      }
  
-     void onChunkRemove() {
+     public void onChunkRemove() {
 @@ -0,0 +0,0 @@ public class ChunkHolder {
              this.chunkMap.needsChangeBroadcasting.remove(this);
          }
@@ -36,8 +36,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +        // Paper end - optimise checkDespawn
      }
-     // Paper end - optimise anyPlayerCloseEnoughForSpawning
-     long lastAutoSaveTime; // Paper - incremental autosave
+     // Paper end
+ 
 diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
 --- a/src/main/java/net/minecraft/server/level/ChunkMap.java
diff --git a/patches/server/Optimize-anyPlayerCloseEnoughForSpawning-to-use-dist.patch b/patches/server/Optimize-anyPlayerCloseEnoughForSpawning-to-use-dist.patch
index fc174734ca..b75cccf5b4 100644
--- a/patches/server/Optimize-anyPlayerCloseEnoughForSpawning-to-use-dist.patch
+++ b/patches/server/Optimize-anyPlayerCloseEnoughForSpawning-to-use-dist.patch
@@ -10,37 +10,35 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 --- a/src/main/java/net/minecraft/server/level/ChunkHolder.java
 +++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java
 @@ -0,0 +0,0 @@ public class ChunkHolder {
-     boolean isUpdateQueued = false; // Paper
-     private final ChunkMap chunkMap; // Paper
+ 
+     // Paper start
+     public void onChunkAdd() {
+-
++        // Paper start - optimise anyPlayerCloseEnoughForSpawning
++        long key = net.minecraft.server.MCUtil.getCoordinateKey(this.pos);
++        this.playersInMobSpawnRange = this.chunkMap.playerMobSpawnMap.getObjectsInRange(key);
++        this.playersInChunkTickRange = this.chunkMap.playerChunkTickRangeMap.getObjectsInRange(key);
++        // Paper end - optimise anyPlayerCloseEnoughForSpawning
+     }
+ 
+     public void onChunkRemove() {
+-
++        // Paper start - optimise anyPlayerCloseEnoughForSpawning
++        this.playersInMobSpawnRange = null;
++        this.playersInChunkTickRange = null;
++        // Paper end - optimise anyPlayerCloseEnoughForSpawning
+     }
+     // Paper end
  
 +    // Paper start - optimise anyPlayerCloseEnoughForSpawning
 +    // cached here to avoid a map lookup
 +    com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> playersInMobSpawnRange;
 +    com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> playersInChunkTickRange;
-+
-+    void onChunkAdd() {
-+        long key = net.minecraft.server.MCUtil.getCoordinateKey(this.pos);
-+        this.playersInMobSpawnRange = this.chunkMap.playerMobSpawnMap.getObjectsInRange(key);
-+        this.playersInChunkTickRange = this.chunkMap.playerChunkTickRangeMap.getObjectsInRange(key);
-+    }
-+
-+    void onChunkRemove() {
-+        this.playersInMobSpawnRange = null;
-+        this.playersInChunkTickRange = null;
-+    }
 +    // Paper end - optimise anyPlayerCloseEnoughForSpawning
 +
      public ChunkHolder(ChunkPos pos, int level, LevelHeightAccessor world, LevelLightEngine lightingProvider, ChunkHolder.LevelChangeListener levelUpdateListener, ChunkHolder.PlayerProvider playersWatchingChunkProvider) {
          this.futures = new AtomicReferenceArray(ChunkHolder.CHUNK_STATUSES.size());
          this.fullChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
-@@ -0,0 +0,0 @@ public class ChunkHolder {
-         this.setTicketLevel(level);
-         this.changedBlocksPerSection = new ShortSet[world.getSectionsCount()];
-         this.chunkMap = (ChunkMap)playersWatchingChunkProvider; // Paper
-+        this.onChunkAdd(); // Paper - optimise anyPlayerCloseEnoughForSpawning
-     }
- 
-     // Paper start
 diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
 --- a/src/main/java/net/minecraft/server/level/ChunkMap.java
@@ -127,22 +125,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      }
  
      protected ChunkGenerator generator() {
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
-                 holder = (ChunkHolder) this.pendingUnloads.remove(pos);
-                 if (holder != null) {
-                     holder.setTicketLevel(level);
-+                    holder.onChunkAdd(); // Paper - optimise anyPlayerCloseEnoughForSpawning - PUT HERE AFTER RE-ADDING ONLY
-                 } else {
-                     holder = new ChunkHolder(new ChunkPos(pos), level, this.level, this.lightEngine, this.queueSorter, this);
-                     // Paper start
-@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
-             ChunkHolder playerchunk = (ChunkHolder) this.updatingChunkMap.remove(j);
- 
-             if (playerchunk != null) {
-+                playerchunk.onChunkRemove(); // Paper
-                 this.pendingUnloads.put(j, playerchunk);
-                 this.modified = true;
-                 ++i;
 @@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
          return this.anyPlayerCloseEnoughForSpawning(pos, false);
      }
@@ -243,8 +225,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 -        this.naturalSpawnChunkCounter.runAllUpdates();
 +        //this.f.a(); // Paper - no longer used
          this.tickingTicketsTracker.runAllUpdates();
+         org.spigotmc.AsyncCatcher.catchOp("DistanceManagerTick"); // Paper
          this.playerTicketManager.runAllUpdates();
-         int i = Integer.MAX_VALUE - this.ticketTracker.runDistanceUpdates(Integer.MAX_VALUE);
 @@ -0,0 +0,0 @@ public abstract class DistanceManager {
          ((ObjectSet) this.playersPerChunk.computeIfAbsent(i, (j) -> {
              return new ObjectOpenHashSet();
diff --git a/patches/server/Provide-E-TE-Chunk-count-stat-methods.patch b/patches/server/Provide-E-TE-Chunk-count-stat-methods.patch
index 9765786032..41970cac37 100644
--- a/patches/server/Provide-E-TE-Chunk-count-stat-methods.patch
+++ b/patches/server/Provide-E-TE-Chunk-count-stat-methods.patch
@@ -42,9 +42,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    @Override
 +    public int getTileEntityCount() {
 +        // We don't use the full world tile entity list, so we must iterate chunks
-+        Long2ObjectLinkedOpenHashMap<ChunkHolder> chunks = world.getChunkSource().chunkMap.visibleChunkMap;
 +        int size = 0;
-+        for (ChunkHolder playerchunk : chunks.values()) {
++        for (ChunkHolder playerchunk : net.minecraft.server.ChunkSystem.getVisibleChunkHolders(this.world)) {
 +            net.minecraft.world.level.chunk.LevelChunk chunk = playerchunk.getTickingChunk();
 +            if (chunk == null) {
 +                continue;
@@ -63,7 +62,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public int getChunkCount() {
 +        int ret = 0;
 +
-+        for (ChunkHolder chunkHolder : world.getChunkSource().chunkMap.visibleChunkMap.values()) {
++        for (ChunkHolder chunkHolder : net.minecraft.server.ChunkSystem.getVisibleChunkHolders(this.world)) {
 +            if (chunkHolder.getTickingChunk() != null) {
 +                ++ret;
 +            }
diff --git a/patches/server/Replace-player-chunk-loader-system.patch b/patches/server/Replace-player-chunk-loader-system.patch
index 72f644c801..5e95bcad62 100644
--- a/patches/server/Replace-player-chunk-loader-system.patch
+++ b/patches/server/Replace-player-chunk-loader-system.patch
@@ -1284,7 +1284,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            worldData.addProperty("tick-view-distance", world.getChunkSource().chunkMap.playerChunkManager.getTargetTickViewDistance()); // Paper - replace chunk loader system
              worldData.addProperty("keep-spawn-loaded", world.keepSpawnInMemory);
              worldData.addProperty("keep-spawn-loaded-range", world.paperConfig().spawn.keepSpawnLoadedRange * 16);
-             worldData.addProperty("visible-chunk-count", visibleChunks.size());
+             worldData.addProperty("visible-chunk-count", allChunks.size());
 diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
 --- a/src/main/java/net/minecraft/server/level/ChunkHolder.java
@@ -1305,8 +1305,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +    // Paper end - no-tick view distance
  
-     // Paper start - optimise anyPlayerCloseEnoughForSpawning
-     // cached here to avoid a map lookup
+     // Paper start
+     public void onChunkAdd() {
 @@ -0,0 +0,0 @@ public class ChunkHolder {
  
      public void blockChanged(BlockPos pos) {
@@ -1440,7 +1440,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
              this.viewDistance = j;
 -            this.distanceManager.updatePlayerTickets(this.viewDistance + 1);
--            Iterator objectiterator = this.updatingChunks.getVisibleValuesCopy().iterator(); // Paper
+-            Iterator objectiterator = net.minecraft.server.ChunkSystem.getUpdatingChunkHolders(this.level).iterator(); // Paper
 -
 -            while (objectiterator.hasNext()) {
 -                ChunkHolder playerchunk = (ChunkHolder) objectiterator.next();
@@ -1484,7 +1484,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
          CsvOutput csvwriter = CsvOutput.builder().addColumn("x").addColumn("z").addColumn("level").addColumn("in_memory").addColumn("status").addColumn("full_status").addColumn("accessible_ready").addColumn("ticking_ready").addColumn("entity_ticking_ready").addColumn("ticket").addColumn("spawning").addColumn("block_entity_count").addColumn("ticking_ticket").addColumn("ticking_level").addColumn("block_ticks").addColumn("fluid_ticks").build(writer);
 -        TickingTracker tickingtracker = this.distanceManager.tickingTracker();
 +        // Paper - replace loader system
-         ObjectBidirectionalIterator objectbidirectionaliterator = this.updatingChunks.getVisibleMap().clone().long2ObjectEntrySet().fastIterator(); // Paper
+         Iterator<ChunkHolder> objectbidirectionaliterator = net.minecraft.server.ChunkSystem.getVisibleChunkHolders(this.level).iterator(); // Paper
  
          while (objectbidirectionaliterator.hasNext()) {
 @@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
diff --git a/patches/server/Sanitise-RegionFileCache-and-make-configurable.patch b/patches/server/Sanitise-RegionFileCache-and-make-configurable.patch
index 6dc02058ac..5d38773204 100644
--- a/patches/server/Sanitise-RegionFileCache-and-make-configurable.patch
+++ b/patches/server/Sanitise-RegionFileCache-and-make-configurable.patch
@@ -15,7 +15,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 --- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
 +++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
 @@ -0,0 +0,0 @@ public class RegionFileStorage implements AutoCloseable {
-         if (regionfile != null) {
+             // Paper end
              return regionfile;
          } else {
 -            if (this.regionCache.size() >= 256) {
diff --git a/patches/server/Show-Paper-in-client-crashes-server-lists-and-Mojang.patch b/patches/server/Show-Paper-in-client-crashes-server-lists-and-Mojang.patch
index b37739d111..d686e70b0c 100644
--- a/patches/server/Show-Paper-in-client-crashes-server-lists-and-Mojang.patch
+++ b/patches/server/Show-Paper-in-client-crashes-server-lists-and-Mojang.patch
@@ -99,6 +99,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
                  log.log( Level.SEVERE, "------------------------------" );
 -                log.log( Level.SEVERE, "Server thread dump (Look for plugins here before reporting to Spigot!):" );
 +                log.log( Level.SEVERE, "Server thread dump (Look for plugins here before reporting to Paper!):" ); // Paper
+                 com.destroystokyo.paper.io.chunk.ChunkTaskManager.dumpAllChunkLoadInfo(); // Paper
                  WatchdogThread.dumpThread( ManagementFactory.getThreadMXBean().getThreadInfo( MinecraftServer.getServer().serverThread.getId(), Integer.MAX_VALUE ), log );
                  log.log( Level.SEVERE, "------------------------------" );
-                 //
diff --git a/patches/server/Warn-on-plugins-accessing-faraway-chunks.patch b/patches/server/Warn-on-plugins-accessing-faraway-chunks.patch
index 6c993d7417..1a6ec1921d 100644
--- a/patches/server/Warn-on-plugins-accessing-faraway-chunks.patch
+++ b/patches/server/Warn-on-plugins-accessing-faraway-chunks.patch
@@ -87,8 +87,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
          return this.world.getChunk(x >> 4, z >> 4).getHeight(CraftHeightMap.toNMS(heightMap), x, z);
      }
 @@ -0,0 +0,0 @@ public class CraftWorld extends CraftRegionAccessor implements World {
+     // Spigot end
      // Paper start
-     @Override
      public java.util.concurrent.CompletableFuture<Chunk> getChunkAtAsync(int x, int z, boolean gen, boolean urgent) {
 +        warnUnsafeChunk("getting a faraway chunk async", x, z); // Paper
          if (Bukkit.isPrimaryThread()) {
diff --git a/patches/server/implement-optional-per-player-mob-spawns.patch b/patches/server/implement-optional-per-player-mob-spawns.patch
index 8ee5fcdd0e..8f726f4f33 100644
--- a/patches/server/implement-optional-per-player-mob-spawns.patch
+++ b/patches/server/implement-optional-per-player-mob-spawns.patch
@@ -304,9 +304,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
      protected ChunkGenerator generator() {
 @@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
-         });
+         }
      }
- 
+     // Paper end
 +    // Paper start
 +    public void updatePlayerMobTypeMap(Entity entity) {
 +        if (!this.level.paperConfig().entities.spawning.perPlayerMobSpawns) {
@@ -331,10 +331,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return entityPlayer.mobCounts[mobCategory.ordinal()];
 +    }
 +    // Paper end
-+
+ 
      private static double euclideanDistanceSquared(ChunkPos pos, Entity entity) {
          double d0 = (double) SectionPos.sectionToBlockCoord(pos.x, 8);
-         double d1 = (double) SectionPos.sectionToBlockCoord(pos.z, 8);
 diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
 --- a/src/main/java/net/minecraft/server/level/DistanceManager.java
@@ -398,8 +397,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
          this.maxHealthCache = this.getMaxHealth();
 +        this.cachedSingleMobDistanceMap = new com.destroystokyo.paper.util.PooledHashSets.PooledObjectLinkedOpenHashSet<>(this); // Paper
      }
- 
-     // Yes, this doesn't match Vanilla, but it's the best we can do for now.
+     // Paper start - Chunk priority
+     public BlockPos getPointInFront(double inFront) {
 diff --git a/src/main/java/net/minecraft/world/level/NaturalSpawner.java b/src/main/java/net/minecraft/world/level/NaturalSpawner.java
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
 --- a/src/main/java/net/minecraft/world/level/NaturalSpawner.java
diff --git a/patches/server/incremental-chunk-and-player-saving.patch b/patches/server/incremental-chunk-and-player-saving.patch
index 3066c362bd..06c9d6f97a 100644
--- a/patches/server/incremental-chunk-and-player-saving.patch
+++ b/patches/server/incremental-chunk-and-player-saving.patch
@@ -57,8 +57,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 --- a/src/main/java/net/minecraft/server/level/ChunkHolder.java
 +++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java
 @@ -0,0 +0,0 @@ public class ChunkHolder {
-         this.playersInChunkTickRange = null;
-     }
+     com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> playersInMobSpawnRange;
+     com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> playersInChunkTickRange;
      // Paper end - optimise anyPlayerCloseEnoughForSpawning
 +    long lastAutoSaveTime; // Paper - incremental autosave
 +    long inactiveTimeStart; // Paper - incremental autosave
@@ -107,18 +107,17 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            }
 +        }
 +        // Paper end
-     }
- 
++    }
++
 +    // Paper start - incremental autosave
 +    public boolean setHasBeenLoaded() {
 +        this.wasAccessibleSinceLastSave = getFullChunkStatus(this.ticketLevel).isOrAfter(ChunkHolder.FullChunkStatus.BORDER);
 +        return this.wasAccessibleSinceLastSave;
-+    }
+     }
 +    // Paper end
-+
+ 
      public void replaceProtoChunk(ImposterProtoChunk chunk) {
          for (int i = 0; i < this.futures.length(); ++i) {
-             CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> completablefuture = (CompletableFuture) this.futures.get(i);
 diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
 --- a/src/main/java/net/minecraft/server/level/ChunkMap.java
@@ -194,13 +193,13 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    // Paper end
 +
      protected void saveAllChunks(boolean flush) {
-         if (flush) {
-             List<ChunkHolder> list = (List) this.visibleChunkMap.values().stream().filter(ChunkHolder::wasAccessibleSinceLastSave).peek(ChunkHolder::refreshAccessibility).collect(Collectors.toList());
+         // Paper start - do not overload I/O threads with too much work when saving
+         int[] saved = new int[1];
 @@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
          }
  
          int l = 0;
--        ObjectIterator objectiterator = this.visibleChunkMap.values().iterator();
+-        Iterator objectiterator = net.minecraft.server.ChunkSystem.getVisibleChunkHolders(this.level).iterator(); // Paper
 -
 -        while (l < 20 && shouldKeepTicking.getAsBoolean() && objectiterator.hasNext()) {
 -            if (this.saveChunkIfNeeded((ChunkHolder) objectiterator.next())) {