线程基础
上一章我们搭好了并发的世界观——进程、线程、并发、并行。这一章开始动手:怎么在 Java 里真正”造一个线程出来”。
你可能会觉得 new Thread().start() 能有多难?但真实情况是:很多人写了 5 年 Java 都没搞清楚 start() 和 run() 的区别、不知道 interrupt 不是强制停止、不理解为什么 join 能让线程”排队”。这些细节决定了你写的是”能跑的并发代码”还是”会出事的并发代码”。
一、创建线程的三种方式
Java 给了我们三种”开线程”的方法,从简单到强大依次递进。
1.1 方式一:继承 Thread 类
最直接的方式——继承 Thread,重写 run() 方法。
class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程 " + Thread.currentThread().getName() + " 在跑");
}
}
MyThread t = new MyThread();
t.start(); // 不要调 run(),要调 start()!
关键点:
start()会创建新的 OS 线程并调用 run();直接调run()只是普通方法调用,不会开新线程——这是新手最常犯的错。- Java 单继承,继承了
Thread就不能继承别的类——这种方式不够灵活,实际项目里不推荐。
1.2 方式二:实现 Runnable 接口
更推荐的方式——实现 Runnable,把它丢给 Thread 执行。
class MyTask implements Runnable {
@Override
public void run() {
System.out.println("任务跑在 " + Thread.currentThread().getName());
}
}
Thread t = new Thread(new MyTask(), "worker-1");
t.start();
// 或者用 Lambda(Runnable 是函数式接口)
new Thread(() -> System.out.println("Lambda 也在 " + Thread.currentThread().getName())).start();
优势:
Runnable是接口,可以同时继承别的类。- 任务(
Runnable)和执行(Thread)解耦——同一个Runnable可以丢给线程池、虚拟线程、ForkJoinPool。 Runnable是函数式接口,可以用 Lambda。
Thread 本身也实现了 Runnable——
Thread类内部有个private Runnable target;字段,Thread.run()默认实现就是if (target != null) target.run();。所以继承Thread重写run,本质是覆盖了”调用 target”的默认行为。
1.3 方式三:实现 Callable + Future
Runnable 的 run() 返回 void——任务跑完拿不到结果。如果你需要”开线程算个值,最后取回来”,用 Callable。
import java.util.concurrent.*;
Callable<Integer> task = () -> {
Thread.sleep(1000);
return 42;
};
FutureTask<Integer> future = new FutureTask<>(task);
new Thread(future).start();
Integer result = future.get(); // 阻塞直到任务完成
System.out.println("结果是 " + result);
关键点:
Callable<V>的call()方法有返回值,且能抛受检异常(Runnable.run()不行)。FutureTask是Future和Runnable的桥梁——它可以丢给Thread或线程池执行。future.get()会阻塞直到任务完成——这是Future的局限,第 44 章的CompletableFuture解决了这个问题。
实际项目里 Callable 几乎总是配合 ExecutorService.submit() 使用——直接 new Thread 跑 FutureTask 的场景很少。
1.4 三种方式对比
| 方式 | 返回值 | 异常 | 推荐度 |
|---|---|---|---|
| 继承 Thread | 无 | 不能抛 | 不推荐(单继承局限) |
| 实现 Runnable | 无 | 不能抛 | 推荐(简单任务) |
| 实现 Callable | 有 | 能抛 | 推荐(需要结果时) |
二、线程的生命周期:六种状态
一个线程从生到死,要经历若干状态。Java 把它们定义在 Thread.State 枚举里——共 6 种。
┌──────────────────────────────────────────┐
│ ▼
┌─NEW───→ RUNNABLE ──┬──→ BLOCKED ──┬──→ RUNNABLE
│ │ │
│ ├──→ WAITING ──┤
│ │ │
│ └──→ TIMED_WAITING ──┘
│
└─────────────────────────────────────────────→ TERMINATED
2.1 六种状态详解
| 状态 | 含义 | 进入方式 |
|---|---|---|
| NEW | 已创建但未 start | new Thread() |
| RUNNABLE | 可运行(可能在跑也可能在等 CPU) | start() |
| BLOCKED | 等待 monitor 锁(synchronized) | 进入 synchronized 块时锁被占 |
| WAITING | 无限期等待被唤醒 | wait() / join() / LockSupport.park() |
| TIMED_WAITING | 限时等待 | sleep(ms) / wait(ms) / join(ms) / parkNanos |
| TERMINATED | 线程执行结束 | run() 正常返回或抛未捕获异常 |
注意:Java 的
RUNNABLE把”正在跑”和”等待 CPU 时间片”合并了——操作系统层面这两者叫 Running 和 Ready,但 Java 不区分。也就是说,一个 Java 线程在RUNNABLE状态下可能在跑,也可能在排队等 CPU。
2.2 BLOCKED vs WAITING 的区别
新手最容易混淆这两个状态——都是”等着”,但等待的东西不同:
- BLOCKED:等的是 synchronized 锁。线程想进入 synchronized 块/方法,但锁被别的线程持有,于是被放进”锁池”等待。当锁释放时,JVM 会从锁池里挑一个 BLOCKED 线程唤醒。
- WAITING:等的是某个事件/通知。比如调用了
obj.wait()(等其他线程obj.notify())、thread.join()(等目标线程结束)、LockSupport.park()(等 unpark)。
重要:ReentrantLock.lock() 让线程进入的是 WAITING(更准确说是 park),不是 BLOCKED!只有 synchronized 才会让线程进入 BLOCKED 状态。这是 JUC 锁和 synchronized 在状态机上的细微差别。
三、线程的核心方法
3.1 start() vs run()
start() → JVM 创建新线程 → 新线程里调用 run()
run() → 普通方法调用,在当前线程里跑,不开新线程
无数 bug 的源头就是把 start() 写成了 run()——代码”看起来跑了”,但其实是单线程的。
3.2 sleep(ms)——睡眠
Thread.sleep(ms) 让当前线程睡眠指定毫秒,不释放锁。
Thread.sleep(1000); // 睡 1 秒
TimeUnit.SECONDS.sleep(1); // 更可读的写法
sleep 期间线程进入 TIMED_WAITING。如果中途被 interrupt,会抛 InterruptedException——所以 sleep 是可中断的。
3.3 join()——等待另一个线程结束
thread.join() 让当前线程阻塞,直到 thread 结束。
Thread t = new Thread(() -> { /* ... */ });
t.start();
t.join(); // 等 t 结束才继续
t.join(1000); // 最多等 1 秒,超时继续
join 的本质是当前线程调用了 t.wait()——所以 join 会让当前线程进入 WAITING,并且会响应中断。
3.4 yield()——礼让
Thread.yield() 是个”hint”——告诉调度器”我愿意让出 CPU”。但调度器完全可以无视——你可能 yield 之后立刻又拿到 CPU。
Thread.yield(); // 礼让,但不保证真的让
实战中 yield 几乎不用——它没有可靠的语义。需要”等待某条件成立”应该用 wait/notify 或 Condition,而不是 yield 死循环。
3.5 interrupt()——中断
Java 没有”强制停止线程”的安全方法——Thread.stop() 早就废弃了(强制停止可能让对象处于不一致状态)。中断是 Java 提供的”协作式停止”机制。
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
// 干活
}
});
t.start();
t.interrupt(); // 设置中断标志
关键点:
interrupt()只是设置中断标志,不强制停止。- 线程应该主动检查
Thread.currentThread().isInterrupted()并优雅退出。 - 如果线程在
sleep/wait/join时被 interrupt,会抛InterruptedException,同时清除中断标志。 - 捕获
InterruptedException后通常要么重新设置中断标志(Thread.currentThread().interrupt()),要么向上抛——不要吞掉异常。
3.6 方法速查表
| 方法 | 作用 | 是否释放锁 | 是否响应中断 |
|---|---|---|---|
start() | 启动新线程 | — | — |
run() | 任务体(直接调是普通调用) | — | — |
sleep(ms) | 当前线程睡眠 | ❌ 不释放 | ✅ 抛异常 |
join() | 等待目标线程结束 | ✅ 释放(内部用 wait) | ✅ 抛异常 |
yield() | 礼让 CPU(hint) | ❌ 不释放 | ❌ |
interrupt() | 设置中断标志 | — | — |
isInterrupted() | 查询中断标志 | — | — |
四、守护线程(Daemon Thread)
Java 的线程分两类:用户线程(User Thread)和守护线程(Daemon Thread)。
- 用户线程:默认都是用户线程。JVM 会等所有用户线程结束才退出。
- 守护线程:在后台默默干活的线程。JVM 不会等守护线程结束——最后一个用户线程结束,JVM 直接退出,所有守护线程被强制终止。
典型的守护线程:GC 线程、Finalizer 线程、编译器线程——它们都是 JVM 的”后勤人员”。
Thread t = new Thread(() -> {
while (true) {
// 定期清理工作
}
});
t.setDaemon(true); // 必须在 start() 之前设置
t.start();
关键点:
setDaemon(true)必须在start()之前调用,否则抛IllegalThreadStateException。- 守护线程里不要操作不可恢复的资源(写文件、改数据库)——因为它可能被任意时刻终止,资源未关闭就消失了。
- 守护线程创建的子线程默认是守护线程。
五、线程优先级:不保证顺序
Thread 有个 setPriority(int) 方法,范围 1(MIN_PRIORITY)到 10(MAX_PRIORITY),默认 5(NORM_PRIORITY)。
t.setPriority(Thread.MAX_PRIORITY); // 10
但要注意——优先级只是给操作系统的 hint,不保证高优先级线程先执行。在不同 OS、不同 JVM 实现上行为完全不同。永远不要靠优先级来保证正确性——它最多用来”微调性能”,而且效果难以预测。
六、实战:多线程模拟并发下载
下面这个例子演示三种创建线程的方式、join 等待、interrupt 中断、守护线程,以及完整的生命周期观察。
观察重点:
t1在 start 前是 NEW,start 后变 RUNNABLE,结束后变 TERMINATED——这是状态机的核心流转。join让主线程等待子线程——主线程打印”等待下载完成”后阻塞,直到 t1/t2/t3 都结束才继续。future.get()阻塞取 Callable 返回值——Callable适合”开线程算个结果”。monitor.interrupt()不会强制停止线程——它只是设置标志,监控线程的sleep抛出InterruptedException,循环检查到中断后 break 退出。- 守护线程
monitor即使是while(true),主线程一结束它也会跟着退出——这就是守护线程的意义。- 优先级高的线程不一定先跑——输出顺序与优先级无必然关系。
七、本章小结
| 主题 | 核心要点 |
|---|---|
| 三种创建方式 | 继承 Thread(不推荐)/ 实现 Runnable(推荐)/ Callable+Future(需返回值) |
start vs run | start 开新线程,run 是普通方法调用 |
| 六种状态 | NEW → RUNNABLE → (BLOCKED/WAITING/TIMED_WAITING) → TERMINATED |
| BLOCKED vs WAITING | BLOCKED 等 synchronized 锁,WAITING 等通知/事件 |
sleep | 不释放锁,响应中断 |
join | 等待目标线程结束(内部用 wait),释放锁,响应中断 |
yield | 礼让 hint,无可靠语义 |
interrupt | 协作式停止——只设标志,线程自己检查 |
| 守护线程 | setDaemon(true) 必须在 start 前;JVM 不等守护线程退出 |
| 优先级 | 只是 hint,不保证顺序,不要靠它保证正确性 |
记忆口诀:
- start 开线程,run 是普通方法——别再搞错。
- sleep 不放锁,join 会放锁——这是面试高频题。
- BLOCKED 等 synchronized,WAITING 等通知——只有
synchronized让线程 BLOCKED。 - interrupt 是协作不是强停——线程自己决定怎么响应。
- 守护线程是后勤——别让它写关键数据。
- 优先级靠不住——别拿它当调度保证。
结语:从”会开线程”到”会管线程”
这一章我们学会了”造一个线程”——三种创建方式、六种状态、核心方法。但真实生产环境里,你几乎不会直接 new Thread——而是用线程池(第 43 章)。直接 new 线程的问题在于:创建/销毁开销大、无法控制并发数、没有任务队列、没有异常处理。
不过理解 Thread 是理解一切并发的基础——线程池内部还是用 Thread,CompletableFuture 默认用 ForkJoinPool,虚拟线程本质也是 Thread 的子类。把这些底子打牢,后面才能游刃有余。
下一章我们直面并发最核心的问题——线程安全:为什么多个线程同时改一个变量会出问题?什么是竞态条件?为什么 i++ 不是原子操作?这将是真正”并发入坑”的一章。