线程池继承体系

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

很多同学问我看源码有什么诀窍,其实最大的诀窍就是勇敢地打开IDEA点进去。

大多数开发者对于线程池继承体系的认识是非常模糊的,他们可能知道一些线程池参数,但不知道这些参数是哪个类定义的,也不知道哪些线程池类是属于JDK的,哪些是Spring扩展的。市面上几乎所有的学习资料都只是一味地介绍线程池的几大参数,而对于整个继承体系、常用线程池类以及常用方法的底层原理一笔带过。个人认为这样的学习是在做无用功,因为你的知识体系是混乱的,会导致后期学习的内容无法归类到现有的知识体系中,最终的结果就是:记不住。

所以本文的目的就是帮大家梳理JDK线程池的设计思想,Spring对线程池的实现会在Spring章节介绍。

一、Executor接口:定义execute()

线程池的初衷是屏蔽线程的创建、使用及调度(复用)等细节,让使用者通过简单的提交即可执行任务。

基于这个设计思想,JDK设计了一个顶级接口:Executor [ɪɡˈzekjətər] (念作:乙哥贼Q特)

它只定义了一个方法:execute() [ˈeksɪkjuːt] (念作:爱克seeQ特)

正如方法注释所说的,execute()作为接口方法,并没有强制子类如何实现。

只要子类最终实现的效果是下面这样即可:

// 搞一个Executor实例
Executor executor = createExecutor();
// 提交任务即可,其他细节它帮你搞定(屏蔽底层细节,分离线程类和任务类)
executor.execute(new RunnableTask1());
executor.execute(new RunnableTask2());

你可以在方法内部直接使用new Thread().start():

class ThreadPerTaskExecutor implements Executor {
    public void execute(Runnable r) {
        new Thread(r).start();
    }
}

甚至可以实现同步调用(接口方法并没有强制子类实现必须走异步调用):

class DirectExecutor implements Executor {
    public void execute(Runnable r) {
        r.run();
    }
}

二、ExecutorService接口:引入线程池状态+Future

顶级接口Executor实在过于单薄,只有一个方法。ExecutorService继承Executor并引入了一些新的方法定义:

如果说线程池的顶级接口Executor是开放式的,允许子类随便选择同步实现还是异步实现,那么作为儿子,ExecutorService则已经下定决心要走异步了,也就是创建线程执行任务,而不是同步调用:

ExecutorService的贡献有两点:

  • 突破性地引入“线程池状态”的概念,要求子类必须实现诸如shutDown()、shutDownNow()、isTerminated()、isShutDown()等方法以便反馈线程池的状态:已关闭?任务已结束?
  • 另外还引入了submit()方法。它的灵感来自于Executor的execute(),但execute()是没有返回值的,而submit()会返回一个Future对象

通过submit()提交一个任务后会返回一个Future接口的实例,我们可以通过这个实例获取结果或者取消任务(后面详细讲)。

也就是说:

  • 第一阶段:线程池顶级接口Executor只规定了一个execute()方法,过于单薄
  • 第二阶段:ExecutorService接口继承Executor并新增展示线程池状态的方法、submit()及Future返回值

如果你只能记住一件事,那么请记住:从ExecutorService开始,线程池可以有返回值了而这个Future,就是之前介绍的FutureTask他爹...

三、ScheduledExecutorService接口:定时任务

如果说ExecutorService已经基本规定了一个线程池需要提供哪些功能,那么ScheduledExecutorService则在此基础上提出了定时任务线程池的概念,也就是说JDK认为定时任务线程池是个更细化的领域,单独抽取一个接口描述。

ScheduledExecutorService要求子类实现定时方法,能够周期性地执行任务。与submit()类似,当任务提交给这些定时方法后,会返回ScheduledFuture,本质也是用来掌控任务进度的:

我知道,现在大家的疑问是:你和我讲这些有啥用?怎么实现的?别急,继续往后看。接口只是规定要这样做,后面会有实现类去实现的。

四、AbstractExecutorService抽象类:继往开来

目前为止,我们分析的都是JDK关于线程池的接口定义,它们的关系是这样的:

这些接口都只是“规定”了一些方法,没有具体的实现。但我们不必惊慌,JDK知道我们很菜,不可能让我们从零实现线程池的,所以提供了AbstractExecutorService抽象类。

AbstractExecutorService做了两件事:

  • 新增newTaskFor()方法,将Runnable和Callable两种任务统一为FutureTask类型
  • 对ExecutorService接口的submit()等方法做了初步实现

newTaskFor()本身并没有做什么,所有的包装工作都是FutureTask做的,老流氓了,这里就不赘述了。

那么,AbstractExecutorService为什么引入FutureTask呢?因为它的野心很大,已经不满足于仅仅执行Runnable,它还要兼容Callable。

实际Executor线程池是在JDK1.5提出的,Callable只不过是它带的小弟,反而Runnable才是前朝老臣(JDK1.0)。不过,道哥李也算够义气:来吧,小R,入我麾下共创Java霸业。

要不咋说道哥李情商高呢,Executor#execute优先支持Runnable这位前朝老臣,直到ExecutorService才通过submit()引入自己的小弟Callable:

既然FutureTask能兼容Runnable/Callable,那么就别管前朝老臣还是后起之秀了,都是自己人,拿来吧你:

嚯,仔细一看submit(),只做了任务包装,真正的执行还是Executor#execute(),忒坏了。线程池任务具体如何执行,下一章会介绍。

最后,我们来总结一下ExecutorService。

道哥李在JDK1.5提出了线程池的设计,本意估计是想支持异步任务获取,但Runnable在JDK1.0就出来了,返回值是void。为了兼容Runnable/Callable,于是引入了FutureTask对两种任务类做包装,同时FutureTask又恰好支持异步结果获取,两全其美。

所以,线程池为什么要引入FutureTask包装Runnable和Callable?

  • Executor顶级接口的历史原因,需要包装Runnable和Callable,统一返回值问题
  • 谋求更强大的功能(异步+结果)

教科书式的设计模式:适配器模式,适配器模式最典型的场景就是新老接口兼容。

五、ForkJoinPool&ThreadPoolExecutor:双雄争霸

JDK的线程池设计发展到这一步,终于出现了两个划时代的实现类

  • ForkJoinPool
  • ThreadPoolExecutor

注意到上面对的加粗字体了吗?是的,不再是抽象类了,而是可以new出来直接使用的实现类

ForkJoinPool比较复杂,这里暂时只讨论ThreadPoolExecutor。后面我们会学习CompletableFuture,它底层使用的线程池就是ForkJoinPool。

其实标题本来想取“三国鼎立的”,但是算上ScheduledExecutorService的话标题太长了,就被我阉割了。常用的Java的线程池如果粗分的话其实有三大类:

  • ForkJoinPool
  • ThreadPoolExecutor
  • ScheduledExecutorService

六、ThreadPoolExecutor实现类

抛开ForkJoinPool,从上面Executor顶级接口一路下来,ThreadPoolExecutor是第一个能拿来就用的实现类!这意义丝毫不亚于七龙珠里孙悟空第一次变成超级赛亚人,太强了。

我强烈建议大家去读一读ThreadPoolExecutor的类注释,真的写得太好了。

ThreadPoolExecutor的贡献有以下几点:

  • 首次引入corePoolSize、maximumPoolSize、keepAliveTime、ThreadFactory、RejectHandler等参数配置,真正实现了线程的“池化”。线程池里的线程数、队列长度、拒绝策略会在这几个阈值的影响下动态调整,大大提高了执行效率和复用率

  • 抽象出workers(线程池),一个Worker就是一个线程

  • 抽象出workQueue(阻塞队列),缓冲待执行任务

  • 内置4种拒绝策略,规避执行压力

关于ThreadPoolExecutor的细节,我们放到下一章。

如果你觉得本篇内容太多,只需要了解线程池主干即可:

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

进群,大家一起学习,一起进步,一起对抗互联网寒冬

猜你喜欢

转载自blog.csdn.net/smart_an/article/details/134890287
今日推荐