zoukankan      html  css  js  c++  java
  • 创建线程都有哪些方式?— Callable篇

    今天我们来看一道面试题引发的思考

    问: 创建线程都有哪些方式?

    答: 我了解的有四种创建方式:

    1. 继承Thread类创建线程类
    2. 通过Runnable接口创建线程类
    3. 通过Callable和Future创建线程
    4. 通过线程池创建

    相信大家回答这个问题没什么难度吧?通常问完创建方式,那么接下来就是问「1、2」跟「3」创建方式的不同了,只要说出「3」有返回值基本这个问题就过了,不管是出于好奇还是疑惑,我们今天来会会这个Callable。

    实现Runnable接口或者是继承Thread,这两种方式都有个缺点,那就是在线程执行完之后无法获得返回值,所谓的返回值在这举个例子,比如有一个下载功能,下载某个界面的所有图片,图片下载完之后要返回给用户用于展示,如果我们采用方式1、2的话,显然很难实现,所以我们可以采用实现Callbale接口,并用Future接收多线程的执行结果来实现,具体代码如下:

    public class TestCallable {
    
        public static void main(String[] args) {
    
            DownImgThread demo = new DownImgThread();
            FutureTask<List<String>> futureTask = new FutureTask<>(demo);
    
            new Thread(futureTask).start();
    
            try {
                System.out.println("-----开始获取图片-----");
                List<String> imgs = futureTask.get();
                for (String img: imgs) {
                    System.out.println("图片:"+img);
                }
                System.out.println("-----获取图片结束-----");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
    
    }
    
    /**
     * 下载图片的线程DownImgThread
     */
    public class DownImgThread implements Callable<List<String>> {
    
        @Override
        public List<String> call() throws Exception {
            List result = new ArrayList();
            for (int i = 0; i < 10; i++) {
                result.add("https://sscai.club/img/a"+i+".jpg");
            }
            /**
             * 模拟3秒的网络请求操作
             */
            Thread.sleep(3000);
            return result;
        }
    }
    

    执行结果:

    -----开始获取图片-----
    图片:https://sscai.club/img/a0.jpg
    图片:https://sscai.club/img/a1.jpg
    图片:https://sscai.club/img/a2.jpg
    图片:https://sscai.club/img/a3.jpg
    图片:https://sscai.club/img/a4.jpg
    图片:https://sscai.club/img/a5.jpg
    图片:https://sscai.club/img/a6.jpg
    图片:https://sscai.club/img/a7.jpg
    图片:https://sscai.club/img/a8.jpg
    图片:https://sscai.club/img/a9.jpg
    -----获取图片结束-----
    

    通过如上代码我门来看看Callable和Future是如何使用的,首先DownImgThread类(下载图片类)实现了Callable接口,接口后面携带一个泛型T,这个泛型T将用作返回值,我们来看一下 Callable 的接口定义:

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

    如上,Callable接口只有一个 call() 方法,同时 call() 方法有一个返回值 V ,所以上边我们在实现 call() 方法时才能将返回值返回回去。在 call() 方法内部通过一个「for循环+休眠」模拟了网络请求的过程,然后将图片的集合返回。

    我们再回到主线程main方法,整体代码还是比较清晰的,可能小伙伴对FutureTask有点陌生。

    在了解FutureTask之前,我们先来了解一下Future。

    Future

    Future是用来获取异步计算结果的,这个结果是未来的,只有当结果最终处理完成后才会出现在Future中,我们来看一下Future接口的源码。

    public interface Future<V> {
    
        boolean cancel(boolean mayInterruptIfRunning);
    
        boolean isCancelled();
    
        boolean isDone();
    
        V get() throws InterruptedException, ExecutionException;
    
        V get(long timeout, TimeUnit unit)
            throws InterruptedException, ExecutionException, TimeoutException;
    }
    

    解读这5个方法:

    • boolean cancel(boolean mayInterruptIfRunning):如果任务还没开始,执行cancel(...)方法将返回false;如果任务已经启动,执行cancel(true)方法将以中断执行此任务线程的方式来试图停止任务,如果停止成功,返回true;当任务已经启动,执行cancel(false)方法将不会对正在执行的任务线程产生影响(让线程正常执行到完成),此时返回false;当任务已经完成,执行cancel(...)方法将返回false。mayInterruptRunning参数表示是否中断执行中的线程。
    • boolean isCancelled():如果任务完成前被取消,则返回true。
    • boolean isDone():如果任务执行结束,无论是正常结束或是中途取消还是发生异常,都返回true。
    • V get():获取异步执行的结果,如果没有结果可用,此方法会阻塞直到异步计算完成。
    • V get(long timeout, TimeUnit unit):获取异步执行结果,如果没有结果可用,此方法会阻塞,但是会有时间限制,如果阻塞时间超过设定的timeout时间,该方法将抛出异常。

    总之,Future实际上提供了3种功能:

    1. 能够中断执行中的任务;
    2. 判断任务是否执行完成;
    3. 获取任务执行完成后的结果。

    但是我们必须明白Future只是一个接口,我们无法直接创建对象,因此就需要其实现类FutureTask。

    FutureTask

    用FutureTask来实现Future,但是这里并不是直接实现的,我们通过一张图来看一下之间的关系:

    FutureTask代码中对应截图部分:

    public class FutureTask<V> implements RunnableFuture<V> {  
    

    FutureTask实现了RunnableFuture接口,然后我们再来看一下RunnableFuture接口:

    public interface RunnableFuture<V> extends Runnable, Future<V> {  
     void run();  
    }  
    

    RunnableFuture同时实现了Runnable、Future这两个接口,也就是,既可以通过Runnable接口实现线程,也可以通过Fufure取得异步线程执行后的结果,因为实现了Runnable的缘故,那么FutureTask也可以直接提交给Executor执行,Executor接口代码如下:

    public interface Executor {
        void execute(Runnable command);
    }
    

    这个地方为什么要提到Executor呢?

    我们再回到最上边写的那个测试代码TestCallable中,有这么一行代码:new Thread(futureTask).start();,这段代码是用来创建并开启一个线程,但是问题来了,这种创建线程的方式是有很大弊端的:

    • 每次new Thread新建对象性能差;
    • 线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或OOM;
    • 缺乏更多功能,如定时执行、定期执行、线程中断。

    而通过Executor线程池的方式显然要效率很高。

    至此,我们了解了Callable、Future、FutureTask,同时知道了Future跟FutureTask之间的关系,还间接地了解到FutureTask也可以直接提交给Executor执行,那么我们重新修改一下最开始的代码:

    public class TestCallable {
    
        public static void main(String[] args) {
    
            /**创建Callable对象任务 **/
            DownImgThread demo = new DownImgThread();
    
            /**创建线程池**/
            ExecutorService executorService = Executors.newSingleThreadExecutor();
    
            /**提交任务并获取执行结果**/
            Future<List<String>> futureTask = executorService.submit(demo);
            
            /**关闭线程池**/
            executorService.shutdown();
    
            try {
                System.out.println("-----开始获取图片-----");
                List<String> imgs = futureTask.get();
                for (String img: imgs) {
                    System.out.println("图片:"+img);
                }
                System.out.println("-----获取图片结束-----");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
    
    }
    

    执行结果:

    -----开始获取图片-----
    图片:https://sscai.club/img/a0.jpg
    图片:https://sscai.club/img/a1.jpg
    图片:https://sscai.club/img/a2.jpg
    图片:https://sscai.club/img/a3.jpg
    图片:https://sscai.club/img/a4.jpg
    图片:https://sscai.club/img/a5.jpg
    图片:https://sscai.club/img/a6.jpg
    图片:https://sscai.club/img/a7.jpg
    图片:https://sscai.club/img/a8.jpg
    图片:https://sscai.club/img/a9.jpg
    -----获取图片结束-----
    

    眼尖的小伙伴,估计要开始吐槽创建线程池的方式了,是的阿里巴巴开发规范中强制要求「线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式」,所以也是给小伙伴留个作业,自己动手修改一下上方代码吧。

    右上角关注一下呗,博客园是我写文章的主阵地~

  • 相关阅读:
    恢复spark挂掉的节点
    启动spark集群
    记录一下SparkStreaming中因为使用redis做数据验证而导致数据结果不对的问题
    ps -aux与ps -ef
    Operation category READ is not supported in state standby
    spark web ui中的skipped的含义
    关于spark ui中executor显示的内存量与设置的内存量不符的问题
    flume修改配置文件
    maven中的各种问题
    java 的集合框架
  • 原文地址:https://www.cnblogs.com/niceyoo/p/13380070.html
Copyright © 2011-2022 走看看