美文网首页
线程池源码研究

线程池源码研究

作者: 猿必过 | 来源:发表于2021-03-11 22: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

    相关文章

      网友评论

          本文标题:线程池源码研究

          本文链接:https://www.haomeiwen.com/subject/bjzoqltx.html