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

作者: 风筝 / 2023-08-07

Callable, Java, JDK, Runnable, 多线程

大家好,我是风筝。

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

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

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

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

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

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

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

public interface Runnable {
    public abstract void run();
}

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

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

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

举个例子

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

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

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

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方法的代码块,从而执行我们自定义的逻辑。

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

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来实现。

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

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

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

举个例子

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

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

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

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()方法获取执行结果。

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

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 也应该在这里才对。

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

相关文章

终于知道公钥、私钥、对称、非对称加密是什么了
什么是零拷贝|Java中的零拷贝
RPC框架的核心是什么?
0. Spring Cloud 是什么
1. Spring Cloud Eureka 实现服务注册与发现
风筝

作者

风筝

古时的风筝,一个平庸的程序员,主语言 Java,第二语言 Python,其实学 Python 的时间比 Java 还要早。喜欢写博客,写博客的过程能加深自己对一个知识点的理解,同时还可以分享给他人。喜欢做一些小东西,所以也会一些前端的东西,React、JavaScript、CSS 都会一些,做一些小工具还够用。