Runnable 创建线程 | JavaSE
Runnable 创建线程
一、学习目标
完成本章后,你应该能够:
- 解释
Runnable接口的作用,以及它与Thread的职责区别。 - 独立使用“实现
Runnable接口”的方式创建并启动线程。 - 理解“线程(Thread)”与“线程任务(Runnable)”分离的设计思想。
- 掌握普通实现类、匿名内部类、Lambda 三种 Runnable 写法。
- 理解为什么 Runnable 相比继承 Thread 具有更好的扩展性。
- 理解同一个 Runnable 任务对象可以交给多个 Thread 对象执行。
- 理解 Runnable 的局限:
run()没有返回值,也不能在方法签名中直接声明抛出受检异常。 - 能够比较
Thread与Runnable两种创建线程方式。
二、核心知识
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 知识问答
- Runnable 是什么?
- Runnable 的核心方法是什么?
- Runnable 是线程还是任务?
- Thread 与 Runnable 分别承担什么职责?
- 实现 Runnable 创建线程需要哪四个步骤?
- 为什么还需要
new Thread(task)? - 为什么最终仍然需要调用
start()? - Runnable 相比继承 Thread 最大的扩展性优势是什么?
- 为什么 Runnable 更符合面向接口编程思想?
- Runnable 为什么可以使用 Lambda?
- 一个 Runnable 对象能否交给多个 Thread?
- 多线程共享一个 Runnable 对象中的字段可能引出什么问题?
- Runnable 的
run()能否直接返回计算结果? - Runnable 与 Callable 最值得关注的区别是什么?
- 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);
}
}
}
回答:
- 创建了几个 Runnable 对象?
- 创建了几个 Thread 对象?
- 启动了几条新的线程?
- 两条线程执行的是不是同一个任务对象?
"main"一定最后输出吗?- 两个线程之间的输出顺序能否确定?
阅读:
Runnable task = () -> {
System.out.println("Hello");
};
Thread thread = new Thread(task);
thread.start();
回答:
- Lambda 的目标类型是什么?
- Lambda 最终对应 Runnable 中哪个方法?
- 真正创建线程的是哪个对象?
- 真正启动线程的是哪一句?
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();
判断:
- 能否通过编译?
- Runnable 要求实现什么方法?
- 应该如何修改?
继续分析:
Runnable task = new MyTask();
task.run();
要求:
- 是否会执行任务代码?
- 是否创建了新的线程?
- 当前任务由谁执行?
- 正确创建新线程应该怎样写?
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。