• 面试必备:Thread、Runnable、Callable、Future、FutureTask,谈谈他们的关系?(荣耀典藏版)


    目录

    前言

    1. Thread 和 Runnable

    1.1 Thread

    1.2 Runnable

    1.3 Thread 和 Runnable 的关系

    2. Callable 、Future 和 FutureTask

    2.1 Callable

    2.2 FutureTask

    2.3 Callable 和 FutureTask 的关系

    2.4 Future

    3. 具体实例


     

    前言

    大家好,我是月夜枫,我又来了!!!

    Thread、Runnable、Callable、Future、FutureTask,你能详细讲出他们的内部关系么?这也是面试经常问到的问题。

    这篇文章主要告诉大家各种对象内部的关系,能达到灵活运用的境界,下面是文章目录:

     

    1. Thread 和 Runnable

    1.1 Thread

    我们先看一下 Thread 最简单的使用姿势:

    1. public class MyThread extends Thread {
    2.     public MyThread(String name) {
    3.         super(name);
    4.     }
    5.     @Override
    6.     public void run() {
    7.         String name = Thread.currentThread().getName();
    8.         System.out.println(name + "已经运行");
    9.     }
    10.     public static void main(String[] args) {
    11.         new MyThread("线程一").start();
    12.     }
    13. }

    线程包含 4 个状态:创建 -> 就绪 -> 运行 -> 结束。

    当执行 start() 后,线程进入就绪状态,当对应的线程抢占到 cpu 调度资源之后,进入运行状态,此时调用的是 run 方法,执行完毕之后就结束了。

    1.2 Runnable

    我们看一下 Runnable 最简单的使用姿势:

    1. public class MyTask implements Runnable {
    2.     @Override
    3.     public void run() {
    4.         String name = Thread.currentThread().getName();
    5.         System.out.println(name + "已经运行");
    6.     }
    7.     public static void main(String[] args) {
    8.         new Thread(new MyTask(),"线程二").start();
    9.     }
    10. }

    这里 MyTask 就是一个 Runnable,实现了 run() 方法,作为 Thread() 的入参。

    基本所有同学都知道这样使用,但是你们知道原理么?

    1.3 Thread 和 Runnable 的关系

    我们看一下 Runnable 的接口定义:

    1. public interface Runnable {
    2.     /**
    3.      * When an object implementing interface Runnable is used
    4.      * to create a thread, starting the thread causes the object's
    5.      * run method to be called in that separately executing
    6.      * thread.
    7.      * 

    8.      * The general contract of the method run is that it may
    9.      * take any action whatsoever.
    10.      *
    11.      * @see     java.lang.Thread#run()
    12.      */
    13.     public abstract void run();
    14. }

    英文翻译大致如下:当一个对象继承并实现了 run() 方法,当线程 start() 后,会在该线程中单独执行该对象的 run() 方法。

    这段翻译,基本就告诉了 Runnable 和 Thread 的关系:

    1. MyTask 继承 Runnable,并实现了 run() 方法;

    2. Thread 初始化,将 MyTask 作为自己的成员变量;

    3. Thread 执行 run() 方法,线程处于“就绪”状态;

    4. 等待 CPU 调度,执行 Thread 的 run() 方法,但是 run() 的内部实现,其实是执行的 MyTask.run() 方法,线程处于“运行”状态。

    这里面的第2、4步,需要对照着源码看看。

    在 Thread 初始化时,MyTask 作为入参 target,最后赋值给 Thread.target

     当执行 Thread.run() 时,其实是执行的 target.run(),即 MyTask.run(),这个是典型的策略模式

     

    2. Callable 、Future 和 FutureTask

    先看一下它们的整体关系图谱:

     

    我刚开始看到这幅图,感觉 Java 真是麻烦,已经有了 Thread 和 Runnable 这两种创建线程的方式,为啥又搞这 3 个东西呢?

    其实对于 Thread 和 Runable,其 run() 都是无返回值的,并且无法抛出异常,所以当你需要返回多线程的数据,就需要借助 Callable 和 Future。

    2.1 Callable

    Callable 是一个接口,里面有个 V call() 方法,这个 V 就是我们的返回值类型:

    1. public interface Callable {
    2.     /**
    3.      * Computes a result, or throws an exception if unable to do so.
    4.      *
    5.      * @return computed result
    6.      * @throws Exception if unable to compute a result
    7.      */
    8.     V call() throws Exception;
    9. }

    我们一般会用匿名类的方式使用 Callable,call() 中是具体的业务逻辑:

    1. Callable callable = new Callable() {
    2.     @Override
    3.     public String call() throws Exception {
    4.         // 执行业务逻辑 ...
    5.         return "this is Callable is running";
    6.     }
    7. };

    这里抛出一个问题,这个 callable.call() 和 Thread.run() 是什么关系呢?

    2.2 FutureTask

    通过关系图谱,FutureTask 继承了 RunnableFuture,RunnableFuture 继承了 Runnable 和 Future:

    1. public interface RunnableFuture extends Runnable, Future {
    2.     /**
    3.      * Sets this Future to the result of its computation
    4.      * unless it has been cancelled.
    5.      */
    6.     void run();
    7. }

    所以,FutureTask 也是个 Runnable !!!

    这里就有点意思了,既然 FutureTask 是个 Runnable,肯定就需要实现 FutureTask.run() 方法,那么 FutureTask 也可以作为 Thread 的初始化入参,使用姿势如下:

    new Thread(FutureTask对象).start();
    

    所以当执行 Thread.run() 时,其实是执行的 FutureTask.run(),这个是我们破解的第一层。

    下面我们再破解 FutureTask.run() 和 Callable.call() 的关系。

    2.3 Callable 和 FutureTask 的关系

    FutureTask 初始化时,Callable 必须作为 FutureTask 的初始化入参:

    当执行 FutureTask.run() 时,其实执行的是 Callable.call(): 

     

    所以,这里又是一个典型的策略模式 !!!

    现在我们应该可以很清楚知道 Thread 、Runnable、FutureTask 和 Callable 的关系:

    • Thread.run() 执行的是 Runnable.run();

    • FutureTask 继承了 Runnable,并实现了 FutureTask.run();

    • FutureTask.run() 执行的是 Callable.run();

    • 依次传递,最后 Thread.run(),其实是执行的 Callable.run()。

    所以整个设计方法,其实就是 2 个策略模式,Thread 和 Runnable 是一个策略模式,FutureTask 和 Callable 又是一个策略模式,最后通过 Runnable 和 FutureTask 的继承关系,将这 2 个策略模式组合在一起。

    嗯嗯。。。我们是不是把 Future 给忘了~~

    2.4 Future

    为什么要有 Future 呢?我再问一个问题,大家可能就知道了。

    我们通过 FutureTask,借助 Thread 执行线程后,结果数据我们怎么获取到呢?这里就需要借助到 Future。

    我们看一下 Future 接口:

    1. public interface Future {
    2.     // 取消任务,如果任务正在运行的,mayInterruptIfRunning为true时,表明这个任务会被打断的,并返回true;
    3.     // 为false时,会等待这个任务执行完,返回true;若任务还没执行,取消任务后返回true,如任务执行完,返回false
    4.     boolean cancel(boolean mayInterruptIfRunning);
    5.     // 判断任务是否被取消了,正常执行完不算被取消
    6.     boolean isCancelled();
    7.     // 判断任务是否已经执行完成,任务取消或发生异常也算是完成,返回true
    8.     boolean isDone();
    9.     // 获取任务返回结果,如果任务没有执行完成则等待完成将结果返回,如果获取的过程中发生异常就抛出异常,
    10.     // 比如中断就会抛出InterruptedException异常等异常
    11.     V get() throws InterruptedException, ExecutionException;
    12.     // 在规定的时间如果没有返回结果就会抛出TimeoutException异常
    13.     V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
    14. }

    对于 FutureTask,Callable 就是他的任务,而 FutureTask 内部维护了一个任务状态,所有的状态都是围绕这个任务来进行的,随着任务的进行,状态也在不断的更新。

    FutureTask 继承了 Future,实现对任务的取消、数据获取、任务状态判断等功能。

    比如我们经常会调用 get() 方法获取数据,如果任务没有执行完成,会将当前线程放入阻塞队列等待,当任务执行完后,会唤醒阻塞队列中的线程。

    3. 具体实例

    1. private static List processByMultiThread(Integer batchSize) throws ExecutionException, InterruptedException {
    2.     List output = new ArrayList<>();
    3.     // 获取分批数据
    4.     List> batchProcessData = getProcessData(batchSize);
    5.     // 启动线程
    6.     List>> futureTaskList = new ArrayList<>();
    7.     for (List processData : batchProcessData) {
    8.         Callable> callable = () -> processOneThread(processData);
    9.         FutureTask> futureTask = new FutureTask<>(callable);
    10.         new Thread(futureTask).start();  // 启动线程
    11.         futureTaskList.add(futureTask);
    12.     }
    13.     // 获取线程返回的数据
    14.     for (FutureTask futureTask : futureTaskList) {
    15.         List processData = (List) futureTask.get();
    16.         output.addAll(processData);
    17.     }
    18.     return output;
    19. }

    这个示例很简单:

    1. 先将数据按照 batchSize 分成 N 批;

    2. 启动 N 个线程,去执行任务;

    3. 通过 futureTask.get() 获取每个线程数据,并汇总输出。

    这个示例其实不太适合线上的场景,因为每次调用都会初始化线程,如果调用过多,内存可能会被撑爆,需要借助线程池。

    完整的示例代码,以及线程池使用姿势,详见github:https://github.com/lml200701158/java-study/blob/master/src/main/java/com/java/parallel/share/MultiThreadProcess.java

    最后说一句(求关注,别白嫖我)

    如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下,您的支持是我坚持写作最大的动力。

    求一键三连:点赞、转发、在看。

  • 相关阅读:
    基于图神经网络的图像分类,遥感图像分析
    2022年10月总结 (距离激动人心的928已经过去一个多月了)
    GaussDB SQL基础语法示例-BOOLEAN表达式
    画中画视频剪辑:批量制作画中画视频,让视频更具吸引力和创意
    SpringBoot项目的搭建
    【LeetCode】2760. 最长奇偶子数组
    21天学Python --- 打卡5:Python && Threading
    24. 两两交换链表中的节点
    51-43 DragNUWA,集成文本、图像和轨迹实现视频生成细粒度控制
    极客时间Kafka - 04 Kafka生产者和消费者拦截器
  • 原文地址:https://blog.csdn.net/weixin_48321993/article/details/126381766