这是张小帅失业之后的第三场面试。
面试官:“实际开发中用过多线程吧,那聊聊线程池吧”。
“有CachedThreadPool:可缓存线程池,FixedThreadPool:定长线程池.......balabala”。小帅暗暗窃喜,还好把这几种线程池背下来了,看来这次可以上岸了。
面试官点点头,继续问到“那线程池底层是如何实现复用的?”
“额,这个....”
寒风中,那个男人的背影在暮色中显得孤寂而凄凉,仿佛与世隔绝,独自面对着无尽的寂寞......
如果问到线程池的话,不好好剖析过底层代码,恐怕真的会像小帅那样被问翻吧。
那么在此我们就来好好剖析一下线程池的底层吧。我们大概从如下几个方面着手:
概览图
说到线程池,其实我们要先聊到池化技术。
池化技术:我们将资源或者任务放入池子,使用时从池中取,用完之后交给池子管理。通过优化资源分配的效率,达到性能的调优。
池化技术优点:
所以我们说线程池是提升线程可重复利用率、可控性的池化技术的一种。
现在我们有这样一个场景,上层有业务系统批量调用底层进行发送邮件,废话不多,直接上代码:
demo
最终运行输出结果为:
由线程:pool-1-thread-1 发送第:0封邮件由线程:pool-1-thread-2 发送第:1封邮件由线程:pool-1-thread-1 发送第:2封邮件由线程:pool-1-thread-2 发送第:3封邮件由线程:pool-1-thread-1 发送第:4封邮件由线程:pool-1-thread-1 发送第:6封邮件由线程:pool-1-thread-2 发送第:5封邮件由线程:pool-1-thread-1 发送第:7封邮件由线程:pool-1-thread-2 发送第:8封邮件由线程:pool-1-thread-1 发送第:9封邮件
上面的例子中从结果来看是10封邮件分别由两条线程发送出去了,上图可见,我们给ThreadPoolExecutor这个执行器分别指定了七个参数。那么参数的含义到底是什么呢?接下来咱们层层抽丝剥茧。
大家估计会有疑问,线程池的种类那么多,案例中为什么要用TheadPoolExecutor类呢,其他的种类是由TheadPoolExecutor通过不同的入参定义出来的,所以我们直接拿ThreadPoolExecutor来看。
我们先来看一下ThreadPoolExecutor的继承关系,有个宏观印象:
宏观继承
我们再来看一下ThreadPoolExecutor的构造方法:
构造方法
下面我们来解释一下几个参数的含义:
大家对上述的含义初步有个概念。
看了上面的构造函数字段大家估计也还是优点懵的,尤其是从来没有接触过商品池的小伙伴。所以老猫又撸了一张商品池的大概的工作流程图,方便大家把这些概念串起来。
大概流程
上图中老猫标记了四条线,简单介绍一下(当然上图若有问题,也希望大家能够指出来)。
接下来我们来看一下执行theadPoolExecutor.execute()的时候到底发生了什么。先来看一下源码:
public void execute(Runnable command) { if (command == null) throw new NullPointerException(); int c = ctl.get(); if (workerCountOf(c) < corePoolSize) { if (addWorker(command, true)) return; c = ctl.get(); } if (isRunning(c) && workQueue.offer(command)) { int recheck = ctl.get(); if (! isRunning(recheck) && remove(command)) reject(command); else if (workerCountOf(recheck) == 0) addWorker(null, false); } else if (!addWorker(command, false)) reject(command); }
(1) ctl变量
进入执行源码之后我们首先看到的是ctl,只知道ctl中拿到了一个int数据至于这个数值有什么用,目前不知道,接着看涉及的相关代码,老猫将相关的代码解读放到源码中进行注释。
//通过ctl获取线程池的状态以及包含的线程数量 private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); private static final int COUNT_BITS = Integer.SIZE - 3; // COUNT_BITS = 32-3 = 29 /**001左移29位 * 00100000 00000000 00000000 00000000 * 操作减1 * 00011111 11111111 11111111 11111111(表示初始化的时候线程情况,1表示均有空闲线程) * 换成十进制:COUNT_MASK = 536870911 */ private static final int COUNT_MASK = (1 << COUNT_BITS) - 1; /** * 运行中状态 * 1的原码 * 00000000 00000000 00000000 00000001 * 取反+1 * 11111111 11111111 11111111 11111111 * 左移29位 * 11100000 00000000 00000000 00000000 **/ // runState is stored in the high-order bits private static final int RUNNING = -1 << COUNT_BITS; //运行中状态 11100000 00000000 00000000 00000000 private static final int SHUTDOWN = 0 << COUNT_BITS; //终止状态 00000000 00000000 00000000 00000000 private static final int STOP = 1 << COUNT_BITS; //停止 00100000 00000000 00000000 00000000 private static final int TIDYING = 2 << COUNT_BITS; // 01000000 00000000 00000000 00000000 private static final int TERMINATED = 3 << COUNT_BITS; // 01100000 00000000 00000000 00000000 //取高3位表示获取运行状态 private static int runStateOf(int c) { return c & ~COUNT_MASK; } //~COUNT_MASK表示取反码:11100000 00000000 00000000 00000000 //取出低位29位的值,当前活跃的线程数 private static int workerCountOf(int c) { return c & COUNT_MASK; } //COUNT_MASK:00011111 11111111 11111111 11111111 //计算ctl的值,ctl=[3位]线程池状态 + [29位]线程池中线程数量。 private static int ctlOf(int rs, int wc) { return rs | wc; } //进行或运算
上面我们针对各个状态以及那么多的二进制表示符有点懵,当然如果不会二进制运算的,大家可以先自己去了解一下二进制的运算逻辑。通过源码中的英文,我们知道CTL的值其实分成两部分组成,高三位是状态,其余均为当前线程数。如下的图:
线程池状态
上面的图的描述解释,其实也都是英文注释版的翻译,我们再来看一下有了这些状态,这些状态是怎么流转的,英文注释是这样的:
/*** RUNNING -> SHUTDOWN * On invocation of shutdown() * (RUNNING or SHUTDOWN) -> STOP * On invocation of shutdownNow() * SHUTDOWN -> TIDYING * When both queue and pool are empty * STOP -> TIDYING * When pool is empty * TIDYING -> TERMINATED * When the terminated() hook method has completed * /
上面的描述不太直观,老猫将流程串了起来,得到了下面的状态机流转图。如下图:
状态机流程
写到这里,其实ctl已经很清楚了,ctl说白了就是状态位和活跃线程数的表示方式。通过ctl咱们可以知道当前是什么状态以及活跃线程数量是多少 (设计很巧妙,如果此处还有问题,欢迎大家私聊老猫)。
(3) 线程池中的线程数小于核心线程数
读完ctl之后,我们来看一下接下来的代码。
if (workerCountOf(c) < corePoolSize) { if (addWorker(command, true)) return; //添加新的线程 c = ctl.get(); //重新获取当前的状态以及线程数量}
继上述的workerCountOf,我们知道这个方法可以获取当前活跃的线程数。如果当前线程数小于配置的核心线程数,则会调用addWorker进行添加新的线程。如果添加失败了,则重新获取ctl的值。
(4) 任务添加到队列的相关逻辑
if (isRunning(c) && workQueue.offer(command)) { int recheck = ctl.get(); //再次check一下,当前线程池是否是运行状态,如果不是运行时状态,则把刚刚添加到workQueue中的command移除掉 if (! isRunning(recheck) && remove(command)) reject(command); else if (workerCountOf(recheck) == 0) addWorker(null, false); }
上述我们知道当添加线程池失败的时候,我们会重新获取ctl的值。此时咱们的第一步就很清楚了:
(5) 线程池中的线程数量小于最大线程数代码逻辑以及拒绝策略的代码逻辑
接下来,我们看一下最后的一个步骤
/** * 进入第三步骤前提: * 1.线程池不是运行状态,所以isRunning(c)为false * 2.workCount >= corePoolSize的时候于此同时并且添加到queue失败的时候执行 */else if (!addWorker(command, false)) reject(command); }
由于调用addWorker的第二个参数是false,则表示对比的是最大线程数,那么如果往线程池中创建线程依然失败,即addWorker返回false,那么则进入if语句中,直接调用reject方法调用拒绝策略了。
写到这里大家估计会对这个第二个参数是false为什么比较的是最大线程数有疑问。其实这个是addWorker中的方法。我们可以大概看一下:
private boolean addWorker(Runnable firstTask, boolean core) { retry: for (int c = ctl.get();;) { // Check if queue empty only if necessary. if (runStateAtLeast(c, SHUTDOWN) && (runStateAtLeast(c, STOP) || firstTask != null || workQueue.isEmpty())) return false; for (;;) { if (workerCountOf(c) >= ((core ? corePoolSize : maximumPoolSize) & COUNT_MASK)) return false; if (compareAndIncrementWorkerCount(c)) break retry; c = ctl.get(); // Re-read ctl if (runStateAtLeast(c, SHUTDOWN)) continue retry; // else CAS failed due to workerCount change; retry inner loop } }}
我们很明显地看到当core为flase的时候咱们获取的是maximumPoolSize,也就是最大线程数。
写到这里,其实咱们的核心主流程大概就已经结束了。这里其实老猫也只是写了一个算是比较入门的开头。当然我们还可以再深入去理addWorker的源码。这个其实就交给大家去细看了,篇幅过长,相信大家也会失去阅读的兴趣了,感兴趣的可以自己研究一下,如果说还是有问题的,可以找老猫一起探讨,老猫的公众号:"程序员老猫"。老猫觉得在上述的源码中比较重要的其实就是ctl值的流转顺序以及计算方式,读懂这个的话,后面一切的源码只要顺藤摸瓜即可理解。
我们上述主要和大家分享了比较核心的theadPoolExecutor。除此之外,线程池Executors里面包含了很多其他的线程池模板。当然这也是小猫直接面试的时候说的那些,其实小猫也就仅仅只是背了线程池模板而已,并不知晓其工作原理。如下几种:
上述针对这些罗列了一下,其实很多官网上也有相关的介绍,当然感兴趣的小伙伴也可以再去刨一刨里面的源码实现。
很多小伙伴在用一些线程池或者第三方中间件的时候可能只停留在如何使用上,一旦出了问题或者被人深入问到其实现原理的时候就比较头大。所以在日常开发的过程中,我们不仅仅需要知道如何去用,其实更应该知道底层的原理是什么。这样才能长立于不败之地。
本文链接:http://www.28at.com/showinfo-26-60967-0.html背会了常见的几个线程池用法,结果被问翻
声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com
上一篇: 面试官:实际工作中哪里用到了自定义注解?