17 多线程基础¶
本章目标¶
- 理解进程与线程
- 掌握创建线程的两种方式
- 掌握线程安全与 synchronized
- 了解 volatile 与常见并发工具
17.1 什么是线程?¶
进程:正在运行的程序(如 Minecraft)。 线程:进程内的执行单元。一个进程可以有多个线程同时干活(如游戏主线程、渲染线程、网络线程)。
进程:Minecraft
├── 主线程(游戏逻辑:tick、实体更新)
├── 渲染线程(画面)
├── 网络线程(收发数据包)
└── 音频线程(声音)
Minecraft 开发铁律
Minecraft 是单线程主循环(主线程跑 tick)。模组代码默认都在主线程执行。不要在主线程里做耗时操作(会卡顿掉帧),也不要随便从其他线程碰游戏数据(会崩)。Forge 提供了 execute() 等机制把任务调度回主线程(第 23 章讲)。
17.2 创建线程:方式一 继承 Thread¶
public class DownloadThread extends Thread {
@Override
public void run() {
System.out.println("开始下载…");
try {
Thread.sleep(2000); // 模拟耗时 2 秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("下载完成");
}
}
// 使用
DownloadThread t = new DownloadThread();
t.start(); // 启动线程(会执行 run())
// t.run(); // ❌ 直接调 run 不是多线程,只是普通方法调用
17.3 创建线程:方式二 实现 Runnable(推荐)¶
Java 单继承,实现接口更灵活:
public class Task implements Runnable {
private final String name;
public Task(String name) { this.name = name; }
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(name + " 工作中… " + i);
}
}
}
// 使用
Thread t1 = new Thread(new Task("线程A"));
Thread t2 = new Thread(new Task("线程B"));
t1.start();
t2.start();
Lambda 创建线程(最简洁)¶
Thread t = new Thread(() -> {
System.out.println("线程里干活");
});
t.start();
17.4 线程常用方法¶
Thread.sleep(1000); // 当前线程睡 1 秒(受检异常要处理)
t.join(); // 等待 t 线程结束
t.isAlive(); // 是否还在运行
t.setName("worker-1"); // 起名字(方便排查问题)
Thread.currentThread(); // 获取当前线程
t.interrupt(); // 请求中断
17.5 线程安全与 synchronized¶
多个线程同时修改同一份数据 → 数据错乱(竞态条件)。
// 危险示例:两个线程同时给 count 加 1
public class Counter {
private int count = 0;
public void increment() {
count++; // 不是原子操作!可能丢更新
}
}
synchronized 加锁¶
public class Counter {
private int count = 0;
public synchronized void increment() { // 同一时刻只有一个线程能进
count++;
}
public synchronized int getCount() {
return count;
}
}
也可以锁代码块:
public void increment() {
synchronized (this) {
count++;
}
}
synchronized 的语义
保证:① 互斥(同一时间只有一个线程进入)② 可见性(修改对其他线程可见)。
17.6 volatile¶
volatile 保证可见性(一个线程修改,其他线程立刻能看到),但不保证原子性:
public class Flag {
private volatile boolean running = true; // 其他线程能及时看到修改
}
适合"一个线程写、其他线程读"的标志位场景。count++ 这种"读-改-写"还是需要 synchronized 或原子类。
17.7 原子类与并发工具¶
AtomicInteger 等(无锁、性能好)¶
import java.util.concurrent.atomic.AtomicInteger;
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // +1 并返回
count.get(); // 读取
ConcurrentHashMap(并发版 Map)¶
import java.util.concurrent.ConcurrentHashMap;
Map<String, Integer> stats = new ConcurrentHashMap<>();
// 多线程环境下安全使用
线程池(ExecutorService)¶
反复 new Thread 开销大,用线程池复用线程:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
ExecutorService pool = Executors.newFixedThreadPool(4); // 4 个线程的池
pool.submit(() -> System.out.println("任务1"));
pool.submit(() -> System.out.println("任务2"));
pool.shutdown(); // 用完关闭
模组里
模组一般不建议自己乱开线程池(生命周期管理麻烦)。Forge/NeoForge 有自己的调度机制,需要异步时优先用 Forge 提供的方案(第 23 章)。了解线程概念即可,实战中听框架的。
17.8 实战:模组场景(示意)¶
// 异步下载图片/资源(真实模组要用 Forge 的调度,这里展示思想)
new Thread(() -> {
byte[] data = downloadImage(url); // 耗时操作,别在主线程做
Minecraft.getInstance().execute(() -> { // 回到主线程操作游戏数据
setPlayerSkin(data);
});
}).start();
17.9 小练习¶
- 创建两个线程同时打印 1~10,观察输出顺序(乱序是正常的)
- 两个线程各给共享 count 加 10000 次,对比加 synchronized 前后的结果
- 用 AtomicInteger 实现同样的累加
- 用线程池提交 5 个任务
本章小结¶
- 线程 = 进程内的执行单元;Minecraft 主线程是游戏核心
- 创建线程:extends Thread 或 implements Runnable / Lambda
- 共享数据要 synchronized / 原子类
- volatile 管可见性不管原子性
- 实战中优先用框架(Forge)提供的调度,别乱开线程
下一章:18 文件与 IO