Runnable 创建线程 | JavaSE

Runnable 创建线程

一、学习目标

完成本章后,你应该能够:

  • 解释 Runnable 接口的作用,以及它与 Thread 的职责区别。
  • 独立使用“实现 Runnable 接口”的方式创建并启动线程。
  • 理解“线程(Thread)”与“线程任务(Runnable)”分离的设计思想。
  • 掌握普通实现类、匿名内部类、Lambda 三种 Runnable 写法。
  • 理解为什么 Runnable 相比继承 Thread 具有更好的扩展性。
  • 理解同一个 Runnable 任务对象可以交给多个 Thread 对象执行。
  • 理解 Runnable 的局限:run() 没有返回值,也不能在方法签名中直接声明抛出受检异常。
  • 能够比较 ThreadRunnable 两种创建线程方式。

二、核心知识

2.1 为什么还需要 Runnable

上一章已经学习了:

class MyThread extends Thread {

    @Override
    public void run() {
        // 线程任务
    }
}

然后:

MyThread thread = new MyThread();
thread.start();

这种方式非常直观。

但是它存在一个明显问题:

MyThread
   ↓
已经继承 Thread

Java 类只支持:

单继承

因此:

class MyThread extends Thread

以后就不能再:

extends 其他业务父类

例如下面这种写法是不允许的:

class Person {
}

class MyThread extends Person, Thread {
}

Java 不支持类的多继承。

所以需要思考:

线程要执行的任务,真的必须通过“继承 Thread”来描述吗?

答案是不需要。

Java 提供了另一个非常重要的接口:

java.lang.Runnable

它专门用于:

描述一段需要被执行的任务。

于是我们可以把:

线程

和:

线程执行的任务

分离开来。


2.2 Runnable 是什么

Runnable 是一个函数式接口。

它的核心结构可以理解为:

@FunctionalInterface
public interface Runnable {

    void run();
}

最核心的方法只有:

void run();

因此 Runnable 表达的是:

一个可以被执行、但是没有返回结果的任务。

例如:

class DownloadTask implements Runnable {

    @Override
    public void run() {
        System.out.println("正在下载文件");
    }
}

这里:

DownloadTask

并不是线程。

它只是:

一个任务

真正代表线程的依然是:

Thread

于是 Runnable 模式的核心思想就形成了:

Runnable
   ↓
描述“做什么”

Thread
   ↓
负责“谁来执行”

2.3 Thread 与 Runnable 的职责分离

这是本章最重要的思想。

继承 Thread 时:

MyThread
├── 是线程
└── 又保存任务

两个职责耦合到了一起。

而 Runnable 模式:

MyRunnable
    ↓
只描述任务

Thread
    ↓
负责线程执行

形成:

任务
与
执行任务的线程
分离

例如:

Runnable task = new MyRunnable();

Thread thread = new Thread(task);

thread.start();

可以把它画成:

MyRunnable
   │
   │ 描述任务
   ▼
 run()
   │
   │ 交给
   ▼
Thread
   │
   │ start()
   ▼
新线程执行任务

这种思想对于后面的:

Callable
线程池
ExecutorService

都非常重要。

以后 Java 并发编程会越来越强调:

提交任务,而不是自己管理每一个线程。


2.4 Runnable 创建线程的四个步骤

标准流程:

第一步
定义类实现 Runnable

        ↓

第二步
重写 run()

        ↓

第三步
创建 Runnable 任务对象
并交给 Thread

        ↓

第四步
调用 Thread.start()

代码:

class MyRunnable implements Runnable {

    @Override
    public void run() {
        System.out.println("子线程执行任务");
    }
}

创建任务:

Runnable task = new MyRunnable();

创建线程:

Thread thread = new Thread(task);

启动:

thread.start();

完整代码:

public class RunnableDemo {

    public static void main(String[] args) {

        Runnable task = new MyRunnable();

        Thread thread = new Thread(task);

        thread.start();

        for (int i = 0; i < 5; i++) {
            System.out.println("main线程:" + i);
        }
    }
}

class MyRunnable implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程:" + i);
        }
    }
}

调用:

thread.start();

后:

main线程
    │
    ├─────────────┐
    │             │
继续执行         新线程
                  │
                  ▼
            task.run()

两个执行流程可以并发推进。


三、使用方法

3.1 方式一:定义 Runnable 实现类

这是最基础、最适合理解原理的写法。

class PrintTask implements Runnable {

    @Override
    public void run() {
        for (int i = 1; i <= 5; i++) {
            System.out.println("任务执行:" + i);
        }
    }
}

然后:

public class Demo {

    public static void main(String[] args) {

        PrintTask task = new PrintTask();

        Thread thread = new Thread(task);

        thread.start();
    }
}

注意对象关系:

task
   ↓
PrintTask 对象

thread
   ↓
Thread 对象

它们不是同一个对象,也承担不同职责。


3.2 一个 Runnable 对象可以交给不同 Thread

例如:

Runnable task = new PrintTask();

Thread t1 = new Thread(task);
Thread t2 = new Thread(task);

t1.start();
t2.start();

此时:

             ┌── Thread t1
             │
Runnable ────┤
             │
             └── Thread t2

也就是说:

一个任务对象,可以被多个线程对象执行。

这正是 Runnable 模式比继承 Thread 更灵活的重要体现。

但是这里也隐藏了一个未来必须解决的问题。

如果:

t1
t2

都访问:

同一个 task 对象中的可变数据

那么这些数据就属于:

共享数据

可能出现线程安全问题。

这个问题将在:

08-06 · 线程安全问题

专门学习。


3.3 方式二:匿名内部类

如果这个 Runnable 任务只使用一次,就没有必要专门创建:

MyRunnable.java

可以使用匿名内部类:

Runnable task = new Runnable() {

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程:" + i);
        }
    }
};

Thread thread = new Thread(task);

thread.start();

甚至可以继续缩写:

new Thread(new Runnable() {

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程:" + i);
        }
    }

}).start();

这种形式表示:

创建 Runnable 匿名实现对象
            ↓
交给 Thread
            ↓
立即调用 start()

3.4 方式三:Lambda 表达式

前面已经学习过:

函数式接口
Lambda

Runnable 恰好是函数式接口。

因此:

new Runnable() {

    @Override
    public void run() {
        System.out.println("Hello");
    }
}

可以简化为:

() -> {
    System.out.println("Hello");
}

所以:

Runnable task = () -> {
    for (int i = 0; i < 5; i++) {
        System.out.println("子线程:" + i);
    }
};

Thread thread = new Thread(task);

thread.start();

还可以进一步:

new Thread(() -> {
    for (int i = 0; i < 5; i++) {
        System.out.println("子线程:" + i);
    }
}).start();

这是实际开发中非常常见的一种写法。


3.5 为什么 Lambda 可以直接传给 Thread

因为:

Thread

存在接收 Runnable 的构造器:

Thread(Runnable task)

而:

Runnable

又是函数式接口。

因此:

new Thread(() -> {
    System.out.println("执行任务");
});

编译器可以推断:

() -> {...}

就是一个:

Runnable

对象。

完整关系:

Lambda
   ↓
Runnable 实例
   ↓
Thread(Runnable)
   ↓
thread.start()
   ↓
执行 Runnable.run()

四、原理与进阶

4.1 Thread 为什么能够执行 Runnable

看:

Runnable task = new MyRunnable();

Thread thread = new Thread(task);

thread.start();

这里创建的是:

Thread 对象

而任务是:

Runnable 对象

Thread 在启动之后会执行自身的:

run()

当平台线程是通过:

new Thread(task)

创建时,它的执行逻辑会进一步调用:

task.run();

因此可以建立简化模型:

thread.start()
      ↓
新的线程开始执行
      ↓
Thread.run()
      ↓
Runnable.run()
      ↓
真正任务代码

所以:

new Thread(task)

相当于告诉 Thread:

“你启动以后,请执行这个 Runnable 所描述的任务。”


4.2 Thread 本身其实也实现了 Runnable

Thread 的类型关系中,本身就有:

public class Thread implements Runnable

因此上一章:

class MyThread extends Thread

重写的:

run()

从类型体系上看,也来自 Runnable 的任务模型。

所以:

Thread
与
Runnable

不是毫无关系的两套机制。

Runnable 实际上是 Java 对:

可执行任务

进行抽象的重要接口。


4.3 Runnable 为什么扩展性更强

假设存在业务类:

class Person {

}

现在一个任务类还希望继承:

Person

如果使用 Thread:

class MyTask extends Thread

就不能再:

extends Person

而 Runnable 只是接口:

class MyTask extends Person implements Runnable {

    @Override
    public void run() {

    }
}

完全合法。

因为 Java 支持:

单继承
+
多接口实现

因此 Runnable 在类体系设计上更加灵活。


4.4 Runnable 更符合“面向接口编程”

之前学习接口时已经接触:

面向接口编程。

Runnable 正是一个非常经典的实际案例。

我们不是要求某个任务:

必须继承某个具体业务类

而只是要求:

你能提供 run() 行为即可

于是:

class DownloadTask implements Runnable
class LogTask implements Runnable
class BackupTask implements Runnable

虽然三个类业务完全不同,但都可以作为:

Runnable

处理。

例如:

public static void startTask(Runnable task) {
    new Thread(task).start();
}

那么:

startTask(new DownloadTask());
startTask(new LogTask());
startTask(new BackupTask());

都可以运行。

这就是接口带来的:

统一抽象
+
多态

4.5 Runnable 不能直接返回任务结果

Runnable 的方法定义是:

void run();

所以:

@Override
public void run() {
    int result = 1 + 2;
}

这里虽然能够在任务内部:

计算 result

但是不能写:

return result;

因为:

run()

返回类型是:

void

因此 Runnable 天然适合:

执行某件事情

例如:

  • 写日志;
  • 打印数据;
  • 下载文件;
  • 保存文件;
  • 处理消息;

但是如果需求是:

子线程帮我算出结果,然后主线程还要拿到这个结果。

Runnable 就不够方便了。

下一章的:

Callable

就是为此而来。


4.6 Runnable 与受检异常

Runnable 的:

void run()

没有声明:

throws Exception

所以不能直接:

@Override
public void run() throws Exception {

}

如果任务中调用了会产生受检异常的方法,就需要在 run() 内部自行处理。

例如:

@Override
public void run() {
    try {
        // 可能出现受检异常的代码
    } catch (Exception e) {
        // 处理异常
    }
}

而下一章的:

Callable.call()

可以声明:

throws Exception

这也是 Runnable 与 Callable 的重要区别。


五、实践应用

5.1 下载任务

class DownloadTask implements Runnable {

    @Override
    public void run() {
        for (int i = 1; i <= 5; i++) {
            System.out.println("下载文件块:" + i);
        }
    }
}

启动:

public class DownloadDemo {

    public static void main(String[] args) {

        Runnable task = new DownloadTask();

        Thread thread = new Thread(task);

        thread.start();

        System.out.println("main继续处理其他工作");
    }
}

形成:

main线程
   ↓
继续处理其他工作

下载线程
   ↓
DownloadTask.run()

5.2 日志任务

Runnable logTask = () -> {
    System.out.println("写入操作日志");
};

new Thread(logTask).start();

Runnable 特别适合这种:

只需要执行
不关心返回值

的任务。


5.3 一个任务由多个线程执行

例如:

class TicketTask implements Runnable {

    private int ticket = 100;

    @Override
    public void run() {
        // 多个线程未来会共同处理 ticket
    }
}

然后:

TicketTask task = new TicketTask();

new Thread(task).start();
new Thread(task).start();
new Thread(task).start();

此时:

三个线程
   ↓
共同访问
   ↓
同一个 task.ticket

这就是后续学习:

线程安全
synchronized
Lock

的典型起点。

本章暂时不要解决,只需要认识:

Runnable 可以让多个线程共享同一个任务对象,这既带来灵活性,也带来了共享数据的并发风险。


六、常见问题

6.1 Runnable 是线程吗?

不是。

Runnable task

表示:

任务

而:

Thread thread

表示:

线程

6.2 创建 Runnable 对象以后线程启动了吗?

没有。

new MyRunnable()

只是创建一个任务对象。

还需要:

Thread thread = new Thread(task);
thread.start();

6.3 Thread 和 Runnable 谁负责执行任务?

可以理解为:

Runnable → 负责描述任务
Thread   → 负责承载并执行任务

6.4 Runnable 为什么比继承 Thread 扩展性好?

因为 Runnable 是接口。

任务类:

implements Runnable

以后还可以继续:

extends 其他父类

也可以实现其他接口。

而继承 Thread 已经占用了 Java 唯一的类继承位置。


6.5 Runnable 可以获取返回值吗?

通过 run() 本身不能直接返回结果,因为:

void run()

没有返回值。

如果需要标准化地获得异步任务结果,应学习下一章:

Callable
FutureTask

6.6 Runnable 可以使用 Lambda 吗?

可以。

因为 Runnable 是:

@FunctionalInterface

例如:

new Thread(() -> {
    System.out.println("执行任务");
}).start();

6.7 一个 Runnable 能给多个 Thread 使用吗?

可以。

例如:

Runnable task = new MyRunnable();

new Thread(task).start();
new Thread(task).start();

但如果多个线程共同修改 task 中的数据,需要考虑线程安全。


6.8 Runnable 和 Thread 哪种更推荐?

对于“描述任务”而言,Runnable 通常具有更好的:

职责分离
扩展能力
代码复用能力

但学习 Thread 继承方式仍然非常重要,因为它最直观地帮助理解线程本身。


七、练习与验收

7.1 知识问答

  1. Runnable 是什么?
  2. Runnable 的核心方法是什么?
  3. Runnable 是线程还是任务?
  4. Thread 与 Runnable 分别承担什么职责?
  5. 实现 Runnable 创建线程需要哪四个步骤?
  6. 为什么还需要 new Thread(task)
  7. 为什么最终仍然需要调用 start()
  8. Runnable 相比继承 Thread 最大的扩展性优势是什么?
  9. 为什么 Runnable 更符合面向接口编程思想?
  10. Runnable 为什么可以使用 Lambda?
  11. 一个 Runnable 对象能否交给多个 Thread?
  12. 多线程共享一个 Runnable 对象中的字段可能引出什么问题?
  13. Runnable 的 run() 能否直接返回计算结果?
  14. Runnable 与 Callable 最值得关注的区别是什么?
  15. Runnable 的 run() 为什么不能直接声明 throws Exception

7.2 代码阅读

先不要运行代码:

public class RunnableRead {

    public static void main(String[] args) {

        Runnable task = new MyTask();

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);

        t1.start();
        t2.start();

        System.out.println("main");
    }
}

class MyTask implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println("task:" + i);
        }
    }
}

回答:

  1. 创建了几个 Runnable 对象?
  2. 创建了几个 Thread 对象?
  3. 启动了几条新的线程?
  4. 两条线程执行的是不是同一个任务对象?
  5. "main" 一定最后输出吗?
  6. 两个线程之间的输出顺序能否确定?

阅读:

Runnable task = () -> {
    System.out.println("Hello");
};

Thread thread = new Thread(task);

thread.start();

回答:

  1. Lambda 的目标类型是什么?
  2. Lambda 最终对应 Runnable 中哪个方法?
  3. 真正创建线程的是哪个对象?
  4. 真正启动线程的是哪一句?

7.3 手写代码

任务一:普通 Runnable

从零手写:

PrintTask

要求:

  • 实现 Runnable;
  • 重写 run()
  • 输出 1~10;
  • 创建任务对象;
  • 创建 Thread;
  • 调用 start()

任务二:匿名内部类

不得创建新的 Runnable 实现类。

使用:

new Runnable() {
    ...
}

完成一个线程:

输出 A~Z

任务三:Lambda

将上一题继续改写成:

new Thread(() -> {
    ...
}).start();

要求说明:

为什么这里能够使用 Lambda?


7.4 Debug

下面代码希望创建一个 Runnable 线程:

class MyTask implements Runnable {

    public void start() {
        System.out.println("执行任务");
    }
}
MyTask task = new MyTask();

Thread thread = new Thread(task);

thread.start();

判断:

  1. 能否通过编译?
  2. Runnable 要求实现什么方法?
  3. 应该如何修改?

继续分析:

Runnable task = new MyTask();

task.run();

要求:

  1. 是否会执行任务代码?
  2. 是否创建了新的线程?
  3. 当前任务由谁执行?
  4. 正确创建新线程应该怎样写?

7.5 综合训练

设计三个任务:

DownloadTask
LogTask
BackupTask

全部实现:

Runnable

要求:

DownloadTask → 输出“正在下载”5次
LogTask      → 输出“正在写日志”5次
BackupTask   → 输出“正在备份”5次

分别通过三个 Thread 启动。

然后再把其中一个任务改写为:

匿名内部类

另一个改写为:

Lambda

最后说明三种写法在本质上是不是同一种 Runnable 任务模型。


7.6 本章验收

如果能够闭卷完成以下内容,本章可以认为基本掌握:

  • 默写 Runnable 创建线程的完整四步流程。
  • 手写一个 Runnable 实现类。
  • 不查看资料写出 new Thread(task).start()
  • 解释 Thread 与 Runnable 的职责区别。
  • 解释为什么 Runnable 比继承 Thread 扩展性更好。
  • 使用匿名内部类创建 Runnable 线程。
  • 使用 Lambda 创建 Runnable 线程。
  • 解释一个 Runnable 为什么可以被多个 Thread 使用。
  • 解释 Runnable 为什么不能通过 run() 返回任务结果。
  • 能够自然引出下一章:为什么需要 Callable。