jdk线程池实现原理分析_georgesnoopy的博客-优快云博客 这篇说明了线程池的基本原理。但是在使用线程池的时候,我们会发现我们可以:
- 1向线程池中提交Runnable任务,也可以提交Callable任务,但是Thread只能驱动Runnable任务,那线程池是怎么实现Callable到Runnable的转换的。
- 我们还可以通过ExecutorService#submit()方法返回的Future#get()方法去检查任务的执行状态,以及获得任务执行的结果,任务没结束还可以阻塞当前线程。但是我们提交任务的时候,最终到底哪个线程来回来驱动这个任务都不知道,那么又是怎么实现的,保证通过Future获取到一定就是我刚刚提交的任务的结果的呢?
要搞清楚这两个问题的答案,就需要结合线程池的实现以及Future的原理来解释。
Promis模式
在生活中,吃饭点菜的时候,点菜后服务员会给到一个小票,然后我们只需要去餐桌等待就可以了,服务员会根据小票给你上菜,这个小票就是点菜的凭证。如果说没有这个小票,那点完菜,啥也不不用干了,时刻钉这厨房:看看厨师有没有开始做你的菜了、做好了没有。。。
对应到异步开发中,这个点菜的过程就是异步任务提交的过程,我们同样需要有个凭证,通过这个凭证在需要的时候来获取我们提交任务的异步执行结果。如下是一个通用promise模式:
在jdk中,我们可能都比较熟悉线程池的实现:ThreadPoolExecutor。配置线程池其实也实现了promise模式,来获取提交给线程池的异步任务的结果,那就是Future,和通用的promise模式对比下:
Future原理
理论分析
在看future这个框架实现的时候,带着几个问题:
- Jdk的线程实现Thread只能驱动Runnablere任务,而Runnable#run()方法是没有入参、也没有返回值的,去看线程池的实现ThreadPoolExecutor的execute()方法也只是支持传入Runnable任务,但是ExecutorService#submit()支持Callable任务,是怎么做到的?(Callable任务是怎么转换成Runnable任务的?又是怎么从异步线程拿到结果的)
- ExecutorService#submit()的时候,都不知道最终会是哪个线程会来驱动这个任务的执行,Future是怎么做到正确拿到你提交任务的结果,而不是其他任务的结果的?
Future的类结构图:
这个图里的两个关键类,搞清楚这两个类,就搞清楚了上面的两个问题:
- runnableAdapter:这个类就实现了Callable任务转换成Runnable任务。具体点就是结合了组合和继承来实现的。
- FutureTask是核心:这个类里真正实现了凭证模式。
FutureTask的核心属性:
- callable:记录向线程池提交的任务。
向线程池提交一个任务,不管是Callable任务还是Runnable任务,其实都是创建了一个FutureTask任务来封装了提交的任务。而FutureTask中用callable这个属性记录下了这个任务。
如果是Runnable任务,那使用Executors.callable(runnable, result)转换成了Callable任务记录在了FutureTask#的callable任务中。
而FutureTask本身又实现了Runnable,所以最终传给ThreadPoolExecutor的任务就是FutureTask,然后FutureTask重写run()方法,来调用自己的属性callable#call()方法,来驱动任务执行。
- runner:这个记录的就是到底是哪个线程驱动执行提交过来的任务。
- outcome:这个字段就是记录了线程驱动任务执行的结果。这个也是为什么通过Future可以随时可以来获取提交任务的执行结果
- waiters:这个记录的是阻塞等待改任务执行完的线程列表(简单粗暴理解就是有哪些线程调用了Future#get()阻塞住了)
- state:FutureTask的状态机,用来记录任务的完成情况,这样当来获取结果的时候,才知道该咋整:直接返回结果 or 阻塞
通过这些属性可以发现:FutureTask是将提交的任务给记录下来了,并且将线程驱动任务的执行结果给记录下来了。然后用状态机来控制,任务执行的情况,以便当来获取任务的时候才知道怎么做。
最终其实是通过提交任务的线程,以及驱动任务执行的线程,分别调用FutureTask不同的方法,将数据写入到FutureTask中。然后需要获取结果的线程直接去FutureTask中拿。
下面这个图解释一下任务提交线程和任务驱动线程分别会修改哪些属性,去看源码也可以发现,对于两边都会修改的这种共享变量使用CAS去修改的:
提交任务的线程/获取结果的线程:
- 提交任务的线程通过调用ExecutorService#submit()提交任务的时候new FutureTask(),会去改变state和callable值。将任务记录在FutureTask中,并记录状态state=NEW
- 通过调用cancel()方法,会去改变state和waiters的值:将state值该为CANNCELLED或者INTERRUPTED,并且清空waiters,将等待在任务上的线程唤醒
对于驱动任务执行的线程池线程:
- 调用run()方法执行任务,会去修改runner,记录是哪个线程执行了本任务,实际会去调用callable.call()。最终将线程的执行结果放在outcome中,并且修改state状态为完成,并清空waiters,唤醒等待在任务上的线程。
和点菜的小票类比:
- 点了哪些菜—callable属性记录
- 哪个厨师在给你做菜---runner记录
- 菜做的进展怎么样了----state记录
- 有哪些猴急的人在线等吃菜的--- waiters记录
- 出菜结果-----outcome记录
FutureTask实现的promise模式结构看起来非常简单,但是其中对组合和继承的理解和使用,我认为是最深的,非常值得学习。(对继承和组合应用的最好的就是设计模式)
源码分析
Future模式最主要的作用就是如何拿到异步线程执行任务的结果?
提交的可以是Runnable或者Callable任务。但是线程池中的Thread只能执行Runnable任务。而ThreadPoolExecutor驱动任务执行的方法只有execute(Runnable),也没见着入参是Callable,那么Future和线程池是如何结合到一起的呢?
FutureTask
FutureTask实现了Future和Runnalbe:
Executor.submit()
向线程池提交一个任务的时候,不管是Runnable任务,还是Callable任务,其实都是封装成了一个FutureTask(它实现了Runnable接口),然后最终给到ThreadPoolExecutor执行的任务是FutureTask,而FutureTask其实就是提交任务的凭证。然后就看FutureTask怎么实现的
创建一个FutureTask
提交的可能是Callable任务,也可能是runnable任务,所以FutureTask也有两个构造方法:
- 提交的是Callable方法的:
- 提交的是Runnable方法的:
这个地方就调用了Executors.callable()方法将Runnable任务转换成Callable任务了。转换的逻辑也很简单。搞了一个适配器:RunnableAdapter,返回的就是这个适配器类对象:
RunnableAdapter的实现也很简单,它实现Callable接口,但组合了Runnable:
执行任务及保存结果
从ExecutorService#submit()的逻辑上可看出,最终提交给ThreadPoolExecutor的任务其实就是FutureTask,而我们知道ThreadPoolExecutor驱动任务执行的方式就是选一个空闲线程,去调用Runnable#run()方法来驱动任务的执行。所以说要看FutureTask是怎么完成任务执行以及拿到结果的,看它重载的Runnable#run()方法就好了。
我们必须明确一点:这个run()方法的调用者是线程池中的线程,不是提交任务的线程
只有明确了这一点,我们才可能理解FutureTask是怎么工作的。来看源码:
其实这里只要明白一点就ok:对于FutureTask对象来说,在运行时就是一块内存,任务提交的线程和最终驱动任务执行的线程都来修改这块内存:
- 任务提交线程:将提交的具体任务写入到这块内存,以便执行线程执行的时候才知道干啥
- 执行线程将执行结果写入到这块内存,以便后续需要任务结果的线程来获取结果。
- 需要获取结果的线程,只需要来读取这块内存,就能拿到结果。只是说可能执行线程还没看是干活或者没干完你就来拿,那就会阻塞。
这里有个非常重要的点:就是FutureTask在任务执行结束(不管是正常结束、还是异常结束)提供了一个回调函数:done()。调用这个done()时机:
- 驱动线程正常执行完任务
- 驱动线程执行任务抛出异常
- 提交任务线程调用了cancel()取消任务执行
读取结果
经过上面分析,不用看源码了也知道读取结果在干啥了:
根据状态返回结果:
- 如果任务状态=NEW(新建没执行)、COMPLETING(执行中)。则阻塞当前线程。
- 其他状态就直接返回结果:
- State=normal,直接返回outcome记录的结果就好了
- state>CANCELED表示的是任务被取消,则报错;
- 其他情况(其实就只有EXCEPTIONAL了),那就抛出执行异常。