点击上方“中兴开发者社区”,关注我们
每天读一篇一线开发者原创好文
▍作者简介
黄宇是从事java开发的开源软件的爱好者。近些年致力于高并发、分布式大数据方向的研发工作。这篇文章主要讲解了java concurrent包中future模式的原理和使用,相信大家能够从中收到启发。
在多线程执行时,对于需要有返回值的场景,常常使用Callable和Future的方式来进行,常见的一种使用方式如下:
运行上面的代码,在控制台种等待三秒钟之后打印出结果。代码非常简单,但是有几个问题需要弄清楚:
1. 线程池是如何调用到Callable的call,结果是如何返回的。
2. Future.get方法是如何阻塞住当前线程的
3. 当Callable运行完是如何通知到阻塞在Future上面的线程的
下面我们开始分析源码解答上述问题(源码基于jdk1.8)。
首先从ExecutorService的submit(Callable )方法开始,此方法对应的实现类是ThreadPoolExecutor, 代码如下:
newTaskFor方法就是将callable转换成了FutureTask,FutrueTask也继承了RunnableFuture,获取到RunnableFuture后,调用了execute(ftask) ,我们继续向下跟代码
图片中的英文注释已经说的比较清楚,我这里在详细说一下,第一行:
int c = ctl.get(), 其中ctl是一个AtomicInteger,每当向线程池放入一个callable或者runnable,这个clt就+1,int c 获取到的就是当前线程池目前的任务数量,
情况1:如果c 小于当前线程池维护最小的工作线程(CorePoolSize)时,那么线程池就尝试开启一个工作线程来执行传入的callable,也就是下面执行的addWorker(command,true), 此方法会自动检查runState和workerCount,从而防止将添加的错误。
情况2:如果c大于等于CorePoolSize时,要判断当前线程池是否正在运行,其次判断callable是否可以加入队列(有可能加入不成功,例如使用了有界队列,队列可能满了),如果以上条件都满足,那么再次通过ctl获取一次数量,做二次检查,按照官方注释,写的是有可能在做完第一次检查后,当前线程池挂了,或者其他错误,那么此时需要做回滚,也就是if中的remove(command),从队列中删除刚才放入的callable,然后在调用reject拒绝此任务,如果二次检查也没有问题,再调用addWorker(null,false),我们发现参数和情况1不同,任务为null,意思是不要为当前任务分配工作线程(因为任务已经加入队列了,不需要立刻执行),false表示判断当前线程池工作线程的数量是否超过了maxPoolsize,有兴趣的同学,可以仔细研究下addWorker方法的源码。
情况3:如果调用addWorker返回false,说明当前线程池的工作线程数量超过了maxpoolSize了,那么新的任务需要拒绝(ps:上面描述的内容需要读者对线程池中,任务、工作线程、corePoolsize、maxPoolsize以及任务队列都熟悉才能理解)
根据上述分析,触发callable执行的代码在addWorker中,代码如下(addWorker的代码较长,我们看关键的部分):
红框部分就是触发执行callbale执行的地方,图片中第一行,firsttask就是我们上面创建的FutureTask,被Worker包装了一下,在获取Thread t,t的start方法就会触发FutureTask的run方法。我们再进入FutureTask的run方法,代码如下:
具体调用callable的方法就在result = c.call(), 至此上面的问题1已经解答了,callable是在何时调用的,执行结果赋值给FutureTask的result变量。
我们再看问题2,当调用FutureTask的get方法,如何阻塞调用线程的。在get方法中,如果判断当前直接没有结束,那么就调用awaitDone方法,代码如下:
awaitDone代码如下:
方法有些复杂,从第一行看起,deadline 经过计算 = 0,因为timed为false,向下看,之所以有for(;;)无线循环,因为下面用到了cas,需要自旋执行最终成功。进入循环体,首先判断当前调用get方法的线程是否被interrupt,如果被interrupt,那么直接返回不会阻塞,后面的几个if基本都是判断状态,如果当前FutureTask已经结束,那么就直接返回,不阻塞,然后初始化了WaitNode,当代码执行到红色框部分,说明当前futureTask没有执行完成,那么需要把上面初始化的WaitNode加入到队列,所谓的队列就是AQS(AbstractQueuedSynchronizer), AQS底层维护了一个双向链表,当多线程调用FutureTask的get方法时,多个线程就会被加入到这个链表,但是加入队列的过程需要锁的控制,这里的控制就是CAS,多个线程同时调用get方法时,当执行到这个红色框部分,每次只有一个线程可以加入链表成功,由于外层由for(;;),所以失败的线程会再次执行,然后又有一个线程执行成功,以此类推,所有的线程都会执行成功,加入队列。在向下看,由于我们调用的get方法,是没有时间限制的,所有timed=false,所以不会进入绿色框的代码块,对于加入队列成功的线程,只是当前线程对象加入队列,但是线程还在执行,此时queued=true,那么下次循环就会进入到蓝色框部分,LockSupport.park(),这个方法是阻塞,这个方法和wait()/notify()/notifyAll()很类似,但是wait之后,如果想唤醒指定的线程无法实现,只能notifyAll,不是很智能,LockSupport.park(thread)方法需要传入一个Thread,也就是阻塞的线程,在调用LockSupport.unPark(thread),传入的thread只要和park的thread相同,就可以唤醒指定的线程。
通过上面的描述,我们知道了,在get方法中,主要是通过park方法进行阻塞的,那是再哪里唤醒阻塞的线程的呢?其实上面也有提过,是再run方法中,因为run方法是调用callable.call的地方,callable执行成功后,会把值付给result变量,然后再调用set方法,在set方法中会调用finishCompletion,然后此方法再调用LockSupport.unpark()代码如下:
finishCompletion代码如下
从第一行开始分析,迭代waiters,waiters是上面说到的AQS对应的双向链表的头结点,需要把整个waiters对应的链表的线程全部唤醒。在循环体中,调用了SupportLock的unpark方法。
至此,FutureTask的大致过程已经分析完成,其中有些细节,笔者也没有了解特别深入,希望读者可以留言共同探讨。