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

12.3 露过脸的方法这节全领工牌:静态方法作用于「当前线程」,实例方法作用于「这条线程」——抓住这条,四大族全部理顺;重头戏是 join(),谁调 join,谁就原地等这条线程收工再往下走。

上一节把线程的一生画成了六状态图,但状态切换的扳机还没发:sleep 怎么睡、join 怎么等、yield 怎么让位——这节把 Thread 类的工具箱整个摊开。12.1 埋的那笔「连接不能全局共用」的账也快到期了,先把地基打牢。

一、先立一条总纲:静态 vs 实例

Thread 类的方法分两派,混了必翻车:

派别 特征 作用于谁 代表
静态方法(static) 用类名调 Thread.xxx() 当前正在执行的线程(Thread.currentThread()) sleep()、yield()
实例方法 用线程对象调 t.xxx() 这条指定的线程 join()、setName()、isAlive()
⚠️ 第一大坑:`t.sleep(1000)` 睡的不是 t

sleep 虽然长得像实例方法,但它是 static——t.sleep(1000) 会被编译器警告,实际效果等同于 Thread.sleep(1000),睡的永远是当前正在跑的线程。哪怕 t 是别人的工牌,写了 t.sleep 也是自己睡。Java 后来自己也嫌这设计别扭,直接建议只写 Thread.sleep(...)。

二、四大族逐个领工牌

① 身份牌:获取与命名(收 12.1/12.2 的账)

方法 说明
Thread.currentThread() 静态,拿到当前正在执行的线程对象。12.1 就是靠它让 main 主线程现身的
getName() / setName("工人A") 工牌。默认名 Thread-0、Thread-1 顺数;new Thread(r, "工人A") 构造传名更顺手(12.2 的好习惯)
isAlive() 还活着吗?活着返回 true。NEW 和 TERMINATED 都是 false——对照 12.3 的状态图一目了然

② 时间控制:睡与等(本节重头戏)

Thread.sleep(long 毫秒) —— 让当前线程睡一会儿:

t.join() —— 谁调 join,谁原地等 t 干完活再走:

public class Demo20 {
    static long sum = 0;   // 工人算完放这里
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            for (int i = 1; i <= 1000; i++) sum += i;   // 工人算账
        }, "会计");
        t.start();
        System.out.println("先打印:" + sum);   // 几乎一定是 0 —— 会计还没算完
        t.join();                               // main 原地等会计收工
        System.out.println("join 后打印:" + sum);   // 500500,稳了
    }
}
💡 看到什么算成功

两行输出:第一行 0,第二行 500500。第一行是 0 不是 bug,恰恰是多线程的教科书现场——它证明 main 和工人是并行的。

③ 协调:让位与插队(了解即可)

方法 说明 泼冷水
Thread.yield() 静态,礼让:建议调度器「我让让,别人先来」 只是建议,调不调看调度器心情,别指望它
t.setPriority(1~10) 优先级,默认 5 数字大只是抢到的概率大一点,不代表一定先跑——12.1 的抢占式调度没变

④ 守护与打断

t.setDaemon(true) —— 把 t 标成守护线程(后台线程):

t.interrupt() —— 打断:给正在 sleep/join 的线程递一张「该醒了」的纸条,对方的 sleep 会立刻抛 InterruptedException 提前醒来。注意是温柔打断,不是强杀(强杀的 stop() 早被官方废弃了)。

三、动手:Demo20 与三个 30 秒实验

上面 Demo20 跑通后,再做三个小实验:

  1. 注释掉 t.join() 那行——两行大概率都是 0。「并行不看结果」的现场。
  2. 把 Thread.sleep(1000) 写成 t.sleep(1000)——IDEA 划黄线警告,但程序照跑,睡的还是 main。static 的坑亲眼看。
  3. 给工人 setDaemon(true)(放 start 前面)——工人还没算完,main 一退场 JVM 直接散场,后面的输出可能就没了。守护线程=伴舞的,主角下台它也走。

四、常见误区

  1. 「sleep 是实例方法,t.sleep 让 t 睡」——错。static 只睡当前线程,写 t.sleep 等于 Thread.sleep。
  2. 「join 会打断对方」——错。join 是自己等,interrupt 才是打断;方向别搞反。
  3. 「setDaemon 什么时候设都行」——错。必须 start 前,start 后设置抛异常。
  4. 「优先级高的一定先跑」——错。只是概率,抢占式调度的随机性没有消失。
  5. 「yield 一调就让出 CPU」——错。只是建议,可能让了白让,调度器理都不理。

五、自测题(先自己想,再展开)

❓ Q1:`t.sleep(5000)` 到底谁睡?

当前线程睡。sleep 是静态方法,永远作用于 Thread.currentThread(),跟 t 半毛钱关系没有。正确写法 Thread.sleep(5000)。

❓ Q2:main 里调 `t.join()` 后,main 处于 12.3 六状态中的哪个?

WAITING(无参版,无限等);join(2000) 则是 TIMED_WAITING。等 t 死亡后被唤醒回 RUNNABLE。

❓ Q3:为什么 Demo20 注释掉 join 后打印的 sum 是 0?

main 和工人并行:main 跑到打印那一行时,工人刚 start 还没算完(甚至没开始)。sum 没被写,默认值 0。加 join 后 main 等收工,才能拿到 500500。

❓ Q4:GC 垃圾回收线程是守护线程吗?这说明了什么?

是。用户线程(main)全部结束后 JVM 直接散场,不管守护线程——守护线程的存在意义是「伺候」主战场,主战场没了,伺候也就没了。

❓ Q5(毕业题):写一个程序:两个子线程分别算 1~1000 与 1001~2000 的和,main 等两人都收工后打印总和。

骨架:两个 Thread 各跑一段循环(各自写进自己的变量)→ t1.join(); t2.join(); 两句都调 → 打印 t1 的结果 + t2 的结果。两行 join 一行都不能少——只等一个,另一个可能还没算完。这题是下节「线程同步」最好的引子。

下一步学什么

相关笔记