荒腔走板
大家好,我是 why,一个四川程序猿,成都好男人
先是本号的特色,技术分享之前先简短的荒腔走板聊聊生活。让文章的温度更多一点点。
上面的图是我在一次跑步的过程中拍的。活动之前赛事方搞了个留言活动,收集每公里路牌的一个宣传语。
我的留言有幸被选中了:
每人知道你在坚持什么,但你自己心里应该清楚。
是在说跑马拉松,也是在说其他的事情。
我记得那天的太阳,骄阳似火,路上的树荫也非常的少。苦就苦在我还报的是超级马拉松(说是超级马拉松,其实就是一个全马 42 km加最后 3 km纯上坡的马拉松)
到底有多晒,我给你看一下对比:
酷暑难耐,以至于 30 公里左右的地方我的心里出现了两个小人:
一个说:我好累啊,我跑不动了,我要退赛。
一个说:好呀好呀,我也好晒啊,退赛退赛。
我说:呸,看你们两个不争气的东西,让我带你去终点
于是在 36 公里的地方碰到了我提交的标语,非常开心,停下来拍了几张照片。给自己说:坚持不住的时候再坚持一下。
最后的 3 公里上坡,抽筋了不知道多少次。远远看见终点拱门的时候我突然想到了在敦煌的时候悟出的一句话:自己给自己的辛苦,不是辛苦,是幸福。
好了,说回文章。
违背直觉的JDK线程池
先用 JDK 线程池来开个题。
还是用我之前这个文章《如何设置线程池参数?美团给出了一个让面试官虎躯一震的回答。》“先劝退一波”这一小节里面的例题:
问:这是一个自定义线程池,假设这个时候来了 100 个比较耗时的任务,请问有多少个线程在运行?
正确回答在之前的文章中回答了,这里不在赘述。
但是我面试的时候曾经遇到过很多对于 JDK 线程池不了解的朋友。
而这些人当中大多数都有一个通病,那就是遇到不太会的问题,那就去猜。
面试者遇到这个不会的题的时候,表面上微微一笑,实际上我都已经揣摩出他们的内心活动了:
MD,这题我没背过呀,但是刚刚听面试官说核心线程数是 10,最大线程数是 30。看题也知道答案不是 10 就是 30。
选择题,百分之 50 的命中率,难道不赌一把?
等等,30 是最大线程数?最大?我感觉就是它了。
于是在电光火石一瞬间的思考后,和我对视起来,自信的说:
于是我也是微微一笑,告诉他:下去再了解一下吧,我们聊聊别的。
确实,如果完全不了解 JDK 线程池运行规则,按照直觉来说,我也会觉得应该是,不管是核心还是最大线程数,有任务来了应该先把线程池里面可用的线程用完了,然后再把任务提交到队列里面去排队。
可惜 JDK 的线程池,就是反直觉的。
那有符合我们直觉的线程池吗?
有的,你经常用的的 Tomcat ,它里面的线程池的运行过程就是先把最大线程数用完,然后再提交任务到队列里面去的。
我带你剖析一下。
Tomcat线程池
先打开 Tomcat 的 server.xml 看一下:
眼熟吧?哪一个学过 java web 的人没有配置过这个文件?哪一个配置过这个文件的人没有留意过 Executor 配置?
具体的可配置项可以查看官方文档:
http://tomcat.apache.org/tomcat-9.0-doc/config/executor.html
同时我找到一个可配置的参数的中文说明如下:
注意其中的第一个参数是 className,图片中少了个字母 c。
然后还有两个参数没有介绍,我补充一下:
1.prestartminSpareThreads:boolean 类型,当服务器启动时,是否要创建出最小空闲线程(核心线程)数量的线程,默认值为 false 。
2.threadRenewalDelay:long 类型,当我们配置了
ThreadLocalLeakPreventionListener 的时候,它会监听一个请求是否停止。当线程停止后,如果有需要,会进行重建,为了避免多个线程,该设置可以检测是否有 2 个线程同时被创建,如果是,则会按照该参数,延迟指定时间创建。 如果拒绝,则线程不会被重建。默认为 1000 ms,设定为负值表示不更新。
我们主要关注 className 参数,如果不配置,默认实现是:
org.apache.catalina.core.StandardThreadExecutor
我们先解读一下这个方法(注意,本文中 Tomcat 源码版本号为:10.0.0-M4):
org.apache.catalina.core.StandardThreadExecutor#startInternal
从 123 行到 130 行,就是构建 Tomcat 线程池的地方,很关键,我解读一下:
123行
taskqueue = new TaskQueue(maxQueueSize);
创建一个 TaskQueue 队列,这个队列是继承自 LinkedBlockingQueue 的:
该队列上的注释值得关注一下:
主要是说这是一个专门为线程池设计的一个任务队列。配合线程池使用的时候和普通队列有不一样的地方。
同时传递了一个队列长度,默认为 Integer.MAX_VALUE:
124行
TaskThreadFactory tf = new TaskThreadFactory(namePrefix,daemon,getThreadPriority());
构建一个 ThreadFactory,其三个入参分为如下:
namePrefix:名称前缀。可以指定,其默认是“tomcat-exec-”。
daemon:是否以守护线程模式启动。默认是 true。
priority:线程优先级。是一个 1 到 10 之前的数,默认是 5。
125行
executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), maxIdleTime, TimeUnit.MILLISECONDS,taskqueue, tf);
构建线程池,其 6 个入参分别如下:
这个具体含义我就不解释了,和 JDK 线程池是一样的。
只是给大家看一下默认参数。
另外还需要十分注意的一点是,这里的 ThreadPoolExecuteor 是 Tomcat 的,不是 JDK 的,虽然名字一样。
看一下 Tomcat 的 ThreadPoolExecuteor注释,里面提到了两个点,一是已提交总数,二是拒绝策略。后面都会讲到。
126行
executor.setThreadRenewalDelay(threadRenewalDelay);
设置 threadRenewalDelay 参数。不是本文重点,可以先不关心。
127 - 129行
if (prestartminSpareThreads) { executor.prestartAllCoreThreads(); }
设置是否预启动所有的核心线程池,这个参数在之前文章中也有讲到过。
prestartminSpareThreads 参数默认是 false。但是我觉得这个地方你设置为 true 也是多次一举。完全没有必要。
为什么呢?
因为在 125 行构建线程池的时候已经调用过这个方法了:
从源码可以看出,不管你调用哪一个线程池构造方法,都会去调用 prestartAllCoreThreads 方法。
所以,这算不算 Tomcat 的一个小 Bug 呢?快拿起你的键盘给它提 pr 吧。