12.3 露过脸的方法这节全领工牌:静态方法作用于「当前线程」,实例方法作用于「这条线程」——抓住这条,四大族全部理顺;重头戏是 join(),谁调 join,谁就原地等这条线程收工再往下走。
上一节把线程的一生画成了六状态图,但状态切换的扳机还没发:sleep 怎么睡、join 怎么等、yield 怎么让位——这节把 Thread 类的工具箱整个摊开。12.1 埋的那笔「连接不能全局共用」的账也快到期了,先把地基打牢。
一、先立一条总纲:静态 vs 实例
Thread 类的方法分两派,混了必翻车:
| 派别 | 特征 | 作用于谁 | 代表 |
|---|---|---|---|
| 静态方法(static) | 用类名调 Thread.xxx() |
当前正在执行的线程(Thread.currentThread()) |
sleep()、yield() |
| 实例方法 | 用线程对象调 t.xxx() |
这条指定的线程 | join()、setName()、isAlive() |
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 毫秒) —— 让当前线程睡一会儿:
- 到点自动醒,回到 RUNNABLE 重新抢 CPU(12.3 的 TIMED_WAITING 就是它在触发)
- 声明抛受检异常
InterruptedException——「睡觉被人打断」不是代码 bug,是外部环境的事,正好又过一遍 5.2 的判定法 - 单位是毫秒:
Thread.sleep(1000)才是 1 秒
t.join() —— 谁调 join,谁原地等 t 干完活再走:
- 实例方法:
t.join()写在 main 里,就是 main 等 t;写在哪条线程里,哪条线程等 - 本质:调用线程进入 WAITING(12.3 的无限等歇脚点),等目标线程 run 结束才被放行
t.join(2000)是限时版:最多等 2 秒,等不到也不傻等- 为什么需要它:12.2 见过——子线程还在算,main 已经跑到打印,结果永远是错的。join 就是「先等收工、再取结果」
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 标成守护线程(后台线程):
- 规矩:用户线程(普通线程)是主角,全部退场后,JVM 不管守护线程死活,直接散场
- 典型:GC 垃圾回收就是守护线程——main 都结束了,还回收什么垃圾
- 必须在
start()之前设置,start 之后再调抛IllegalThreadStateException(12.2「start 只能一次」的亲戚)
t.interrupt() —— 打断:给正在 sleep/join 的线程递一张「该醒了」的纸条,对方的 sleep 会立刻抛 InterruptedException 提前醒来。注意是温柔打断,不是强杀(强杀的 stop() 早被官方废弃了)。
三、动手:Demo20 与三个 30 秒实验
上面 Demo20 跑通后,再做三个小实验:
- 注释掉
t.join()那行——两行大概率都是0。「并行不看结果」的现场。 - 把
Thread.sleep(1000)写成t.sleep(1000)——IDEA 划黄线警告,但程序照跑,睡的还是 main。static 的坑亲眼看。 - 给工人
setDaemon(true)(放 start 前面)——工人还没算完,main 一退场 JVM 直接散场,后面的输出可能就没了。守护线程=伴舞的,主角下台它也走。
四、常见误区
- 「sleep 是实例方法,t.sleep 让 t 睡」——错。static 只睡当前线程,写
t.sleep等于Thread.sleep。 - 「join 会打断对方」——错。join 是自己等,interrupt 才是打断;方向别搞反。
- 「setDaemon 什么时候设都行」——错。必须 start 前,start 后设置抛异常。
- 「优先级高的一定先跑」——错。只是概率,抢占式调度的随机性没有消失。
- 「yield 一调就让出 CPU」——错。只是建议,可能让了白让,调度器理都不理。
五、自测题(先自己想,再展开)
当前线程睡。sleep 是静态方法,永远作用于 Thread.currentThread(),跟 t 半毛钱关系没有。正确写法 Thread.sleep(5000)。
WAITING(无参版,无限等);join(2000) 则是 TIMED_WAITING。等 t 死亡后被唤醒回 RUNNABLE。
main 和工人并行:main 跑到打印那一行时,工人刚 start 还没算完(甚至没开始)。sum 没被写,默认值 0。加 join 后 main 等收工,才能拿到 500500。
是。用户线程(main)全部结束后 JVM 直接散场,不管守护线程——守护线程的存在意义是「伺候」主战场,主战场没了,伺候也就没了。
骨架:两个 Thread 各跑一段循环(各自写进自己的变量)→ t1.join(); t2.join(); 两句都调 → 打印 t1 的结果 + t2 的结果。两行 join 一行都不能少——只等一个,另一个可能还没算完。这题是下节「线程同步」最好的引子。
下一步学什么
- ☑ 12.5 线程同步 ✅ —— 点此复习:抢桌子账清算,BLOCKED 谜底揭晓
- 想温故:12.3 状态切换是这节每个方法的背后剧本
相关笔记
- 第十二章 多线程/12.2 线程的创建:start/run 分家、起名习惯的出处
- 第十二章 多线程/12.3 线程的生命周期及状态转换:sleep/join 触发的歇脚点全在这张图上
- 第五章 异常/5.2 运行时异常与编译时异常:InterruptedException 为什么是受检——「睡觉被打断不是你的错」
- 第四章 面向对象(下)/4.7 函数式编程:Demo20 里 Runnable 的 Lambda 写法老家