☕ 第十二章 多线程
📝 一句话概括

工人的一生六个状态:NEW(招聘到岗前)→ RUNNABLE(能干活)→ TERMINATED(退休),中间随时可能去三个歇脚点——TIMED_WAITING(定时歇)、WAITING(无限等)、BLOCKED(排队抢锁)——歇够了再回 RUNNABLE 抢 CPU。

12.2 请上了第二个工人,但工人也有作息:什么时候能干活、什么时候歇着、什么时候退休——生命周期就是研究这条作息表。Java 用 Thread.State 枚举表示,t.getState() 随时可查。

一、主线三站:生、干、死

① NEW(新建):new MyThread() 完的一瞬间。工人已经招进厂(对象在内存里),但还没上工——调 run() 不算上工(12.2 假上工的账)。

② RUNNABLE(可运行):start() 之后、run() 结束之前。注意它的名字是「可运行」不是「运行中」——这个状态把「抢到 CPU 正在跑」和「排队等 CPU」合在一起了,因为抢不抢得到全看调度(12.1 抢占式随机的账)。

③ TERMINATED(死亡):run() 执行完毕。终点站,不能复生——再次 start() 直接 IllegalThreadStateException(12.2 的账,这回知道为什么了:人都退休了还怎么返聘)。

二、三个歇脚状态(重点)

RUNNABLE 的工人随时可能被叫去歇着,歇的地方有三个,区别只在「等多久、等什么」:

状态 进去的姿势 多久能出来
TIMED_WAITING Thread.sleep(1000)、wait(1000) 带时间 时间到自动醒
WAITING wait()、join() 不带时间 别人唤醒/干完活才醒
BLOCKED 想拿锁,锁被别的工人占着 等到锁(锁是什么,本章后面揭晓)

三个状态歇完都回到 RUNNABLE 重新排队抢 CPU——不是原地接着跑。

sleep 与 join:本节两位主角

Thread.sleep(毫秒)——定时歇脚:

public class Demo19 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            for (int i = 1; i <= 3; i++) {
                System.out.println("干活 " + i);
                try {
                    Thread.sleep(500);       // 歇 500 毫秒再干下一条
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        System.out.println("开工前:" + t.getState());   // NEW
        t.start();
        System.out.println("start 后:" + t.getState()); // RUNNABLE
        Thread.sleep(100);                               // 主线程歇一会再看
        System.out.println("干着活:" + t.getState());    // TIMED_WAITING
        t.join();                                        // 主线程等 t 干完
        System.out.println("收工后:" + t.getState());    // TERMINATED
    }
}

t.join()——主线程调用后原地等 t 干完才继续。谁调 join,谁就 WAITING(无限等)。用处:想让某条线程先跑完再走后面流程,就是「插队汇合」。

⚠️ sleep 抛受检异常——5.2 的账又来了

Thread.sleep 声明抛 InterruptedException(受检异常),必须 try-catch 或 throws。为什么受检?因为「睡觉被打断」不是代码 bug,是外部环境(别的线程来打断你)——正好符合 5.2 的判定法。

三、一个微妙但常考的认知

RUNNABLE ≠ 正在 CPU 上跑。

同理,getState() 看到的是调用那一瞬间的快照——多线程下状态瞬息万变,两次查询之间状态可能换了好几轮。

四、完整一生速查

new 出来            → NEW
start()            → RUNNABLE
sleep(ms)/wait(ms) → TIMED_WAITING ─时间到→ 回 RUNNABLE
wait()/join()      → WAITING ─被唤醒/对方完事→ 回 RUNNABLE
抢锁失败            → BLOCKED ─拿到锁→ 回 RUNNABLE
run() 跑完          → TERMINATED(终点站)
🧪 三个 30 秒小实验

实验 1:Demo19 五连拍 跑一遍看五个状态打印——NEW、RUNNABLE、TIMED_WAITING、TERMINATED 一次看全(WAITING 和 BLOCKED 需要特殊场景,后面两节配齐)。

实验 2:sleep 睡着的线程占不占 CPU? 让线程 sleep(10000) → 开任务管理器看 CPU 占用几乎为 0。歇脚状态不占 CPU,这正是 sleep 的价值:把 CPU 让给别人。

实验 3:join 不带参数 vs 带 t.join() 主线程等到天荒地老(WAITING);t.join(1000) 最多等 1 秒(TIMED_WAITING)——同一个方法带不带毫秒参数,歇脚点都不一样。

五、常见误区

  1. RUNNABLE = 正在运行——它是「就绪 + 运行」的合并态,只保证有资格跑
  2. sleep 会释放锁——不会!sleep 睡着了抱着锁睡(这个坑本章后面「线程同步」重点清算,先记结论)
  3. main 也是从 NEW 走过来的——是,只是 JVM 帮你 start 了,你没亲眼看到而已
  4. 想让线程停就 stop()——stop() 早已废弃(暴力打死工人容易把厂子搞乱),正确姿势本章后面讲(用标志位温柔收工)
  5. wait 和 sleep 是一回事——差远了:wait 要先持锁、会释放锁、靠别人唤醒;sleep 抱锁睡、时间到自醒(锁的细节后面揭晓)

六、自测题

❓ Q1:六个状态分别是什么?

NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。

❓ Q2:RUNNABLE 为什么叫「可运行」而不叫「运行中」?

Java 把操作系统的「就绪 + 运行」合并了——它只保证有资格抢 CPU,不保证此刻在跑(跨平台抽象)。

❓ Q3:sleep(1000) 和 join() 分别进入哪个状态?怎么出来?

sleep(1000) → TIMED_WAITING,时间到自动醒;join() → WAITING,等目标线程 run 结束才回。

❓ Q4:为什么死掉的线程再 start 会抛异常?

TERMINATED 是终点站,线程对象的一条命只能 start 一次——复生在 JVM 层面就不允许。

❓ Q5(毕业题):写程序完整观察一条线程的一生——打印出 NEW、RUNNABLE、TIMED_WAITING、TERMINATED 四个状态(配合 join)。

要点:① new 后立刻 getState → NEW;② start 后立刻 → RUNNABLE;③ run 里 sleep,主线程稍等再查 → TIMED_WAITING;④ t.join() 后 → TERMINATED;⑤ 加分:说出 sleep 必须 try-catch 是因为 InterruptedException 受检、属于外部环境异常。全对毕业。

下一步学什么

相关笔记