Minestom’s threading model is one of its most powerful features. Unlike traditional Minecraft servers that run everything on a single thread, Minestom distributes the workload across multiple threads for better performance.
Minestom can tick multiple chunks and their entities in parallel, dramatically improving performance on multi-core systems.
// Global operations that must run after all chunks tick- Process scheduled tasks- Update world border transitions- Synchronize time across instances- Dispatch tick events
The dispatcher processes several types of updates:
ThreadDispatcher.java:154-188
sealed interface Update<P, E> { // Register a new partition (chunk) record PartitionLoad<P, E>(P partition) implements Update<P, E> {} // Delete an existing partition record PartitionUnload<P, E>(P partition) implements Update<P, E> {} // Update an element (entity moved to different chunk) record ElementUpdate<P, E>(E element, P partition) implements Update<P, E> {} // Remove an element (entity removed) record ElementRemove<P, E>(E element) implements Update<P, E> {}}
// When a chunk is loadeddispatcher.createPartition(chunk);// When a chunk is unloadeddispatcher.deletePartition(chunk);// When an entity moves to a different chunkdispatcher.updateElement(entity, newChunk);// When an entity is removeddispatcher.removeElement(entity);
Minestom automatically handles these updates when you use standard instance methods. Manual signaling is only needed for custom implementations.
// Safe - in an event handlereventNode.addListener(PlayerMoveEvent.class, event -> { event.getPlayer().sendMessage("You moved!");});// Safe - using schedulerplayer.scheduler().scheduleNextTick(() -> { player.setHealth(20.0f);});// Unsafe - direct modification from another thread// DON'T DO THISnew Thread(() -> { player.setHealth(20.0f); // WRONG!}).start();
// Acquire exclusive access to an entityAcquirable<Entity> acquirable = entity.acquirable();acquirable.sync(e -> { // This block has exclusive access to the entity e.setVelocity(new Vec(0, 10, 0));});// Or asynchronouslyacquirable.async(e -> { e.setCustomName(Component.text("Updated"));}).thenRun(() -> { System.out.println("Entity updated!");});
The Acquirable API ensures thread-safe access by guaranteeing only one thread can access the entity at a time.
SchedulerManager scheduler = MinecraftServer.getSchedulerManager();// Run once after delayscheduler.buildTask(() -> { System.out.println("5 seconds later...");}).delay(5, TimeUnit.SECOND).schedule();// Run repeatedlyscheduler.buildTask(() -> { System.out.println("Every second");}).repeat(1, TimeUnit.SECOND).schedule();// Run on shutdownscheduler.buildShutdownTask(() -> { System.out.println("Server stopping...");});
// WRONG - Don't do I/O during ticksinstance.tick(time -> { File file = new File("data.txt"); String data = Files.readString(file.toPath()); // Blocks the tick thread!});// RIGHT - Do I/O asynchronouslyCompletableFuture.supplyAsync(() -> { return Files.readString(Path.of("data.txt"));}).thenAccept(data -> { // Use the data});
For advanced users, you can create custom thread providers:
public class RegionThreadProvider implements ThreadProvider<Chunk> { private final int threadCount; private final int regionSize; public RegionThreadProvider(int threadCount, int regionSize) { this.threadCount = threadCount; this.regionSize = regionSize; } @Override public int findThread(Chunk chunk) { // Group chunks into regions int regionX = chunk.getChunkX() / regionSize; int regionZ = chunk.getChunkZ() / regionSize; // Assign regions to threads int hash = Objects.hash(regionX, regionZ); return Math.abs(hash % threadCount); }}// Use itThreadDispatcher<Chunk, Entity> dispatcher = ThreadDispatcher.dispatcher( new RegionThreadProvider(4, 8), 4);
This can improve cache locality by keeping nearby chunks on the same thread.