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

五节一条线:认识工厂 → 请工人 → 看一生 → 发工具 → 上锁。多线程的一切都是「并发的收益」和「共享的代价」做交换——想快就并行,并行就要共享,共享就要上锁。

一、五节串线

节 主题 一句话收口
12.1 概念 进程=工厂(独立内存),线程=工人(共享资源、真正干活);并发=交替的错觉,并行=多核真同时
12.2 创建 继承 Thread 占继承名额,实现 Runnable 任务工人分离(Lambda 老主场);start 真上工,run 假上工
12.3 生命周期 六状态 + 三歇脚点;RUNNABLE ≠ 正在跑;TERMINATED 不能复生(start 只能一次的谜底)
12.4 方法 static 作用于当前线程,实例方法作用于这条线程;join=等收工再走;GC=守护线程
12.5 同步 count++ 三步会插队丢更新;锁=排队;多线程必须用同一把锁;11.4 抢桌子账清算

二、概念速查 12 条

  1. 进程:正在运行的程序,独立内存围墙;任务管理器一行一个
  2. 线程:进程里真正干活的单位,共享进程资源
  3. 并发:单核交替(宏观同时微观轮流);并行:多核同时
  4. 抢占式调度:谁抢到 CPU 谁跑,完全随机
  5. main 主线程:Thread.currentThread().getName() 亲眼看
  6. Thread / Runnable:自带头脑的工人 vs 剥出来的任务单(函数式接口)
  7. start vs run:通知 JVM 开新线程 vs 主线程普通方法调用
  8. 六状态:NEW → RUNNABLE →(TIMED_WAITING / WAITING / BLOCKED)→ TERMINATED
  9. sleep / join:定时歇(静态、睡当前)vs 等收工(实例、谁调谁等)
  10. 守护线程:用户线程全退它跟着退(GC 典型);setDaemon 必须 start 前
  11. 线程安全三要素:共享资源 + 多线程 + 有修改
  12. 锁:synchronized 代码块 / 同步方法(this)/ 静态方法(Class)/ Lock 手动档

三、代码模板 4 段(背这几块就够)

① 创建(Runnable + Lambda,标准姿势)

Thread t = new Thread(() -> {
    // 工人的活
}, "工人A");
t.start();

② 等结果(join 收账)

t.start();
t.join();            // main 原地等收工,再取结果

③ 定时循环(sleep 节拍器)

while (true) {
    System.out.println("滴答 " + new Date());
    Thread.sleep(1000);          // 抛受检 InterruptedException
}

④ 同步(卖票标准骨架)

synchronized (lock) {            // lock = 多线程共用的同一把锁
    if (count > 0) {
        System.out.println(getName() + " 卖出第 " + count-- + " 张");
    }
}

四、名坑回顾(本章五大坑)

  1. run 直接调——写了一堆多线程代码跑出单线程效果;招工人用 start()
  2. start 调两次——IllegalThreadStateException;工人不能复活(TERMINATED 终点站)
  3. t.sleep() 以为睡 t——static 只睡当前线程;写 Thread.sleep(...)
  4. 忘 join 就取结果——工人还没算完,拿到的永远是初始值(Demo20 的 0)
  5. 各锁各的 / Lock 忘 unlock——等于没锁裸奔;手动锁不还,全员永久 BLOCKED

五、收官暗线:收益与代价的交换

整章其实是一场交易:

想要快 → 就得并行 → 并行就得共享资源 → 共享就会被插队 → 就得上锁 → 上锁就牺牲一部分并行度。

从 12.1「多线程让界面不卡」的甜头,到 12.5「锁让并行变排队」的代价,五节走完的是同一条因果链。这也是为什么后面会学线程池(把锁的代价摊薄)、wait/notify(排队时别干烧 CPU)、并发工具类(别人替你把锁用对)——这门课后面所有「高级」的东西,都是在给这条交易链找更聪明的解法。

六、跨章连线

七、毕业大题:三窗口卖票(本章集大成)

写一个程序:100 张票、3 个窗口同时卖,要求不重票、不跳号、不卖负数,卖完打印「共耗时」。

自检清单(6 项):

  1. 票数 count 是共享资源 → 必须是静态成员或同一 Runnable 对象的字段
  2. 用 Runnable + Lambda 创建三个窗口(别占继承名额)
  3. 卖票的核心代码用 synchronized 包住,锁对象三窗口共用同一把
  4. 判断和扣减必须在同一个同步块里(拆开照样重票)
  5. count 到 0 时优雅收摊退出循环,别死循环
  6. 回答:为什么 count-- 一行也要锁?(答出「读加写三步」才算懂)
✅ 过关标准

三个窗口的票号加起来正好 1~100 各出现一次、无重复无遗漏,跑十次次次如此——你就真正毕业了。

下一步学什么