从单线程到多线程:Java Thread 的前世今生与状态流转
前言:为什么我们需要多线程?
想象一下,你是一个餐厅服务员,同时有三桌客人需要点餐、上菜、结账。如果你只能按顺序一桌一桌服务,那等待的客人肯定会抱怨"太慢了"。多线程就像雇佣了多个服务员,每个人负责自己的工作,效率瞬间提升。
但在享受高效的同时,你也需要面对新的问题:多个服务员会不会撞到一起?谁先服务谁?服务完怎么汇报?这些正是 Java 多线程要解决的核心问题。
今天,我们就来深入剖析 Java 线程的"一生"——从诞生到消亡的完整旅程,以及三种创建线程的方式,带你真正搞懂多线程的本质。
第一幕:线程的六种状态——一个线程的一生
Java 通过 Thread.State 枚举精确定义了线程的 6 种状态,每个状态都代表了线程在某个特定时刻的处境:
| 状态 | 含义 | 生活类比 |
|---|---|---|
| NEW | 线程对象已创建,但未启动 | 新员工入职报道,还没开始工作 |
| RUNNABLE | 线程正在运行或等待 CPU | 员工在工位上忙碌(或排队等电脑用) |
| BLOCKED | 线程因竞争锁被阻塞 | 员工想用打印机,但别人正在用,只能等 |
| WAITING | 线程进入无超时等待 | 员工在会议室等人开会,不设结束时间 |
| TIMED_WAITING | 线程进入有超时等待 | 员工设置了 30 分钟闹钟小憩 |
| TERMINATED | 线程执行完毕或异常终止 | 员工下班或离职了 |
状态流转全景图
plaintext
NEW → RUNNABLE → [BLOCKED / WAITING / TIMED_WAITING] → TERMINATED
从 NEW 到 TERMINATED,一个线程完整走完它的一生,期间可能在 BLOCKED、WAITING、TIMED_WAITING 之间反复切换——这就是线程的"职场生涯"。
第二幕:Thread 核心 API 五虎将
理解了状态流转,我们来看看那些控制线程命运的五个核心方法。
1️⃣ start() vs run() —— 启动 vs 执行
这是新手最容易混淆的两个方法:
run():只是 Thread 类的一个普通方法,直接调用不会启动新线程,相当于在当前线程"自己干活"。start():真正的"新线程启动器",JVM 会分配资源并调用run(),让线程进入 RUNNABLE 状态。
java
public class ThreadStartRunDemo {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("当前执行线程名:" + Thread.currentThread().getName());
System.out.println("线程状态:" + Thread.currentThread().getState());
}, "MyThread");
// 调用 start 前,线程只是个"新生儿"
System.out.println("调用start前,线程状态:" + thread.getState()); // NEW
// 直接调用 run():不会启动新线程,在 main 线程中执行
System.out.println("\n=== 直接调用run() ===");
thread.run(); // 输出:当前执行线程名:main
// 调用 start():真正启动新线程
System.out.println("\n=== 调用start() ===");
thread.start(); // 输出:当前执行线程名:MyThread,状态:RUNNABLE
// 等待线程执行完毕
Thread.sleep(100);
System.out.println("线程执行完毕后状态:" + thread.getState()); // TERMINATED
}
}
2️⃣ join() —— 我要等你做完
join() 让当前线程"暂停等待",直到目标线程执行完毕。就像老板说:"我先等你做完这个项目,再安排下一个任务。"
java
public class ThreadJoinDemo {
public static void main(String[] args) throws InterruptedException {
Thread taskThread = new Thread(() -> {
for (int i = 0; i < 3; i++) {
try {
Thread.sleep(500); // 模拟耗时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("子线程执行中:" + i);
}
}, "TaskThread");
taskThread.start();
System.out.println("main线程等待子线程执行...");
taskThread.join(); // main 线程进入 WAITING 状态,等 taskThread 执行完
System.out.println("子线程执行完毕,main线程继续执行");
}
}
输出结果:
plaintext
main线程等待子线程执行...
子线程执行中:0
子线程执行中:1
子线程执行中:2
子线程执行完毕,main线程继续执行
3️⃣ interrupt() —— 礼貌地请求中断
注意!interrupt() 不是"强制杀掉线程",而是"贴个标签说请停止":
- 给线程设置中断标记(
isInterrupted()返回 true); - 如果线程处于阻塞状态(sleep、join、wait),会抛出
InterruptedException并清除标记; - 线程需要自己检测标记,决定是否停止——这是"优雅中断"的精髓。
4️⃣ isAlive() —— 你还在吗?
isAlive() 返回线程是否"活着"(非 NEW 且非 TERMINATED)。
java
public class ThreadInterruptIsAliveDemo {
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
// 循环检测中断标记,决定是否退出
while (!Thread.currentThread().isInterrupted()) {
System.out.println("工作线程运行中...");
try {
Thread.sleep(1000); // 进入 TIMED_WAITING
} catch (InterruptedException e) {
System.out.println("工作线程被中断,清除中断标记");
// 抛出异常后中断标记会被清除,需要手动再次中断
Thread.currentThread().interrupt();
}
}
System.out.println("工作线程退出,中断标记:" + Thread.currentThread().isInterrupted());
}, "WorkerThread");
// 启动前,isAlive() = false
System.out.println("启动前,isAlive:" + worker.isAlive()); // false
worker.start();
System.out.println("启动后,isAlive:" + worker.isAlive()); // true
// 主线程等待 3 秒后中断工作线程
Thread.sleep(3000);
worker.interrupt();
// 等待工作线程终止
worker.join();
System.out.println("终止后,isAlive:" + worker.isAlive()); // false
}
}
5️⃣ 完整状态流转示例
下面这个例子展示了线程从 NEW → RUNNABLE → TIMED_WAITING → TERMINATED 的完整旅程:
java
public class ThreadStateDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
try {
// 线程运行中,状态为 RUNNABLE
System.out.println("线程执行中,状态:" + Thread.currentThread().getState());
// 调用 sleep,进入 TIMED_WAITING
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 执行完毕,进入 TERMINATED
}, "StateDemoThread");
// 1. NEW 状态:创建但未启动
System.out.println("1. 初始状态:" + thread.getState()); // NEW
// 2. 调用 start(),进入 RUNNABLE
thread.start();
Thread.sleep(100); // 等线程启动
System.out.println("2. 启动后状态:" + thread.getState()); // RUNNABLE/TIMED_WAITING
// 3. 线程 sleep 中,状态为 TIMED_WAITING
Thread.sleep(500);
System.out.println("3. sleep中状态:" + thread.getState()); // TIMED_WAITING
// 4. 等待线程执行完,状态为 TERMINATED
thread.join();
System.out.println("4. 执行完毕状态:" + thread.getState()); // TERMINATED
}
}
第三幕:三种创建线程的方式——你适合哪一种?
先明确一个核心误区
实现 Runnable/Callable 本身并不是"创建线程",而是定义"线程要执行的任务";真正创建并启动线程的始终是 Thread 类。
方式一:继承 Thread 类(不推荐)
通过继承 Thread 类,重写 run() 方法。这种方式简单直观,但缺点明显:由于 Java 单继承限制,继承 Thread 后无法再继承其他类,线程和任务高度耦合。
java
class MyThread extends Thread {
@Override
public void run() {
System.out.println("继承Thread的线程执行:" + Thread.currentThread().getName());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class ThreadExtendDemo {
public static void main(String[] args) {
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();
thread1.start();
thread2.start();
}
}
方式二:实现 Runnable 接口(推荐)
Runnable 是函数式接口,定义任务逻辑后,传入 Thread 构造器来创建线程。任务和线程解耦,多个线程可以共享同一个任务。
java
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("实现Runnable的线程执行:" + Thread.currentThread().getName());
}
}
public class RunnableImplDemo {
public static void main(String[] args) {
MyRunnable task = new MyRunnable();
Thread thread1 = new Thread(task, "Runnable线程1");
Thread thread2 = new Thread(task, "Runnable线程2");
thread1.start();
thread2.start();
// Lambda 简化写法
Thread thread3 = new Thread(() -> {
System.out.println("Lambda简化Runnable:" + Thread.currentThread().getName());
}, "Lambda线程");
thread3.start();
}
}
共享任务示例(多线程卖票):
java
Runnable ticketTask = () -> {
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + "卖出第" + i + "张票");
}
};
// 3 个线程卖同一批票
new Thread(ticketTask, "窗口1").start();
new Thread(ticketTask, "窗口2").start();
new Thread(ticketTask, "窗口3").start();
方式三:实现 Callable 接口(需要返回值时)
Callable 是 JDK 1.5 新增的接口,解决了 Runnable 无返回值、不能抛检查异常的问题,需要配合 FutureTask 来获取返回值。
java
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
class MyCallable implements Callable<Integer> {
private int num;
public MyCallable(int num) {
this.num = num;
}
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= num; i++) {
sum += i;
Thread.sleep(10); // 可直接 throws,无需 try-catch
}
return sum;
}
}
public class CallableImplDemo {
public static void main(String[] args) {
Callable<Integer> callableTask = new MyCallable(100);
FutureTask<Integer> futureTask = new FutureTask<>(callableTask);
Thread thread = new Thread(futureTask, "Callable线程");
thread.start();
try {
Integer result = futureTask.get(); // 阻塞等待结果
System.out.println("1-100的和:" + result); // 输出 5050
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
三种方式对比表
| 特性 | 继承 Thread 类 | 实现 Runnable 接口 | 实现 Callable 接口 |
|---|---|---|---|
| 核心方法 | run()(无返回值) | run()(无返回值) | call()(有返回值) |
| 异常处理 | 只能 try-catch | 只能 try-catch | 可 throws 检查异常 |
| 线程-任务耦合度 | 高(线程 = 任务) | 低(任务独立) | 低(任务独立) |
| 继承限制 | 不能继承其他类 | 可继承其他类 | 可继承其他类 |
| 返回值 | 无 | 无 | 有(通过 FutureTask) |
| 适用场景 | 简单任务、无需复用 | 任务复用、多线程共享 | 需要返回结果 / 抛异常 |
总结:选择策略
- 简单无返回值:优先用
Runnable(配合 Lambda 简化代码); - 需要返回值 / 抛异常:用
Callable + FutureTask; - 尽量避免继承 Thread(单继承限制 + 耦合度高);
- 实际开发:很少直接
new Thread,而是用ExecutorService线程池管理 Runnable/Callable 任务,更高效。
进阶思考
当你掌握了线程的基础知识和创建方式后,下一步应该思考:
- 线程安全:多个线程共享数据时,如何避免数据竞争?(synchronized、Lock、volatile)
- 线程池:频繁创建销毁线程开销大,如何复用线程?(ExecutorService、ThreadPoolExecutor)
- 并发工具:JUC 包提供了哪些强大的并发工具?(CountDownLatch、CyclicBarrier、Semaphore)
多线程的世界博大精深,今天我们只是揭开了序幕。接下来,让我们一起继续探索并发编程的更多奥秘吧!
推荐阅读:
- 《Java 并发编程实战》—— 并发领域的圣经
- JDK 源码:
java.lang.Thread、java.util.concurrent包
互动话题:你在项目中遇到过哪些多线程相关的坑?欢迎在评论区分享你的经历!