继承 Thread 创建线程 | JavaSE
继承 Thread 创建线程
一、学习目标
完成本章后,你应该能够:
- 理解
java.lang.Thread在 Java 多线程编程中的基本作用。 - 独立使用“继承
Thread类”的方式创建并启动线程。 - 准确说明
run()方法承担什么职责。 - 准确说明
start()方法承担什么职责。 - 彻底区分
thread.start()与thread.run()。 - 理解调用
start()后主线程与子线程的执行顺序为什么具有不确定性。 - 理解一个
Thread对象最多只能启动一次。 - 能够分析继承
Thread方式的优点、缺点与适用场景。
二、核心知识
2.1 Java 中线程的代表:Thread
Java 为线程提供了核心类:
java.lang.Thread
Thread 表示:
Java 中的一条执行线程。
上一章我们建立了:
进程
└── 多条线程
这一章第一次真正创建自己的线程。
最经典的创建方式之一就是:
继承 Thread
2.2 创建线程的核心思想
假设我们希望程序产生一条新的执行流程,并让这条执行流程执行:
打印 0~4
需要解决两个问题。
第一个:
新线程到底干什么?
答案:
run()
第二个:
什么时候真正创建新的执行流程并让它运行?
答案:
start()
所以可以先记住最重要的一组关系:
run()
↓
描述线程任务
start()
↓
启动线程
千万不要把二者混为一谈。
2.3 继承 Thread 创建线程的四个步骤
完整流程:
第一步
定义类继承 Thread
↓
第二步
重写 run()
↓
第三步
创建线程对象
↓
第四步
调用 start()
对应代码:
class MyThread extends Thread {
@Override
public void run() {
// 子线程需要执行的任务
}
}
然后:
MyThread thread = new MyThread();
thread.start();
这样才真正启动一条新的执行线程。
2.4 第一个完整线程程序
public class ThreadDemo {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
for (int i = 0; i < 5; i++) {
System.out.println("main线程:" + i);
}
}
}
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程:" + i);
}
}
}
此时程序至少存在两条我们关心的执行流程:
main 线程
│
├── 创建 MyThread
│
├── thread.start()
│
└── main 循环
│
│
│并发
│
▼
MyThread 子线程
│
└── run()
调用:
thread.start();
之后,新的线程会被调度执行 run()。
与此同时:
main
并不会因为调用 start() 就自动等待子线程执行完毕。
因此:
main线程
+
MyThread线程
会并发执行。
三、使用方法
3.1 第一步:定义 Thread 子类
class DownloadThread extends Thread {
}
为什么能这样做?
因为:
DownloadThread
继承了:
Thread
因此它本身就是一种线程类型。
3.2 第二步:重写 run()
class DownloadThread extends Thread {
@Override
public void run() {
System.out.println("开始下载文件");
}
}
run() 中应该写:
这条线程真正需要执行的任务。
例如:
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println("正在处理:" + i);
}
}
3.3 第三步:创建线程对象
DownloadThread thread = new DownloadThread();
注意:
创建对象:
new DownloadThread()
并不等于线程已经开始执行。
此时只是创建了一个线程对象。
3.4 第四步:调用 start()
thread.start();
start() 的作用是:
调度该线程开始执行。
线程真正开始执行后,会运行它的:
run()
于是:
thread.start()
↓
JVM / 调度系统
↓
新的执行线程
↓
thread.run()
3.5 同时启动两个线程
一个 MyThread 类可以创建多个线程对象:
public class ThreadDemo {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.start();
t2.start();
}
}
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("执行任务:" + i);
}
}
}
此时:
t1
和:
t2
是两个不同的线程对象。
它们启动后分别拥有自己的执行流程。
概念模型:
main
├── t1.start()
│ └── t1.run()
│
└── t2.start()
└── t2.run()
三个执行流程可能在时间上相互交错。
四、原理与进阶
4.1 start() 和 run() 是本章最重要的区别
这是必须彻底掌握的知识点。
假设:
MyThread thread = new MyThread();
如果写:
thread.start();
表示:
启动线程,让新的线程执行
run()。
模型:
main线程
│
└── start()
│
├────────→ 新线程
│ │
│ └── run()
│
└── main继续执行
于是:
main
与:
新线程
可以并发执行。
4.2 如果直接调用 run() 会怎样
如果写:
thread.run();
对于本章通过 Thread 子类创建的传统平台线程对象来说,它只是:
当前线程进行了一次普通的方法调用。
例如:
public class ThreadDemo {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.run();
System.out.println("main结束");
}
}
实际模型接近:
main线程
│
├── 调用 thread.run()
│ │
│ └── run执行完毕
│
└── main继续
整个过程仍然主要发生在:
main线程
并没有因为调用:
run()
就产生新的执行线程。
因此:
thread.start();
和:
thread.run();
完全不是一回事。
可以记:
start() → 启线程
run() → 跑任务
4.3 为什么不能用 run() 替代 start()
因为普通方法调用:
thread.run();
只是遵循普通 Java 方法调用规则。
调用者还是:
当前线程
而:
thread.start();
进入了线程生命周期管理。
JDK 会安排:
该 Thread 对象代表的线程
开始执行。
因此真正创建多线程程序时:
必须调用 start()
而不是自己直接调用 run()。
4.4 start() 调用后不代表 run() 已经立即执行
看:
thread.start();
System.out.println("main");
不能认为结果一定是:
子线程
main
也不能认为一定是:
main
子线程
因为:
start()
的核心语义是:
把线程调度为可以开始执行。
之后具体什么时候获得执行机会受到线程调度影响。
因此:
start()
不是:
“立即完整执行 run()”
更不是:
“等 run() 执行完成以后再继续”
4.5 多线程执行顺序具有不确定性
运行:
public class ThreadDemo {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
for (int i = 0; i < 5; i++) {
System.out.println("main:" + i);
}
}
}
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("child:" + i);
}
}
}
某一次可能得到:
main:0
child:0
main:1
child:1
child:2
main:2
...
第二次可能变成:
child:0
child:1
main:0
main:1
child:2
...
这都是正常的。
必须建立:
不要根据某一次运行结果推断线程永远按照这个顺序运行。
4.6 一个 Thread 对象最多启动一次
例如:
MyThread thread = new MyThread();
thread.start();
thread.start();
第二次:
start()
是不允许的。
线程对象一旦已经启动,不能再次通过同一个对象重新启动。
即使:
线程已经执行完毕
也不能:
再次 start()
如果再次启动,会出现:
IllegalThreadStateException
如果希望再运行一次任务,应创建:
新的 Thread 对象
例如:
new MyThread().start();
new MyThread().start();
4.7 Thread 对象和线程任务不要混淆
在本章方式中:
class MyThread extends Thread
导致:
线程对象
+
任务代码
被放在了同一个类里。
例如:
class MyThread extends Thread {
@Override
public void run() {
// 任务
}
}
这里:
MyThread 对象
既表示线程,又携带它执行的任务。
这种写法确实简单。
但是也产生一个问题:
MyThread
已经:
extends Thread
Java 又只支持类的单继承。
因此它不能再:
extends 其他业务父类
这就是本方式最主要的扩展性缺陷。
下一章学习:
Runnable
就是为了把:
任务
和:
线程
进一步分离。
4.8 “不要把主线程代码写在 start() 前面”到底是什么意思
初学教程中经常提醒:
不要把主线程的大量任务放在启动子线程之前。
例如:
for (int i = 0; i < 1_000_000; i++) {
// main执行大量任务
}
thread.start();
这样在:
thread.start()
真正执行以前:
子线程压根还没有启动
所以前面的任务不存在多线程并发。
但需要准确理解:
这不是 Java 语法限制。
你当然可以在:
start()
之前执行任意代码。
真正的问题只是:
没有调用 start()
↓
新线程尚未启动
↓
前面的代码当然只有当前线程自己执行
如果你的目标是观察:
main线程
+
子线程
并发执行,就应该先调用:
start()
再让 main 执行自己的任务。
五、实践应用
5.1 模拟后台任务
public class DownloadDemo {
public static void main(String[] args) {
DownloadThread thread = new DownloadThread();
thread.start();
for (int i = 1; i <= 5; i++) {
System.out.println("main 正在处理用户操作:" + i);
}
}
}
class DownloadThread extends Thread {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("正在下载第 " + i + " 个文件块");
}
}
}
从逻辑上:
main线程
↓
处理用户操作
DownloadThread
↓
执行下载任务
两个任务可以并发推进。
5.2 创建不同类型的任务线程
例如:
class LogThread extends Thread {
@Override
public void run() {
System.out.println("写入日志");
}
}
以及:
class BackupThread extends Thread {
@Override
public void run() {
System.out.println("备份数据");
}
}
启动:
public class Application {
public static void main(String[] args) {
LogThread logThread = new LogThread();
BackupThread backupThread = new BackupThread();
logThread.start();
backupThread.start();
System.out.println("main继续执行");
}
}
程序便拥有多个执行任务。
不过随着项目扩大:
一个任务定义一个 Thread 子类
会越来越笨重。
这也是为什么工程开发中通常更倾向于:
任务与线程分离
后续通过:
Runnable
和:
Callable
表达任务。
六、常见问题
6.1 创建 Thread 对象以后线程就开始了吗?
没有。
new MyThread();
只是创建对象。
真正启动线程:
thread.start();
6.2 run() 是谁调用的?
正常线程启动流程中,我们调用:
start()
线程开始执行后,其:
run()
作为线程任务被执行。
初学阶段不要自己用:
thread.run();
代替线程启动。
6.3 start() 会直接执行完整的 run() 再返回吗?
不会。
调用:
start()
之后,新线程与当前线程可以并发推进。
6.4 为什么每次输出顺序不同?
因为多个线程的具体调度顺序具有不确定性。
不能依赖某一次输出顺序。
6.5 可以连续调用两次 start() 吗?
不可以。
同一个 Thread 对象最多启动一次。
6.6 线程执行完后还能重新 start() 吗?
不能。
如果需要再次执行,创建新的线程对象。
6.7 继承 Thread 最大的优点是什么?
代码直观、简单:
继承 Thread
重写 run
创建对象
start
非常适合学习线程运行模型。
6.8 继承 Thread 最大的问题是什么?
Java 类只能单继承。
一旦:
class MyThread extends Thread
就无法再:
extends 其他父类
扩展能力较弱。
6.9 为什么实际开发更常强调 Runnable、Callable 和线程池?
因为工程中通常希望:
任务
与:
执行任务的线程
解耦。
例如:
Task
↓
交给
↓
Thread / ThreadPool
会比:
每个任务自己继承 Thread
更加灵活。
不过理解 Thread 继承方式仍然非常重要,因为它最直观地展示了线程执行模型。
七、练习与验收
7.1 知识问答
- Java 中表示线程的核心类是什么?
- 继承
Thread创建线程需要哪几个步骤? - 为什么需要重写
run()? run()方法表示什么?start()方法表示什么?start()与run()有什么本质区别?- 为什么直接调用
run()不能实现本章所说的新线程执行? - 创建
Thread对象是否等于线程已经启动? - 调用
start()以后,主线程会不会自动等待子线程结束? - 为什么多线程程序的输出顺序通常不固定?
- 同一个
Thread对象为什么不能调用两次start()? - 一个线程执行结束后还能重新启动吗?
- 如果希望任务再次执行,应该怎么办?
- 继承
Thread有什么优点? - 继承
Thread有什么缺点? - Java 的单继承机制为什么会影响这种创建线程方式?
7.2 代码阅读
不要运行代码,先预测。
public class ThreadRead {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
for (int i = 0; i < 3; i++) {
System.out.println("main:" + i);
}
}
}
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println("child:" + i);
}
}
}
回答:
- 程序中至少存在几条我们主动使用的执行流程?
child:0一定先于main:0吗?child:0、1、2自身的顺序会不会变成2、0、1?- main 循环和 child 循环之间为什么可能交错?
- 能否写出程序唯一确定的完整输出顺序?为什么?
再阅读:
public class ThreadRead2 {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.run();
System.out.println("main结束");
}
}
class MyThread extends Thread {
@Override
public void run() {
System.out.println("执行任务");
}
}
回答:
- 这里是否真正启动了新的线程?
run()由哪条线程执行?- 为什么?
7.3 手写代码
任务一:基础线程
关闭 AI 自动补全,从零完成:
MyThread
要求:
- 继承
Thread; - 重写
run(); - 输出 1~10;
- 在
main()中创建线程对象; - 调用
start()。
任务二:两个线程
创建:
NumberThread
LetterThread
其中:
NumberThread → 输出 1~10
LetterThread → 输出 A~J
在 main() 中分别启动。
观察多次运行结果。
回答:
两种输出是否始终按照相同顺序交错?
任务三:主线程 + 子线程
子线程:
输出:子线程 1~20
main:
输出:主线程 1~20
要求先:
start();
再执行 main 循环。
重复运行至少三次,观察结果。
7.4 Debug
下面代码希望启动新线程:
class MyThread extends Thread {
@Override
public void run() {
System.out.println("子线程执行");
}
}
public class Test {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.run();
}
}
要求:
- 判断代码是否存在语法错误。
- 判断它是否达到了“启动新线程”的目的。
- 解释错误原因。
- 修正代码。
继续分析:
MyThread thread = new MyThread();
thread.start();
thread.start();
要求:
- 判断第一次
start()是否正常。 - 判断第二次调用会发生什么。
- 给出正确设计思路。
7.5 综合训练
设计一个简单的“双任务系统”。
需求:
任务 A:
输出“正在读取文件”,循环 20 次
任务 B:
输出“正在处理数据”,循环 20 次
main:
输出“主程序继续运行”,循环 20 次
要求:
FileThread extends ThreadCalculateThread extends Thread- 分别重写
run() - 创建两个线程对象
- 使用
start()启动 - main 自己继续执行循环
- 连续运行程序至少三次
- 对比三次输出顺序
- 用自己的语言解释为什么每次顺序可能不同
暂时不要使用:
Runnable
Callable
线程池
synchronized
Lock
这些属于后续章节。
7.6 本章验收
在不查看资料的情况下完成以下内容:
- 默写继承
Thread创建线程的完整四步流程。 - 手写一个完整可运行的
Thread子类。 - 解释
run()的职责。 - 解释
start()的职责。 - 画图说明:
thread.run()
与:
thread.start()
的执行模型区别。
- 解释为什么多线程输出顺序不能依赖一次运行结果。
- 解释为什么同一个线程对象不能启动两次。
- 说出继承
Thread的一个优点和一个核心缺点。 - 能够独立 Debug “把
start()错写成run()”的问题。
如果以上内容可以闭卷完成,就可以进入下一章:
Runnable 创建线程