除了改 setCapacity 方法之外,我在写文章的时候不经意间还触发了另外一个答案:
在调用完 setCapacity 方法之后,再次调用 put 方法,也能得到预期的输出:
我们观察 put 方法就能发现其实道理是一样的:
当调用完 setCapacity 方法之后,再次调用 put 方法,由于不满足标号为 ① 的代码的条件,所以就不会被阻塞。
于是可以顺利走到标号为 ② 的地方唤醒被阻塞的线程。
所以也就变相的达到了改变队列长度,唤醒被阻塞的任务目的。
而究根结底,就是需要执行一次唤醒的操作。
那么那一种优雅一点呢?
那肯定是第一种把逻辑封装在 setCapacity 方法里面操作起来更加优雅。
第二种方式,大多适用于那种“你也不知道为什么,反正这样写程序就是正常了”的情况。
现在我们知道在线程池里面动态调整队列长度的坑是什么了。
那就是队列满了之后,调用 put 方法的线程就会被阻塞住,即使此时另外的线程调用了 setCapacity 方法,改变了队列长度,如果没有线程再次触发 put 操作,被阻塞的线程也不会被唤醒。
是不是?
了不了解?
对不对?
这是不对的,朋友们。
看到前面内容,频频点头的朋友,要注意了。
这地方要开始转弯了。
开始转弯
线程池里面往队列里面添加对象的时候,用的是 offer 命令,并没有用 put 命令:
我们看看 offer 命令在干啥事儿:
队列满了之后,直接返回 false,不会出现阻塞的情况。
也就是说,线程池中根本就不会出现我前面说的需要唤醒的情况,因为根本就没有阻塞中的线程。
在和大佬交流的过程中,他提到了一个 VariableLinkedBlockingQueue
的东西。
这个类位于 MQ 包里面,我前面提到的 setCapacity 方法的修改方式就是在它这里学来的:
同时,项目里面也用到了它的 put 方法:
所以,它是有可能出现我们前面分析的情况,有需要被唤醒的线程。
但是,你想想,线程池里面并没有使用 put 方法,是不是就刚好避免这样的情况?
是的,确实是。
但是,不够严谨,如果知道有问题了的话,为什么要留个坑在这里呢?
你学 MQ 的 VariableLinkedBlockingQueue 考虑的周全一点,就算 put 方法阻塞的时候也能用,它不香吗?
写到这里其实好像除了让你熟悉一下 LinkedBlockingQueue 外,似乎是一个没啥卵用的知识点,
但是,我能让这个没有卵用的知识点起到大作用。
因为这其实是一个小细节。
假设我出去面试,在面试的时候提到动态调整方法的时候,在不经意间拿捏一下这个小细节,即使我没有真的落地过动态调整,但是我提到这样的一个小细节,就显得很真实。
面试官一听:很不错,有整体,有局部,应该是假不了。
在 VariableLinkedBlockingQueue 里面还有几处细节,拿 put 方法来说:
判断条件从 count.get() >= capacity
变成了 count.get() = capacity
,目的是为了支持 capacity 由大变小的场景。
这样的地方还有好几处,就不一一列举了。
魔鬼,都在细节里面。
同学们得好好的拿捏一下。
JDK bug
其实原计划写到前面,就打算收尾了,因为我本来就只是想补充一下我之前没有注意到的细节。
但是,我手贱,跑到 JDK bug 列表里面去搜索了一下 LinkedBlockingQueue,想看看还有没有什么其他的收获。
我是万万没想到,确实是有一点意外收获的。
首先是这一个 bug ,它是在 2019-12-29 被提出来的:
看标题的意思也是想要给 LinkedBlockingQueue 赋能,可以让它的容量进行修改。
加上他下面的场景描述,应该也想要和线程池配合,找到队列的抓手,下钻到底层逻辑,联动监控系统,拉通配置页面,打出一套动态适应的组合拳。
但是官方并没有采纳这个建议。
回复里面说写 concurrent 包的这些哥们对于在并发类里面加东西是非常谨慎的。他们觉得给 ThreadPoolExecutor 提供可动态修改的特性会带来或者已经带来众多的 bug 了。
我理解就是简单一句话:建议还是不错的,但是我不敢动。并发这块,牵一发动全身,不知道会出些什么幺蛾子。
所以要实现这个功能,还是得自己想办法。
这里也就解释了为什么用 final 去修饰了队列的容量,毕竟把功能缩减一下,出现 bug 的几率也少了很多。
第二个 bug 就有意思了,和我们动态调整线程池的需求非常匹配:
https://bugs.openjdk.java.net/browse/JDK-8241094
这是一个 2020 年 3 月份提出的 bug,描述的是说在更新线程池的核心线程数的时候,会抛出一个拒绝异常。
在 bug 描述的那部分他贴了很多代码,但是他给的代码写的很复杂,不太好理解。
好在 Martin 大佬写了一个简化版,一目了然,就好理解的多:
这段代码是干了个啥事儿呢,简单给大家汇报一下。
首先 main 方法里面有个循环,循环里面是调用了 test 方法,当 test 方法抛出异常的时候循环结束。
然后 test 方法里面是每次都搞一个新的线程池,接着往线程池里面提交队列长度加最大线程数个任务,最后关闭这个线程池。
同时还有另外一个线程把线程池的核心线程数从 1 修改为 5。
你可以打开前面提到的 bug 链接,把这段代码贴出来跑一下,非常的匪夷所思。
Martin 大佬他也认为这是一个 BUG.
说实在的,我跑了一下案例,我觉得这应该算是一个 bug,但是经过 Doug Lea 老爷子的亲自认证,他并不觉得这是一个 Bug。
主要是这个 bug 确实也有点超出我的认知,而且在链接中并没有明确的说具体原因是什么,导致我定位的时间非常的长,甚至一度想要放弃。
但是最终定位到问题之后也是长叹一口:害,就这?没啥意思。
先看一下问题的表现是怎么样的: