线程池源码研究

2021-03-11  本文已影响0人  猿必过

前言:第一次写源码分析类文章,有点忐忑,还是硬着头皮上了。
之前几篇线程池文章主要是讲解线程池使用场景,这篇文章我以非代码方式讲解源码,这个估计没人这么干过吧!哈哈。

说实话一打开那种源码贴,不够耐心真心看不完,而且也记不住啊,之前学过一段时间的《记忆法》,最强大脑里面的冠军
袁文魁写了一本书专门讲记忆方法的书,里面说图形记忆是最快,记忆比较难忘的一种记忆方法,如果能加上情绪、味觉
触觉就记的更牢了,这可能和人类历史也有关系,有文字才几千年,没文字的几百万年呢。没文字的时候只能靠
图形、图案来记忆了。

下面,从3点说明线程池工作原理

  1. 线程池的接口定义和继承关系
  2. 线程池中线程的状态描述
  3. 线程池工作细节

因为不能粘贴源码,我会用思维导图的形式把上面几个点串起来。

1、线程池的接口定义和继承关系

thread1

上图可以看出线程池有哪些接口和类。最外面的接口是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线程新增和对各种状态如何做处理。首先我们给自己提几个问题,这样分析比较有针对性。
问题如下:

  1. work线程什么时候才start(),如何定义的
  2. work线程怎么实现阻塞获取任务
  3. 线程池操作如何做到线程安全

首先我们看第一个问题,我也一直比较好奇。这个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

本文由猿必过 YBG 发布
禁止未经授权转载,违者依法追究相关法律责任
如需授权可联系:zhuyunhui@yuanbiguo.com

上一篇下一篇

猜你喜欢

热点阅读