前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >线程池源码研究

线程池源码研究

原创
作者头像
朱可道
修改2021-03-14 17:02:15
3920
修改2021-03-14 17:02:15
举报
文章被收录于专栏:后端面试题

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

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

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

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

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

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

thread1
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个。

我们看下线程池状态有哪些?

线程池状态
线程池状态

从上面二进制可以看出为啥是高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标示位置。比如

代码语言:javascript
复制
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

代码语言:javascript
复制
    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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1、线程池的接口定义和继承关系
  • 2、线程池中线程的状态描述
  • 3、线程池工作细节
  • 参考
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档