前言:第一次写源码分析类文章,有点忐忑,还是硬着头皮上了。 之前几篇线程池文章主要是讲解线程池使用场景,这篇文章我以非代码方式讲解源码,这个估计没人这么干过吧!哈哈。
说实话一打开那种源码贴,不够耐心真心看不完,而且也记不住啊,之前学过一段时间的《记忆法》,最强大脑里面的冠军 袁文魁写了一本书专门讲记忆方法的书,里面说图形记忆是最快,记忆比较难忘的一种记忆方法,如果能加上情绪、味觉 触觉就记的更牢了,这可能和人类历史也有关系,有文字才几千年,没文字的几百万年呢。没文字的时候只能靠 图形、图案来记忆了。
下面,从3点说明线程池工作原理
- 线程池的接口定义和继承关系
- 线程池中线程的状态描述
- 线程池工作细节
因为不能粘贴源码,我会用思维导图的形式把上面几个点串起来。
1、线程池的接口定义和继承关系
上图可以看出线程池有哪些接口和类。最外面的接口是Executors,里面只有个一个方法是execute
, 然后是AbstractExecutorService,可以说是用了模版设计模式,线程的执行操作里面都有。
我们看一个比较不常用的方法,AbstractExecutorService.invokeAny(你可以直接使用额), 参数有tasks,time,timeUnit。 干什么用的呢,场景就是有一批任务,设置一个超时时间等待所有task执行完才返回Futures,这个时候get()不会阻塞了。看了这个方法的源码 其实就是使用了ExecutorCompletionService帮你实现了,这个类poll操作可以返回最新执行完的Future,想想之前真傻逼,jdk已经提供了这个方法, 直接拿来用就可以了,这也印证了看源码真的可以提效,某些场景已经有相关的实现了。
上面的思维导图,我们再看右边的部分,创建线程池源码中出现两种不一样的构造方法。大部分我们还是用 ThreadPoolExecutor这个类的构造方法,但是也有几个方法,比如newSingle*
系列的。
那他们的差别在什么地方,看了源码发现FinalizableDelegatedExecutorService里面就多了一个方法,重写了 finalize(),这里面就是调用shutdown关闭线程池,那很好理解了就是线程池可以自己销毁。非单例的线程池可以这样玩,释放线程池资源。
这里衍生一个面试题:newSingleThreadExecutor(1)
和 newFixedThreadPool(1)
有什么区别?
答案是newSingleThreadExecutor里面委托掉了ThreadPoolExecutor这个类,只提供线程执行的方法,像 修改线程数、暂停线程等方法都去掉了,其实就是起到一种保护线程配置的作用,开闭原则的一个体现吧。
写到这里有点困了,快晚上11点了,🐎 🐎 🐎
2、线程池中线程的状态描述
楼上装修,这两天没写,提前上班来公司写点代码。
一般抽象类很少定义属性,主要是定义一些抽象方法。那线程池的状态和数量定义在哪呢?
答案是ThreadPoolExecutor, 这个类里面有个ctl的原子类。ctl高 3
位用来表示线程池状态,后 29
位用来记录线程池线程个数。 所以线程池里面线程的最大只有2的28次方-1个。
我们看下线程池状态有哪些?
状态 | 定义 | 二进制 | 备注 |
---|---|---|---|
RUNNING | -1 << COUNT_BITS | 111...000 | 接受且处理任务 |
SHUTDOWN | 0 << COUNT_BITS | 000...000 | 不接受但处理任务 |
STOP | 1 << COUNT_BITS | 001...000 | 不接受不处理,interrupt线程 |
TIDYING | 2 << COUNT_BITS | 010...000 | 整理状态,由terminated触发,直到workcount=0 |
TERMINATED | 3 << COUNT_BITS | 011...000 | terminated结束 |
从上面二进制可以看出为啥是高3位
,因为-1到3刚好够了,不多不少。
3、线程池工作细节
最后,我们看下线程池工作细节,其实就是分析work线程新增和对各种状态如何做处理。首先我们给自己提几个问题,这样分析比较有针对性。 问题如下:
- work线程什么时候才start(),如何定义的
- work线程怎么实现阻塞获取任务
- 线程池操作如何做到线程安全
首先我们看第一个问题,我也一直比较好奇。这个work线程是特殊封装过的。
我们在提交任务的时候,AbstractExecutorService统一处理了,不管是submit或者execute,Runnable或者Callable都会包装成 RunnableFuture,RunnableFuture只是实现了Runnable和Future接口,自己本身也是一个接口,他有个实现是new FutureTask<T>(runnable, value)
。
FutureTask提供了很多protectd方法,你可以覆盖这些方法,自定义扩展业务逻辑,比如done()
方法。 如果你看这个类,非常有意思,里面淋漓尽致的展示了Unsafe类的强大之处,可以线程安全的操作类属性还可以用到cas特性,前提是volatile定义的。
看下执行线程的流程:
从上面的图可以看出,在submit/execute之后【区别:execute返回void,submit返回Future】,如果线程池是正常工作,就会启动Worker();
我们在新增任务的时候,有个编程技巧,定义label, 这样break标示位置。比如
retry:
for(;;){
...
break retry;
continue retre;
}
我们再看下第二个问题,worker线程是如何阻塞重用线程的。
老规矩,线程里面不是 for(;;) 就是 while循环,源码中是while循环。
while (task != null || (task = getTask()) != null) ... runStateAtLeast(ctl.get(), STOP) //如果STOP就终止
其中getTask就是从ThreadPoolExecutor的workQueue阻塞队列中take新加入的任务。
第三个问题,详细说下Worker对象,看下Worker对象的定义,它是AbstractQueuedSynchronizer的子类,如此则可以自定义加锁行为,获取锁和释放锁就可以 托管给ThreadPoolExecutor来判断了,最后源码处就用了Worker.isLocked()。
有一点比较重要,ThreadPoolExecutor许多获取线程状态的方法都是使用属性mainLock
来保证线程安全的。比如下面的getActiveCount
public int getActiveCount() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
int n = 0;
for (Worker w : workers)
if (w.isLocked()) // 能保证准确性
++n;
return n;
} finally {
mainLock.unlock();
}
}
打脸了说了不贴源码的,😢。
参考
[Java未开源的Unsafe类]https://www.cnblogs.com/daxin/p/3366606.html [线程池之ThreadPoolExecutor线程池源码分析笔记]https://www.cnblogs.com/huangjuncong/p/10031525.html
扫描二维码,关注公众号“猿必过”
回复 “面试题” 自行领取吧。
微信群交流讨论,请添加微信号:zyhui98,备注:面试题加群
本文由猿必过 YBG 发布 禁止未经授权转载,违者依法追究相关法律责任 如需授权可联系:zhuyunhui@yuanbiguo.com