什么时候用 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框架的核心到底是什么

相关推荐
DuelCode36 分钟前
Windows VMWare Centos Docker部署Springboot 应用实现文件上传返回文件http链接
java·spring boot·mysql·nginx·docker·centos·mybatis
优创学社240 分钟前
基于springboot的社区生鲜团购系统
java·spring boot·后端
why技术44 分钟前
Stack Overflow,轰然倒下!
前端·人工智能·后端
幽络源小助理1 小时前
SpringBoot基于Mysql的商业辅助决策系统设计与实现
java·vue.js·spring boot·后端·mysql·spring
猴哥源码1 小时前
基于Java+springboot 的车险理赔信息管理系统
java·spring boot
YuTaoShao1 小时前
【LeetCode 热题 100】48. 旋转图像——转置+水平翻转
java·算法·leetcode·职场和发展
ai小鬼头2 小时前
AIStarter如何助力用户与创作者?Stable Diffusion一键管理教程!
后端·架构·github
Dcs2 小时前
超强推理不止“大”——手把手教你部署 Mistral Small 3.2 24B 大模型
java
简佐义的博客2 小时前
破解非模式物种GO/KEGG注释难题
开发语言·数据库·后端·oracle·golang
东阳马生架构2 小时前
订单初版—1.分布式订单系统的简要设计文档
java