Writing a multithreaded plugin
The Minecraft server runs game code primarily on a single thread. This is huge limitation as a singular thread can only be so fast.
Luckily, there are server cores that allow the game code to run on multiple threads. These include Folia and ShreddedPaper. However, a normal Bukkit plugin will not work on these server cores, and needs to be rewritten to have a multithread environment.
What is a thread?
Single threaded
A thread is what your code executes on. One thread can execute one piece of code at a time. This means if your plugin is single-threaded, only one part of the plugin will be executed at a given time.
flowchart LR
A(Method A)
B(Method B)
C(Method C)
D(Method D)
A --> B --> C --> D
Asynchronous calls
When you make an asynchronous call on your single-threaded plugin, a second thread is spawned to execute the call. This is good for when you want to execute slow I/O tasks without it blocking your primary thread.
With Bukkit, you can create an asynchronous call with Bukkit.getScheduler().runTaskAsynchronously
. Some events like AsyncPlayerPreLoginEvent
run asynchronous by default.
flowchart LR
A(Method A)
B(Method B)
C(Method C)
D(Method D)
Async(Asynchronous I/O call)
A --> B --> C --> D
A --> Async --> D
Multithreaded
When your plugin is multithreaded, it means that there is more than just a single thread executing your code. This results in many parts of your plugins being executed at the same time as eachother.
Issues will arise if you try to run a single-threaded plugin in a multithreaded environment, and these will be covered below.
flowchart LR
A(Method A)
B(Method B)
C(Method C)
D(Method D)
A --> B
C --> D
The APIs for multithreaded plugins
There are certain APIs that are specific to multithreaded plugins. These APIs exist in all recent versions of Paper, however if you want your plugin to be compatible with older versions and Spigot, check out MultiLib.
plugin.yml
Firstly, in your plugin.yml
, you will need to tell the server that your plugin is a multithreaded plugin.
Do that by adding folia-supported: true
:
name: MyPlugin
version: 1.0.0
main: com.exmaple.MyPlugin
api-version: 1.20
folia-supported: true
Accessing the world
Each thread is only able to access the region of the world that it is in charge of.
First, check if your thread is in charge of that region of world:
Location location = new Location(Bukkit.getWorld("world"), 0, 0, 0);
if (Bukkit.isOwnedByCurrentRegion(location)) {
// We are in charge of this location! Let's modify the block here
location.getBlock().setType(Material.AIR);
}
Otherwise, if we aren’t in charge of that region, we will need to schedule our code to run in that region:
// If we aren't in charge of that location we need to schedule it
Location location = new Location(Bukkit.getWorld("world"), 0, 0, 0);
Bukkit.getRegionScheduler().run(plugin, location, t -> {
// Now we are in charge of that location! Let's modify the block!
location.getBlock().setType(Material.AIR);
});
We can also do this for entities:
Entity entity = Bukkit.getWorld("world").getEntities().get(0);
if (Bukkit.isOwnedByCurrentRegion(entity)) {
// We are in charge of this entity! Let's remove it
entity.remove();
} else {
// We aren't in charge of that entity, let's schedule it
entity.getScheduler().run(plugin, t -> {
// Now we are in charge of that entity! Let's remove it!
entity.remove();
}, null);
}
Asynchronous calls
Asynchronous calls can be made with:
Bukkit.getAsyncScheduler().runNow(plugin, t -> {
// Async code goes here
});
Tips
During most events and commands, you will be on the thread of the player/entity/block that triggered the event or command.
Use teleportAsync
When teleporting entities or players, be sure to do this:
entity.teleportAsync(location);
Instead of this:
entity.teleport(location); // Do not do this
Java gotchas for multithreaded plugins
Race conditions
Consider the code below.
Player player;
void nextPlayer() {
Player nextPlayer = this.getTheNextPlayer(this.player);
nextPlayer.sendMessage("You are now the player!");
this.player = nextPlayer;
}
If you ran the method nextPlayer
twice in a single-threaded plugin, you would expect the following to occur:
player
isPlayerA
- Call 1 executes
this.getTheNextPlayer()
and gets the next playerPlayerB
- Call 1 sends
PlayerB
the message"You are now the player!"
- Call 1 saves
player
to bePlayerB
- Call 2 now executes
this.getTheNextPlayer()
and gets the next playerPlayerC
- Call 2 sends
PlayerC
the message"You are now the player!"
- Call 2 saves
player
to bePlayerC
This makes sense. However, if the method nextPlayer
is running on two threads at the same time, the following will occur:
player
isPlayerA
- Thread 1 executes
this.getTheNextPlayer()
and gets the next playerPlayerB
- Thread 2 also executes
this.getTheNextPlayer()
. Sinceplayer
is stillPlayerA
, the next player is stillPlayerB
- Thread 1 sends
PlayerB
the message"You are now the player!"
- Thread 2 also sends
PlayerB
the message"You are now the player!"
- Thread 1 saves
player
to bePlayerB
- Thread 2 also saves
player
to bePlayerB
This shows the issue of race conditions and why you need to try to avoid them
Synchronized statement
To avoid race conditions, you may need to make sure your code is only executing on one thread at a time. A synchronized
statement solves this by locking the given Java object. For example:
final Object lockObject = new Object();
Player player;
void nextPlayer() {
synchronized (this.lockObject) {
// This block of code will only run once at a time for any `this.lockObject`
Player nextPlayer = this.getTheNextPlayer(this.player);
nextPlayer.sendMessage("You are now the player!");
this.player = nextPlayer;
}
}
You can also lock the object that holds the method as so:
Player player;
synchronized void nextPlayer() {
// This block of code will only run once at a time for the object containing this method
Player nextPlayer = this.getTheNextPlayer(this.player);
nextPlayer.sendMessage("You are now the player!");
this.player = nextPlayer;
}
Data types
Common data types are typically not multithread safe. This means only one thread can safely access them at a time. For example:
// Not thread-safe data types:
List list = new ArrayList();
Queue queue = new LinkedList();
Map map = new HashMap();
Set set = new HashSet();
If you need multiple threads to be able to access the data types at the same time, consider these following thread-safe data types instead:
// Thread-safe data types
List frequentlyModifiedList = Collections.synchronizedList(new ArrayList());
List infrequentlyModifiedList = new CopyOnWriteArrayList();
Queue queue = new LinkedBlockingDeque();
Map map = new ConcurrentHashMap();
Set set = ConcurrentHashMap.newKeySet();