请工人有两条路:继承 Thread 重写 run(工人自带头脑)或 实现 Runnable 把任务交给它(任务与工人分离,4.7 Lambda 老主场)——最要命的坑是 start() 才是真上工,直接调 run() 是假上工。
12.1 说「Java 程序至少有一条 main 主线程」,工人不止一个才能叫多线程。这节就干一件事:亲手把第二个工人请上岗。
一、方式一:继承 Thread
Thread 类自带「工人」血统——继承它、重写 run(),这个类的对象就是工人:
public class Demo17 {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start(); // 上工:开新线程跑 run
for (int i = 0; i < 5; i++) {
System.out.println("main 主线程 " + i);
}
}
}
class MyThread extends Thread {
@Override
public void run() { // @Override 收 4.1 的账
for (int i = 0; i < 5; i++) {
System.out.println(getName() + " 子线程 " + i);
}
}
}
期望输出(顺序每次都可能不一样——12.1 说的抢占式调度随机,这回亲眼看):
main 主线程 0
Thread-0 子线程 0
main 主线程 1
Thread-0 子线程 1
...
两条执行流交替出现,谁先谁后全看谁抢到 CPU——这就是多线程和单线程最直观的区别。
二、方式二:实现 Runnable(推荐)
继承 Thread 有个硬伤:占掉了类唯一的继承名额(4.1「Java 单继承」的账)。更聪明的做法是把「活儿」和「工人」分开——活儿写进 Runnable,工人只管把活儿带走:
public class Demo18 {
public static void main(String[] args) {
Runnable r = () -> { // Lambda 三段式收 4.7 的账
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " 干活 " + i);
}
};
new Thread(r, "工人A").start(); // 第二个参数起名,告别 Thread-0
}
}
对比方式一的三点进步:
- 不占继承名额——任务类想继承谁继承谁
- 任务与工人分离——同一份 Runnable 可以丢给十个 Thread(一份活儿雇十个工人)
- Runnable 是函数式接口——Lambda 一行顶一个类,4.7 埋的「Lambda 老主场」正式开张
new Thread(r) 不起名就是 Thread-0、Thread-1;传第二个参数 "工人A",以后看日志一眼知道哪条线程在作妖。排查多线程问题,名字就是工人的工牌。
三、本节第一大坑:start() ≠ run()
MyThread t = new MyThread();
t.run(); // 假上工:主线程亲自把 run 跑完,才执行下一行
t.start(); // 真上工:JVM 开新线程,主线程立刻返回继续往下跑
t.start() |
t.run() |
|
|---|---|---|
| 本质 | 通知 JVM 开新线程执行 run | 普通方法调用 |
| 执行者 | 新开的子线程 | 还是 main 主线程 |
| 效果 | 两条流并发 | 和没写多线程一样 |
| 能调几次 | 一次(第二次 IllegalThreadStateException) |
随便调(就是普通方法) |
记忆口诀:start 是招聘上岗,run 只是背了一遍工作内容。背得再熟,人也还在主线程这间办公室里。
四、常用方法速览
| 方法 | 作用 |
|---|---|
start() |
开新线程执行 run(只能调一次) |
run() |
工作内容本身,被 start 调用才有并发效果 |
getName() / setName() |
取 / 改工人名(Thread-0、main...) |
Thread.currentThread() |
摸到「当前正在执行这行代码」的线程 |
Thread.sleep(毫秒) |
让当前线程歇一会儿(本节只露脸,下节细讲) |
实验 1:交替输出亲眼看调度 Demo17 连跑三次 → 三次输出顺序都可能不同。第一次「看见」抢占式调度。
实验 2:start 改成 run
Demo17 把 t.start() 改成 t.run() → 输出变成先打完 5 行子线程再打 main,永远单线程顺序。假上工实锤。
实验 3:start 两次
Demo17 里连写两行 t.start(); → 第二次直接 IllegalThreadStateException。一个工人不能上两次岗,想再干要 new 新工人。
五、常见误区
- 调 run() 以为开了线程——没有新线程,只有主线程在演独角戏
- start 调多次——一次上岗一条命,重复 start 抛
IllegalThreadStateException - 重写 run 写成了重载——
run(String s)带参数的重载不是重写,Thread 根本不会调它(4.1 重写的签名规矩) - 以为先 start 的线程先执行——抢占式调度完全随机,线程没有「先来后到」的保证
- 继承 Thread 后滥用——大多数场景用 Runnable:不占继承、可复用、能进线程池(11.5 的池子后面会重逢)
六、自测题
方式一继承 Thread 重写 run;方式二实现 Runnable 把任务交给 Thread。推荐 Runnable:不占继承名额、任务可复用、Lambda 友好。
start 通知 JVM 开新线程执行 run(真并发);run 是普通方法调用,还在主线程里跑(假并发)。start 只能调一次。
抢占式调度——CPU 分配给哪个线程是随机的,程序员控制不了(12.1 的账)。
给线程起名。默认 Thread-0/1/2 没法看日志定位问题,工牌式的排查习惯。
要点:① 一个 Runnable 写打印 Thread.currentThread().getName();② new Thread(r, "A").start() 和 new Thread(r, "B").start();③ 说出「三条执行流并发(main + A + B),顺序随机」;④ 加分:解释同一个 Runnable 被两个 Thread 共用——任务与工人分离的直接受益。全对毕业。
下一步学什么
- ☑ 12.3 线程的生命周期及状态转换 ✅ 已完成——点此复习:六状态速查 + sleep/join 两主角 + RUNNABLE 合并态认知。
相关笔记
- 第十二章 多线程/12.1 进程与线程:工厂与工人、抢占式调度随机
- 第四章 面向对象(下)/4.1 继承:单继承名额,方式一的硬伤
- 第四章 面向对象(下)/4.7 函数式编程:Runnable = Lambda 老主场
- 第十一章 JDBC/11.5 DbUtils:任务与工人分离的思路,线程池还会重逢