14.1 线程创建与生命周期
学过方法、对象、Lambda 和异常后,用约 50 分钟跑完本篇 Java 17 平台线程实验。你会启动、等待和协作取消线程,也能解释六种
Thread.State各自等待的是什么。
一个人处理两个窗口
这里适合把两项工作表示为独立任务。先别急着讨论共享库存;本页只回答:任务怎样交给线程,调用者怎样知道线程完成,以及程序怎样请求线程停止。
1. Runnable 是任务,Thread 是执行者
Runnable printReceipt = () ->
System.out.println("打印回执:" + Thread.currentThread().getName());
Thread worker = new Thread(printReceipt, "receipt-worker");
worker.start();Runnable 的单一抽象方法是 void run()。它只描述工作,不拥有线程。
还可以继承 Thread 并覆盖 run():
final class CatalogThread extends Thread {
CatalogThread() {
super("catalog-worker");
}
@Override
public void run() {
System.out.println("查询目录");
}
}这种写法能运行,但把任务和线程生命周期绑定在一起,也占用了 Java 的单继承位置。多数业务代码更适合将 Runnable 交给 Thread;需要 Callable 返回结果时,使用下一专题的执行器。
2. run() 与 start() 是两件事
调用 run() 只是普通方法调用,仍由当前线程执行。调用 start() 才请求 JVM 建立新的执行路径,并由新线程调用 run()。
下面是完整、输出稳定的实验:
public class ThreadLifecycleDemo {
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(
() -> System.out.println("任务线程:" + Thread.currentThread().getName()),
"library-worker");
worker.run();
worker.start();
worker.join();
if (worker.isAlive() || worker.getState() != Thread.State.TERMINATED) {
throw new AssertionError("join 返回后线程应已终止");
}
System.out.println("线程检查通过");
}
}运行:
chapter14_thread_root=$(mktemp -d)
mkdir -p "$chapter14_thread_root/out"
# 将上面的代码保存为 "$chapter14_thread_root/ThreadLifecycleDemo.java"
javac --release 17 -Xlint:all -Werror \
-d "$chapter14_thread_root/out" \
"$chapter14_thread_root/ThreadLifecycleDemo.java"
java -ea -cp "$chapter14_thread_root/out" ThreadLifecycleDemo
rm -rf -- "$chapter14_thread_root"输出:
任务线程:main
任务线程:library-worker
线程检查通过实验先直接调用 worker.run(),再启动工作线程,最后用 join() 等待它结束,所以三行顺序是确定的。多个已经启动的线程之间通常没有这种顺序保证。
3. 一个 Thread 只能启动一次
线程从未启动状态走到终止状态后,不能倒回去:
Thread worker = new Thread(() -> { });
worker.start();
worker.join();
worker.start(); // IllegalThreadStateException异常不是因为第一次任务失败,而是因为线程对象的生命周期是一次性的。重复工作应该创建新线程,或复用线程池中的工作线程,而不是复用 Thread 对象。
4. 六种 Java 线程状态
NEW --start()--> RUNNABLE -----------------------> TERMINATED
| ^
| | 获得监视器/条件满足/超时
v |
BLOCKED / WAITING / TIMED_WAITING| 状态 | 典型原因 | 关键区别 |
|---|---|---|
NEW | new Thread(...) 后、start() 前 | 尚未启动 |
RUNNABLE | 可运行或正在执行 | Java 不另设 RUNNING;等待某些操作系统资源时也可能显示此状态 |
BLOCKED | 等待进入 synchronized 区域 | 等的是监视器锁 |
WAITING | wait()、join()、park() 无超时等待 | 等另一个动作,无截止时间 |
TIMED_WAITING | sleep()、带超时的 wait()/join() | 有截止时间 |
TERMINATED | run() 返回或因未捕获异常退出 | 不能再次启动 |
状态快照不能用来协调
Thread.State observed = worker.getState();这行读到的是瞬时状态。下一行执行前,线程可能已经切换状态。因此,不要写“等到 getState() == WAITING 再修改数据”之类协议,也不要用固定毫秒数的 sleep() 猜另一个线程已经走到哪一步。
需要确定先后关系时,使用 join()、CountDownLatch、BlockingQueue、Phaser 或其他明确的协作工具。状态快照适合日志、监控和线程转储分析。
5. join() 建立完成关系
final int[] result = new int[1];
Thread worker = new Thread(() -> result[0] = 42);
worker.start();
worker.join();
System.out.println(result[0]);这里先启动线程,再用无超时的 join() 确认终止;工作线程的动作 happens-before 调用者随后执行的动作,所以这里能看到 42。如果去掉 join(),主线程既不知道赋值何时发生,也不应直接依赖该结果。
对尚未启动的线程调用 join() 会立即返回,不能替代启动后的等待。join() 自身可被中断。调用者若不想无限等,可以使用 join(timeout),但超时返回后必须再检查 worker.isAlive();超时不等于线程已经结束。
6. 命名、守护线程与未捕获异常
给线程命名能直接改善日志和线程转储:
Thread worker = new Thread(task, "library-catalog-1");
worker.setUncaughtExceptionHandler((thread, error) ->
System.err.println(thread.getName() + " 失败:" + error));线程分为用户线程和守护线程。JVM 中只剩守护线程时,可以退出,不会等待守护线程完成。
worker.setDaemon(true); // 必须在 start() 前调用守护线程不是可靠的资源清理机制:JVM 退出时可能来不及刷新缓冲区或完成写入。必须完成的数据应由显式生命周期、关闭信号和 join()/执行器关闭来管理。
未捕获异常会终止当前线程。普通 Thread 可设置 UncaughtExceptionHandler;在线程池里,execute() 与 submit() 的异常路径不同,详见线程池专题。
7. 中断:请尽快停下
Java 17 已弃用 Thread.stop():强行终止可能破坏对象不变量。本篇使用中断表达协作取消。
worker.interrupt();这只是发出请求。任务要在合适位置检查:
while (!Thread.currentThread().isInterrupted()) {
processNextBook();
}如果线程正阻塞在 sleep()、wait()、join() 或许多并发工具的可中断方法中,它们会抛 InterruptedException。抛出时中断状态会被清除。
实验:恢复中断状态
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
public class InterruptionDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch ready = new CountDownLatch(1);
AtomicBoolean restored = new AtomicBoolean();
CountDownLatch neverReleased = new CountDownLatch(1);
Thread worker = new Thread(() -> {
try {
ready.countDown();
neverReleased.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
restored.set(Thread.currentThread().isInterrupted());
}
}, "interruptible-worker");
worker.start();
ready.await();
worker.interrupt();
worker.join();
System.out.println("中断已恢复:" + restored.get());
if (!restored.get()) {
throw new AssertionError("中断状态没有恢复");
}
System.out.println("中断检查通过");
}
}输出:
中断已恢复:true
中断检查通过ready 让主线程知道任务已准备好;neverReleased 永不倒数,只能被中断解除等待。即使中断早于 await() 到达,等待也会抛出异常,因此不用猜调度时间。捕获块无法向上抛受检异常,于是恢复中断状态,让更外层的任务框架仍能观察到取消。
如果你的方法签名允许,直接传播更清楚:
void awaitReceipt(Thread worker) throws InterruptedException {
worker.join();
}8. 线程安全从哪里开始
线程本身并不自动制造问题。危险来自可变共享状态:
- 局部变量本身属于当前调用栈,但它引用的对象仍可能被多个线程共享;
- 创建后不变的对象更容易安全共享;
- 多个线程读同一份不可变数据通常无需加锁;
- 多个线程读写同一库存、余额或集合时,必须定义同步规则。
线程启动也有一条发布保证:调用线程在 start() 前的动作 happens-before 新线程中的动作。因此,在启动前完整构造好任务所需数据,比启动后再无同步地修改字段更容易推理。
9. 常见故障
| 现象 | 原因与处理 |
|---|---|
调 run() 后线程名仍是 main | run() 是普通调用;使用 start() |
第二次 start() 失败 | Thread 一次性;创建新对象或使用执行器 |
用 sleep() 后测试仍偶发失败 | 时间延迟不是完成关系;改用明确协作工具 |
| 捕获中断后服务关不掉 | 中断被吞;传播或恢复状态 |
| JVM 在主方法结束后仍不退出 | 仍有用户线程存活或线程池未关闭 |
| 守护线程的文件只写了一半 | 守护线程不保证收尾;显式停止并等待 |
10. 练习
- 创建名为
catalog-worker和receipt-worker的两个线程。不要假定它们的输出顺序;用两个join()等待全部结束。 - 运行“双重
start()”失败示例,记录异常类型。解释为什么调用两次run()不会触发同一异常。 - 给一个每次处理 100 本书的循环增加中断检查。比较“传播
InterruptedException”与“捕获并恢复状态”的适用位置。 - 用
CountDownLatch代替sleep(),确认两个线程准备好后统一放行;调度器仍决定各自何时实际执行。
离开两个窗口前,再跑四次协作
先用线程名证明直接调用 run() 仍在 main 中,而 start() 会进入新的执行路径。等待两个工作线程时分别调用 join(),只断言它们都已完成,不断言彼此的输出顺序。
随后让同一个 Thread 启动两次,确认第二次抛 IllegalThreadStateException;再用 CountDownLatch 代替固定休眠,让主线程确定工作线程已经准备好后才发送中断。捕获 InterruptedException 时,要么继续传播,要么恢复中断并尽快结束当前任务。
线程状态只用于观察。若一段业务代码需要反复读取 getState() 才知道下一步做什么,通常意味着缺少真正的完成、消息或条件协议。
查阅 Java 17 的正式契约
Thread:启动、等待、中断、守护属性与未捕获异常。Thread.State:六种 JVM 线程状态。- JLS 17.4.5:happens-before order:
start、join、锁和 volatile 的可见性关系。 CountDownLatch:用事件替代时间猜测。
下一步:保护共同规则
线程是一段一次性的生命周期:start() 启动另一条执行路径,join() 建立完成关系,中断提出取消请求。下一篇进入真正困难的部分,多个线程同时修改最后一本书的库存时,怎样让业务规则始终成立。