跳转至

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. 创建两个线程同时打印 1~10,观察输出顺序(乱序是正常的)
  2. 两个线程各给共享 count 加 10000 次,对比加 synchronized 前后的结果
  3. 用 AtomicInteger 实现同样的累加
  4. 用线程池提交 5 个任务

本章小结

  • 线程 = 进程内的执行单元;Minecraft 主线程是游戏核心
  • 创建线程:extends Thread 或 implements Runnable / Lambda
  • 共享数据要 synchronized / 原子类
  • volatile 管可见性不管原子性
  • 实战中优先用框架(Forge)提供的调度,别乱开线程

下一章:18 文件与 IO

🏠 返回总站