Skip to content

多线程与多任务 ​

Bukkit 的调度框架在 org.bukkit.scheduler 包下。掌握它能做延时任务、定时循环、异步执行重活——但线程安全是这里的核心红线。

核心:tick ​

Bukkit 调度以**游戏刻(tick)**为时间单位。理想情况 1 tick = 50ms,20 tick = 1 秒。受性能影响实际 tick 不一定精确(用 /tps 查每秒 tick 数)。

换算

  • 1 秒 ≈ 20 tick
  • 5 秒 ≈ 100 tick
  • 1 分钟 ≈ 1200 tick

包结构 ​

  • BukkitScheduler:调度器,负责创建/管理任务(类似线程池)
  • BukkitTask:单个任务的句柄,可取消、查 id
  • BukkitRunnable:抽象类,对 BukkitScheduler 的封装,用起来更简洁

两个入口 ​

入口一:Bukkit.getScheduler() ​

java
Bukkit.getScheduler().runTask(this, new Runnable() {
    @Override
    public void run() {
        // 逻辑
    }
});

入口二:BukkitRunnable(推荐) ​

java
new BukkitRunnable() {
    @Override
    public void run() {
        // 逻辑
    }
}.runTaskLater(this, 20L); // 20 tick 后执行一次

BukkitRunnable 更简洁,还能在 run() 内调 cancel() 自我取消。

四类调度方法 ​

方法时机线程
runTask(plugin, runnable)下一 tick主线程
runTaskAsynchronously(plugin, runnable)下一 tick异步线程
runTaskLater(plugin, runnable, delay)delay tick 后主线程
runTaskLaterAsynchronously(...)delay tick 后异步
runTaskTimer(plugin, runnable, delay, period)delay 后开始,每 period 重复主线程
runTaskTimerAsynchronously(...)同上异步

同步任务(主线程) ​

java
new BukkitRunnable() {
    @Override
    public void run() {
        Bukkit.broadcastMessage("5 秒到了!");
    }
}.runTaskLater(this, 100L); // 100 tick = 5 秒后

定时循环 ​

java
new BukkitRunnable() {
    int count = 0;
    @Override
    public void run() {
        count++;
        Bukkit.broadcastMessage("第 " + count + " 次广播");
        if (count >= 5) {
            cancel(); // 自我取消
        }
    }
}.runTaskTimer(this, 0L, 100L); // 立即开始,每 100 tick 一次

异步任务 ​

java
new BukkitRunnable() {
    @Override
    public void run() {
        // 耗时操作:查数据库、读大文件、HTTP 请求
        // ... 这里不能调大部分 Bukkit API!
    }
}.runTaskAsynchronously(this);

线程安全红线 ​

异步线程绝不能直接操作 Bukkit API

绝大部分 Bukkit API 不是线程安全的。在异步线程里调 player.sendMessage、改方块、生成实体、Bukkit.getPlayer 等,会导致数据竞争、 ConcurrentModification、甚至崩服。

异步线程只能做"纯计算/IO"(数据库、文件、网络)。做完后要把结果切回主线程再操作游戏:

java
new BukkitRunnable() {
    @Override
    public void run() {
        // 异步:查数据库
        int coins = queryDatabase(playerName);

        // 切回主线程操作游戏
        new BukkitRunnable() {
            @Override
            public void run() {
                Player p = Bukkit.getPlayer(playerName);
                if (p != null) p.sendMessage("你有 " + coins + " 金币");
            }
        }.runTask(plugin);
    }
}.runTaskAsynchronously(plugin);

注意事项 ​

  • 别用过时方法:scheduleAsyncDelayedTask 等已废弃,用上面的 runTask*
  • 同步任务别做重活:主线程卡 = 全服卡(TPS 掉)。耗时操作丢异步
  • 定时任务记得取消:BukkitTask 可 cancel(),或 BukkitRunnable 内 cancel()。不取消的任务会一直跑、泄漏
  • 插件禁用时清理:在 onDisable 里取消所有自己调度的任务,或用 Bukkit.getScheduler().cancelTasks(this)

何时用同步、何时用异步 ​

场景用
改方块、生成实体、发消息、调 Bukkit API同步(主线程)
查数据库、读写大文件、HTTP 请求、重计算异步,结果切回主线程
延时执行简单逻辑同步 runTaskLater
定时循环(每秒广播)同步 runTaskTimer

调度器是性能与并发的关键。下一篇自定义合成表。