继承 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 知识问答

  1. Java 中表示线程的核心类是什么?
  2. 继承 Thread 创建线程需要哪几个步骤?
  3. 为什么需要重写 run()
  4. run() 方法表示什么?
  5. start() 方法表示什么?
  6. start()run() 有什么本质区别?
  7. 为什么直接调用 run() 不能实现本章所说的新线程执行?
  8. 创建 Thread 对象是否等于线程已经启动?
  9. 调用 start() 以后,主线程会不会自动等待子线程结束?
  10. 为什么多线程程序的输出顺序通常不固定?
  11. 同一个 Thread 对象为什么不能调用两次 start()
  12. 一个线程执行结束后还能重新启动吗?
  13. 如果希望任务再次执行,应该怎么办?
  14. 继承 Thread 有什么优点?
  15. 继承 Thread 有什么缺点?
  16. 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);
        }
    }
}

回答:

  1. 程序中至少存在几条我们主动使用的执行流程?
  2. child:0 一定先于 main:0 吗?
  3. child:0、1、2 自身的顺序会不会变成 2、0、1
  4. main 循环和 child 循环之间为什么可能交错?
  5. 能否写出程序唯一确定的完整输出顺序?为什么?

再阅读:

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("执行任务");
    }
}

回答:

  1. 这里是否真正启动了新的线程?
  2. run() 由哪条线程执行?
  3. 为什么?

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();
    }
}

要求:

  1. 判断代码是否存在语法错误。
  2. 判断它是否达到了“启动新线程”的目的。
  3. 解释错误原因。
  4. 修正代码。

继续分析:

MyThread thread = new MyThread();

thread.start();

thread.start();

要求:

  1. 判断第一次 start() 是否正常。
  2. 判断第二次调用会发生什么。
  3. 给出正确设计思路。

7.5 综合训练

设计一个简单的“双任务系统”。

需求:

任务 A:
输出“正在读取文件”,循环 20 次

任务 B:
输出“正在处理数据”,循环 20 次

main:
输出“主程序继续运行”,循环 20 次

要求:

  • FileThread extends Thread
  • CalculateThread extends Thread
  • 分别重写 run()
  • 创建两个线程对象
  • 使用 start() 启动
  • main 自己继续执行循环
  • 连续运行程序至少三次
  • 对比三次输出顺序
  • 用自己的语言解释为什么每次顺序可能不同

暂时不要使用:

Runnable
Callable
线程池
synchronized
Lock

这些属于后续章节。


7.6 本章验收

在不查看资料的情况下完成以下内容:

  1. 默写继承 Thread 创建线程的完整四步流程。
  2. 手写一个完整可运行的 Thread 子类。
  3. 解释 run() 的职责。
  4. 解释 start() 的职责。
  5. 画图说明:
thread.run()

与:

thread.start()

的执行模型区别。

  1. 解释为什么多线程输出顺序不能依赖一次运行结果。
  2. 解释为什么同一个线程对象不能启动两次。
  3. 说出继承 Thread 的一个优点和一个核心缺点。
  4. 能够独立 Debug “把 start() 错写成 run()”的问题。

如果以上内容可以闭卷完成,就可以进入下一章:

Runnable 创建线程