再聊聊拒绝策略
拒绝策略需要看这个方法:
org.apache.tomcat.util.threads.ThreadPoolExecutor#execute(java.lang.Runnable, long, java.util.concurrent.TimeUnit)
看一下该方法上的注释:
如果队列满了,则会等待指定时间后再次放入队列。
如果再次放入队列的时候还是满的,则抛出拒绝异常。
这个逻辑就类似于你去上厕所,发现坑位全都被人占着。这个时候你的身体告诉你,你括弧肌最多还能在忍一分钟。
于是,你掐着表在门口,深呼吸,闭眼冥想,等了一分钟。
运气好的,再去一看:哎,有个空的坑位了,赶紧占着。
运气不好,再去一看:哎,还是没有位置,怎么办呢?抛异常吧。具体怎么抛就不说了,自行想象。
所以我们看看这个地方,Tomcat 的代码是怎么实现的:
catch 部分首先判断队列是不是 Tomcat 的自定义队列。如果是,则进入这个 if 分支。
关键的逻辑就在这个 if 判断里面了。
可以看到 172 行:
if (!queue.force(command, timeout, unit))
调用了队列的 force 方法。我们知道 BlockingQueue 是没有 force 方法的。
所以这个force 是 Tomcat 自定义队列特有的方法:
public boolean force(Runnable o, long timeout, TimeUnit unit) throws InterruptedException { if (parent == null || parent.isShutdown()) throw new RejectedExecutionException(sm.getString("taskQueue.notRunning")); //forces the item onto the queue, to be used if the task is rejected return super.offer(o,timeout,unit); }
进去一看发现:害,也不过如此嘛。就是对原生 offer 方法的一层包装而已。
如果成功加入队列则万事大吉,啥事没有。
如果没有成功加入队列,则抛出异常,并维护 submittedCount 参数。
前面说过:submittedCount 参数 = 队列中的任务个数 + 正在运行的任务数。
所以,这里需要进行减一操作。
拒绝策略就说完了。
但是这个地方的源码是我带你找到的。如果你想自己找到应该怎么操作呢?
你想啊,你是想测试拒绝策略。那只要触发其拒绝策略就行了。
比如下面这样:
给一个只能容纳 450 个任务的线程池提交 500 个任务。
然后就会抛出这个异常:
就会找到 Tomcat 线程池的 174 行:
然后你打上一个断点,玩去吧。
那我们刚刚说的,可以在门口等一分钟再进坑是怎么回事呢?
我们把参数告诉线程池就可以了,比如下面这样:
然后再去运行,因为队列满了后,触发拒绝异常,然后等 3 秒再去提交任务。而我们提交的一个任务 2 秒就能被执行完。
所以,这个场景下,所有的任务都会被正常执行。
现在你知道为了把你给它的任务尽量快速、全部的执行完成,Tomcat有多努力了吗?
小彩蛋
在看 Tomcat 自定义队列的时候我发现了作者这样的注释:
这个地方作用是把 forcedRemainingCapacity 参数设置为 0。
这个参数是在什么时候设置的呢?
就是下面这个关闭方法的时候:
org.apache.tomcat.util.threads.ThreadPoolExecutor#contextStopping
可以看到,调用 setCorePoolSize 方法之前,作者直接把 forcedRemainingCapacity 参数设置为了 0。
注释上面写的原因是JDK ThreadPoolExecutor.setCorePoolSize 方法会去检查 remainingCapacity 是否为 0。
至于为什么会去做这样的检查,Tomcat 的作者两次表示:I don't see why。I did not understand why。
so,他 fake 了 condition。
总之就是他说他也不明白为什么JDK 线程池 setCorePoolSize 方法调小核心线程池的时候要的限制队列剩余长度为 0 ,反正这样写就对了。
别问,问就是规定。
于是我去看了 JDK 线程池的 setCorePoolSize 方法,发现这个限制是在 jdk 1.6 里有,1.6 之后的版本对线程池进行了大规模的重构,取消了这个限制:
那 Tomcat 直接设置为 0 会带来什么问题呢?
正常的逻辑是队列剩余大小 = 队列长度 - 队列里排队的任务数。
而当你对其线程池(队列长度为300)进行监控的时候正常情况应该是这样:
但是当你调用 contextStopping 方法后可能会出现这样的问题:
很明显不符合上面的算法了。
好了,如果你们以后需要对 Tomcat 的线程池进行监控,且 JDK 版本在 1.6版本以上。那你可以去掉这个限制,以免误报警。
好了,恭喜你,朋友。又学到了一个基本用不上的知识点,奇怪的知识又增加了一点点。