6. Runnable:线程和任务到底有什么区别
发布于 • 阅读量 0
前面几节我一直在用 Thread 写代码。
比如:
Thread thread = new Thread(() -> {
// 处理 PDF
}, "pdf-thread-1");
thread.start();
这种写法已经能跑起来了,但这里其实有一个更重要的概念:线程和任务不是一回事。
刚开始看代码时,很容易把它们混在一起。
我以前也会下意识觉得,Thread 里面写了代码,那 Thread 就是任务。后来写线程池的时候才发现,线程池真正接收的并不是线程,而是任务。
这个任务,最常见的表示方式就是 Runnable。
Thread 是执行者,Runnable 是任务
我现在更愿意这样理解:
Thread:负责执行任务的线程;
Runnable:描述这个线程要执行什么任务。
比如我要处理一个 PDF。
“处理 PDF”这件事本身是任务。
而“由哪个线程去处理”,这是执行问题。
如果用生活里的说法,可以理解成:
Runnable 是活;
Thread 是干活的人。
代码里也是一样。
Runnable task = () -> {
System.out.println("开始处理 PDF:" + Thread.currentThread().getName());
};
这里的 task 只是描述了一段要执行的逻辑。
它自己不会自动执行。
真正让它跑起来,需要交给线程:
Thread thread = new Thread(task, "pdf-thread-1");
thread.start();
这时 pdf-thread-1 才会去执行 task 里面的代码。
先把任务单独拆出来
新建一个类:
com.succos.thread.RunnableDemo
代码如下:
package com.succos.thread;
public class RunnableDemo {
public static void main(String[] args) {
System.out.println("main 开始:" + Thread.currentThread().getName());
Runnable task = () -> {
System.out.println("任务开始执行:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
System.out.println("任务执行完成:" + Thread.currentThread().getName());
};
Thread thread = new Thread(task, "pdf-thread-1");
thread.start();
System.out.println("main 继续执行:" + Thread.currentThread().getName());
}
}
这段代码和之前直接写 new Thread(() -> {...}) 的效果差不多。
区别只是我把任务先单独定义成了:
Runnable task = () -> {
// 任务内容
};
然后再交给线程:
Thread thread = new Thread(task, "pdf-thread-1");
这样写以后,任务和线程的边界会更清楚。
Runnable 不等于马上执行
这里有一个点要注意。
当我写下:
Runnable task = () -> {
System.out.println("任务开始执行");
};
这段代码并不会马上执行。
它只是创建了一个任务对象。
真正执行是在这里:
thread.start();
start() 启动线程以后,新线程会去执行 Runnable 里的 run() 方法。
所以这段代码的执行流程大概是:
main 线程创建 Runnable 任务;
main 线程创建 Thread,并把 Runnable 交给 Thread;
main 线程调用 thread.start();
pdf-thread-1 启动;
pdf-thread-1 执行 Runnable 里的代码。
这就是为什么我说:
Runnable 是任务描述,不是任务执行。
这个点后面看线程池的时候很重要。
因为线程池里的 execute() 方法接收的也是 Runnable:
executor.execute(task);
它的意思不是“创建一个线程”,而是“提交一个任务,让线程池里的线程去执行”。
Lambda 写法还原一下
前面的写法用了 Lambda:
Runnable task = () -> {
System.out.println("任务开始执行");
};
如果不写 Lambda,它其实等价于匿名内部类:
Runnable task = new Runnable() {
@Override
public void run() {
System.out.println("任务开始执行");
}
};
也就是说,Runnable 本质上就是一个接口:
public interface Runnable {
void run();
}
它里面只有一个方法:
void run();
所以 Lambda 才能这样写。
我现在理解 Lambda 时,一般不会把它想得太神秘。它只是把“实现一个只有一个方法的接口”写得更短一点。
下面这两种写法,本质上是在表达同一件事:
Runnable task = new Runnable() {
@Override
public void run() {
System.out.println("处理 PDF");
}
};
和:
Runnable task = () -> {
System.out.println("处理 PDF");
};
只是第二种更简洁。
用 Runnable 处理一个 PDF
现在把任务换成真正的 PDF 处理逻辑。
package com.succos.thread;
import com.succos.dto.FileItemContext;
import com.succos.service.PdfWatermarkService;
public class RunnablePdfDemo {
public static void main(String[] args) {
Runnable pdfTask = () -> {
System.out.println("开始处理 PDF:" + Thread.currentThread().getName());
try {
FileItemContext fileItemContext = new FileItemContext();
fileItemContext.setSourcePath("input/test1.pdf");
fileItemContext.setTargetPath("output/test1-watermark.pdf");
fileItemContext.setWaterMakeText("上下文网");
PdfWatermarkService service = new PdfWatermarkService();
service.addWaterMakerOfPDF(fileItemContext);
System.out.println("PDF 处理完成:" + Thread.currentThread().getName());
} catch (Exception e) {
e.printStackTrace();
}
};
Thread thread = new Thread(pdfTask, "pdf-thread-1");
thread.start();
System.out.println("main 继续执行:" + Thread.currentThread().getName());
}
}
这里的结构就比较清楚了:
pdfTask:描述 PDF 怎么处理;
pdf-thread-1:负责执行这个任务。
这个拆分看起来只是写法上的变化,但对后面理解线程池很关键。
因为线程池的核心不是“你自己创建线程”,而是:
你提交任务;
线程池安排线程执行任务。
同一个 Runnable 可以交给多个 Thread 吗?
可以。
比如:
Runnable task = () -> {
System.out.println("执行任务:" + Thread.currentThread().getName());
};
Thread thread1 = new Thread(task, "pdf-thread-1");
Thread thread2 = new Thread(task, "pdf-thread-2");
thread1.start();
thread2.start();
这表示同一个任务逻辑,被两个线程各执行一遍。
运行后可能输出:
执行任务:pdf-thread-1
执行任务:pdf-thread-2
不过这里要注意:如果 Runnable 里面访问了共享变量,就可能出现线程安全问题。
比如:
private static int count = 0;
Runnable task = () -> {
count++;
};
如果多个线程同时执行这个任务,count++ 就可能出问题。
所以 Runnable 可以复用,但复用时要看里面有没有共享状态。
如果每个线程处理的是不同 PDF,最好每个任务里用自己的局部变量,不要把任务状态都放到共享成员变量里。
Runnable 没有返回值
Runnable 有一个限制:它没有返回值。
它的方法签名是:
void run();
所以如果我只是想执行一个任务,比如处理 PDF、写日志、调用接口,用 Runnable 很自然。
但是如果我想拿到任务结果,比如:
PDF 是否处理成功;
生成后的文件路径是什么;
失败原因是什么;
处理耗时是多少。
那 Runnable 就不太方便了。
后面会学到 Callable 和 Future。
Callable 和 Runnable 的区别可以先简单记一下:
Runnable:执行任务,没有返回值;
Callable:执行任务,有返回值。
比如线程池里:
executor.execute(runnableTask);
适合只执行任务。
而:
Future<String> future = executor.submit(callableTask);
适合执行完以后还要拿结果。
这一节先不用展开,后面到 Future 时再细说。
Runnable 不能直接抛受检异常
还有一个细节。
Runnable 的 run() 方法没有声明 throws Exception:
public interface Runnable {
void run();
}
所以在 Lambda 里如果调用了会抛受检异常的方法,就需要自己处理。
比如 PDF 水印方法如果是:
service.addWaterMakerOfPDF(fileItemContext);
并且它会抛异常,那在 Runnable 里通常要这样写:
Runnable task = () -> {
try {
service.addWaterMakerOfPDF(fileItemContext);
} catch (Exception e) {
e.printStackTrace();
}
};
不能直接写成:
Runnable task = () -> {
service.addWaterMakerOfPDF(fileItemContext);
};
如果这个方法抛的是受检异常,编译就过不了。
这个限制也是后面 Callable 更方便的原因之一。
Callable 的 call() 方法可以抛异常,也可以返回结果。
为什么线程池接收 Runnable
前面这些内容,其实是在给线程池做铺垫。
线程池的基本用法是:
executor.execute(() -> {
System.out.println("处理 PDF");
});
这里传进去的 Lambda,本质上就是一个 Runnable。
线程池内部会有一批线程,这些线程不需要我们每次手动创建。
我们只需要不断提交任务:
~~~text
提交任务1;
提交任务2;
提交任务3;