你好呀,我是歪歪。
前几天和一个大佬聊天的时候他说自己最近在做线程池的监控,刚刚把动态调整的功能开发完成。
想起我之前写过这方面的文章,就找出来看了一下:《如何设置线程池参数?美团给出了一个让面试官虎躯一震的回答。》
然后给我指出了一个问题,我仔细思考了一下,好像确实是留了一个坑。
为了更好的描述这个坑,我先给大家回顾一下线程池动态调整的几个关键点。
首先,为什么需要对线程池的参数进行动态调整呢?
因为随着业务的发展,有可能出现一个线程池开始够用,但是渐渐的被塞满的情况。
这样就会导致后续提交过来的任务被拒绝。
没有一劳永逸的配置方案,相关的参数应该是随着系统的浮动而浮动的。
所以,我们可以对线程池进行多维度的监控,比如其中的一个维度就是队列使用度的监控。
当队列使用度超过 80% 的时候就发送预警短信,提醒相应的负责人提高警惕,可以到对应的管理后台页面进行线程池参数的调整,防止出现任务被拒绝的情况。
以后有人问你线程池的各个参数怎么配置的时候,你先把分为 IO 密集型和 CPU 密集型的这个八股文答案背完之后。
加上一个:但是,除了这些方案外,我在实际解决问题的时候用的是另外一套方案”。
然后把上面的话复述一遍。
那么线程池可以修改的参数有哪些呢?
正常来说是可以调整核心线程数和最大线程数的。
线程池也直接提供了其对应的 set 方法:
但是其实还有一个关键参数也是需要调整的,那就是队列的长度。
哦,对了,说明一下,本文默认使用的队列是 LinkedBlockingQueue
。
其容量是 final 修饰的,也就是说指定之后就不能修改:
所以队列的长度调整起来稍微要动点脑筋。
至于怎么绕过 final 这个限制,等下就说,先先给大家上个代码。
我一般是不会贴大段的代码的,但是这次为什么贴了呢?
因为我发现我之前的那篇文章就没有贴,之前写的代码也早就不知道去哪里了。
所以,我又苦哈哈的敲了一遍...
import cn.hutool.core.thread.NamedThreadFactory; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class ThreadChangeDemo { public static void main(String[] args) { dynamicModifyExecutor(); } private static ThreadPoolExecutor buildThreadPoolExecutor() { return new ThreadPoolExecutor(2, 5, 60, TimeUnit.SECONDS, new ResizeableCapacityLinkedBlockingQueue<>(10), new NamedThreadFactory("why技术", false)); } private static void dynamicModifyExecutor() { ThreadPoolExecutor executor = buildThreadPoolExecutor(); for (int i = 0; i < 15; i++) { executor.execute(() -> { threadPoolStatus(executor,"创建任务"); try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } }); } threadPoolStatus(executor,"改变之前"); executor.setCorePoolSize(10); executor.setMaximumPoolSize(10); ResizeableCapacityLinkedBlockingQueue<Runnable> queue = (ResizeableCapacityLinkedBlockingQueue)executor.getQueue(); queue.setCapacity(100); threadPoolStatus(executor,"改变之后"); } /** * 打印线程池状态 * * @param executor * @param name */ private static void threadPoolStatus(ThreadPoolExecutor executor, String name) { BlockingQueue<Runnable> queue = executor.getQueue(); System.out.println(Thread.currentThread().getName() + "-" + name + "-:" + "核心线程数:" + executor.getCorePoolSize() + " 活动线程数:" + executor.getActiveCount() + " 最大线程数:" + executor.getMaximumPoolSize() + " 线程池活跃度:" + divide(executor.getActiveCount(), executor.getMaximumPoolSize()) + " 任务完成数:" + executor.getCompletedTaskCount() + " 队列大小:" + (queue.size() + queue.remainingCapacity()) + " 当前排队线程数:" + queue.size() + " 队列剩余大小:" + queue.remainingCapacity() + " 队列使用度:" + divide(queue.size(), queue.size() + queue.remainingCapacity())); } private static String divide(int num1, int num2) { return String.format("%1.2f%%", Double.parseDouble(num1 + "") / Double.parseDouble(num2 + "") * 100); } }
当你把这个代码粘过去之后,你会发现你没有 NamedThreadFactory
这个类。
没有关系,我用的是 hutool 工具包里面的,你要是没有,可以自定义一个,也可以在构造函数里面不传,这不是重点,问题不大。
问题大的是 ResizeableCapacityLinkedBlockingQueue
这个玩意。
它是怎么来的呢?
在之前的文章里面提到过:
就是把 LinkedBlockingQueue 粘贴一份出来,修改个名字,然后把 Capacity 参数的 final 修饰符去掉,并提供其对应的 get/set 方法。
感觉非常的简单,就能实现 capacity 参数的动态变更。
但是,我当时写的时候就感觉是有坑的。
毕竟这么简单的话,为什么官方要把它给设计为 final 呢?
坑在哪里?
关于 LinkedBlockingQueue
的工作原理就不在这里说了,都是属于必背八股文的内容。
主要说一下前面提到的场景中,如果我直接把 final 修饰符去掉,并提供其对应的 get/set 方法,这样的做法坑在哪里。
先说一下,如果没有特殊说明,本文中的源码都是 JDK 8 版本。
我们看一下这个 put 方法:
主要看这个被框起来的部分。
while 条件里面的 capacity 我们知道代表的是当前容量。
那么 count.get 是个什么玩意呢?
就是当前队列里面有多少个元素。
count.get == capacity 就是说队列已经满了,然后执行 notFull.await()
把当前的这个 put 操作挂起来。
来个简单的例子验证一下:
申请一个长度为 5 的队列,然后在循环里面调用 put 方法,当队列满了之后,程序就阻塞住了。
通过 dump 当前线程可以知道主线程确实是阻塞在了我们前面分析的地方:
现在我们把队列换成我修改后的队列验证一下。
下面验证程序的思路就是在一个子线程中执行队列的 put 操作,直到容量满了,被阻塞。
然后主线程把容量修改为 100。
上面的程序其实我想要达到的效果是当容量扩大之后,子线程不应该继续阻塞。
但是经过前面的分析,我们知道这里并不会去唤醒子线程。
所以,输出结果是这样的:
子线程还是阻塞着,所以并没有达到预期。
所以这个时候我们应该怎么办呢?
当然是去主动唤醒一下啦。
也就是修改一下 setCapacity 的逻辑:
public void setCapacity(int capacity) { final int oldCapacity = this.capacity; this.capacity = capacity; final int size = count.get(); if (capacity > size && size >= oldCapacity) { signalNotFull(); } }
核心逻辑就是发现如果容量扩大了,那么就调用一下 signalNotFull
方法:
唤醒一下被 park 起来的线程。
如果看到这里你觉得你有点懵,不知道 LinkedBlockingQueue 的这几个玩意是干啥的:
赶紧去花一小时时间补充一下 LinkedBlockingQueue 相关的知识点。这样玩意,面试也经常考的。
好了,我们说回来。
修改完我们自定义的 setCapacity 方法后,再次执行程序,就出现了我们预期的输出: