什么时候用 Runnable?什么时候用 Callable ?

公众号「古时的风筝」,专注于后端技术,尤其是 Java 及周边生态。

个人博客:www.moonkite.cn

大家好,我是风筝

提到 Java 就不得不说多线程了,就算你不想说,面试官也得让你说呀,对不对。那说到多线程,就不得提线程了(这不废话吗)。那说到线程,就不得不说RunnableCallable这两个家伙了。

说熟悉也是真熟悉,在刚学习多线程的时候,第一个例子大概就是下面这样子的。

java 复制代码
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("执行线程" + Thread.currentThread().getName());
    }
}).start();

看到了 Runnable的身影,有时候还会看到Callable的。

但是说很熟悉吧,印象也不是很大,好像就用了一下这两位的名号,然后剩下的部分就跟他俩没啥关系了。

今天,我们就来看看这两位到底是什么,有什么区别,什时候应该用 Runnable,什么时候又应该用 Callable

Runnable

自从Java诞生,Runnable就存在了,元老中的元老了,在 1.5之前,如果你想使用线程,那必须要实现自 Runnable。因为到了 JDK1.5,JDK 才加入了Callable

java 复制代码
@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

是不是接口非常简单,就一个抽象方法。

其实还可以再简化一下, @FunctionalInterface标明这个接口是一个函数式接口。函数式接口是在 JDK8才加入的,为的就是实现函数式编程,就那种Lambada表达式。

所以在 JDK1.7中,是没有@FunctionalInterface修饰的,简简单单。我们找到 JDK1.7的 Ruunable实现,是下面这样子

java 复制代码
public interface Runnable {
    public abstract void run();
}

想了解更多函数式编程的话,可以看这篇文章:Lambda、函数式接口、Stream 一次性全给你

如果一个线程类要实现 Runnable 接口,则这个类必须定义一个名为 run 的无参数方法。

实现了 Runnable 接口的类可以通过实例化一个 Thread 实例,并将自身作为目标传递来运行。

举个例子

首先定义一个RunnableThread类,并实现自(implements)Runnable接口,然后重写 run方法。

java 复制代码
public class RunnableThread implements Runnable{
    @Override
    public void run() {
        System.out.println("当前线程名称"+ Thread.currentThread().getName());
    }
}

使用RunnableThread作为线程类(Thread)实例化的参数,然后调用run方法。

java 复制代码
RunnableThread runnableThread = new RunnableThread();
Thread thread = new Thread(runnableThread);
thread.start();

注意,是调用新 new 出来的 Thread 实例的start() 方法,不要调用run方法,虽然我们是重写Runnablerun方法的。调用 run方法并没有创建线程的效果,而是直接在当前线程执行,就和执行一个普通类的普通方法一模一样。

为什么要调用 start()方法呢,我们看看 Threadstart()方法实现中,其实是调用了一个名称为 start0()的 native 方法,native 方法就不是用 Java 实现的了,而是在 JVM 层面的实现。

这个start0方法的主要逻辑就是启动一个操作系统线程,并和 JVM 线程绑定,开辟一些空间来存储线程状态和上下文的数据,然后执行绑定的 JVM 线程(也就是我们实现了Runnable的类)的 run方法的代码块,从而执行我们自定义的逻辑。

还可以用线程池的方式调用

java 复制代码
ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
        .setNameFormat("thread-pool-%d").build();
ExecutorService singleThreadPool = new ThreadPoolExecutor(1, 1,
        0L, TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue<Runnable>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());

singleThreadPool.execute(runnableThread);
singleThreadPool.shutdown();

如果 Runnable那么完美的话,就没必要在 JDK1.5中加入和它超级相似的 Callable了。

Runnable有什么不完美的地方吗?就是它的 run方法是没有返回值的。

如果你想在主线程中拿到新开启线程的返回值的话,Runnable就不太方便了。必须要借助共享变量来完成。

所以,如果你的场景是要有返回值的话, 就要 Callable出手了。

Callable

Callable是在 JDK1.5才加入的,为的就是弥补 Runnable没有返回值的缺陷,虽然绝大多数场景都可以用 Runnable来实现。

java 复制代码
@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

Runnable类似的,@FunctionalInterface也是后来加入的,可以不考虑,只是为了函数式写法。

Callable接口只有一个 call方法,并且有一个泛型返回值,可以返回任何类型。

举个例子

首先声明一个类,实现自 Callable接口,返回值为字符串类型

java 复制代码
public class CallableThread implements Callable<String> {
    @Override
    public String call() throws Exception {
        return "线程名称:" + Thread.currentThread().getName();
    }
}

在代码中通过下面的方式调用

java 复制代码
CallableThread callableThread = new CallableThread();
FutureTask<String> futureTask = new FutureTask<>(callableThread);
Thread thread = new Thread(futureTask);
thread.start();
String result = futureTask.get();
System.out.println("执行结果= " + result);

看上去就比 Runnable要复杂一点,要借助FutureTask了,因为 Thread类没有接受Callable的构造函数。

使用 FutureTask.get()方法获取执行结果。

在日常的开发中,不建议直接这样用,除非你明确的知道这样做没有问题,否则的话,推荐使用线程池的方式来使用。

java 复制代码
CallableThread callableThread = new CallableThread();
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(callableThread);
String result = future.get();
System.out.println("任务执行结果: " + result);
executor.shutdown();

如何选择用哪一个

取舍的基本原则就是需不需要返回值,如果不需要返回值,那直接就选 Runnable,不用犹豫。如果有返回值的话,那更不用犹豫,不要想着借助共享变量的方式。

另外还有一点就是是否需要抛出异常, Runnable是不接受抛出异常的,Callable可以抛出异常。

Runnable适合那种纯异步的处理逻辑。比如每天定时计算报表,将报表存储到数据库或者其他地方,只是要计算,不需要马上展示,展示内容是在其他的方法中单独获取的。

比如那些非核心的功能,当核心流程执行完毕后,非核心功能就自己去执行吧,至于成不成功的,不是特别重要。例如一个购物下单流程,下单、减库存、加到用户的订单列表、扣款是核心功能,之后的发送APP通知、短信通知这些就启动新线程去干去吧。

最后

Runnablejava.lang这个包下,而当JDK1.5发布的时候,新加入的 Callable被安置在了 java.util.concurrent这个包下,这是 Java 里有名的并发编程相关包,各种锁啊、多线程工具类啊,都被放在这个包下。按道理,Runnable 也应该在这里才对。

可见再厉害的项目也是随着项目的扩大而慢慢的规划,而前期的一些看似不太合理的地方,只能做兼容和妥协。

推荐阅读

我的第一个 Chrome 插件上线了,欢迎试用!

前端同事最讨厌的后端行为,看看你中了没有

RPC框架的核心到底是什么

相关推荐
程序媛小果3 分钟前
基于java+SpringBoot+Vue的宠物咖啡馆平台设计与实现
java·vue.js·spring boot
追风林8 分钟前
mac m1 docker本地部署canal 监听mysql的binglog日志
java·docker·mac
芒果披萨23 分钟前
El表达式和JSTL
java·el
许野平1 小时前
Rust: 利用 chrono 库实现日期和字符串互相转换
开发语言·后端·rust·字符串·转换·日期·chrono
duration~1 小时前
Maven随笔
java·maven
zmgst1 小时前
canal1.1.7使用canal-adapter进行mysql同步数据
java·数据库·mysql
跃ZHD1 小时前
前后端分离,Jackson,Long精度丢失
java
blammmp2 小时前
Java:数据结构-枚举
java·开发语言·数据结构
独行soc2 小时前
#渗透测试#SRC漏洞挖掘#深入挖掘XSS漏洞02之测试流程
web安全·面试·渗透测试·xss·漏洞挖掘·1024程序员节
暗黑起源喵2 小时前
设计模式-工厂设计模式
java·开发语言·设计模式