填个坑!再谈线程池动态调整那点事。(上)

简介: 填个坑!再谈线程池动态调整那点事。(上)

你好呀,我是歪歪。

前几天和一个大佬聊天的时候他说自己最近在做线程池的监控,刚刚把动态调整的功能开发完成。

想起我之前写过这方面的文章,就找出来看了一下:《如何设置线程池参数?美团给出了一个让面试官虎躯一震的回答。》

然后给我指出了一个问题,我仔细思考了一下,好像确实是留了一个坑。

为了更好的描述这个坑,我先给大家回顾一下线程池动态调整的几个关键点。

首先,为什么需要对线程池的参数进行动态调整呢?

因为随着业务的发展,有可能出现一个线程池开始够用,但是渐渐的被塞满的情况。

这样就会导致后续提交过来的任务被拒绝。

没有一劳永逸的配置方案,相关的参数应该是随着系统的浮动而浮动的。

所以,我们可以对线程池进行多维度的监控,比如其中的一个维度就是队列使用度的监控。

当队列使用度超过 80% 的时候就发送预警短信,提醒相应的负责人提高警惕,可以到对应的管理后台页面进行线程池参数的调整,防止出现任务被拒绝的情况。

以后有人问你线程池的各个参数怎么配置的时候,你先把分为 IO 密集型和 CPU 密集型的这个八股文答案背完之后。

加上一个:但是,除了这些方案外,我在实际解决问题的时候用的是另外一套方案”。

然后把上面的话复述一遍。

image.png

那么线程池可以修改的参数有哪些呢?

正常来说是可以调整核心线程数和最大线程数的。

线程池也直接提供了其对应的 set 方法:

image.png

但是其实还有一个关键参数也是需要调整的,那就是队列的长度。

哦,对了,说明一下,本文默认使用的队列是 LinkedBlockingQueue

其容量是 final 修饰的,也就是说指定之后就不能修改:

image.png

所以队列的长度调整起来稍微要动点脑筋。

至于怎么绕过 final 这个限制,等下就说,先先给大家上个代码。

我一般是不会贴大段的代码的,但是这次为什么贴了呢?

因为我发现我之前的那篇文章就没有贴,之前写的代码也早就不知道去哪里了。

所以,我又苦哈哈的敲了一遍...

image.png

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 方法。

image.png

感觉非常的简单,就能实现 capacity 参数的动态变更。

但是,我当时写的时候就感觉是有坑的。

毕竟这么简单的话,为什么官方要把它给设计为 final 呢?

image.png



坑在哪里?


关于 LinkedBlockingQueue 的工作原理就不在这里说了,都是属于必背八股文的内容。

主要说一下前面提到的场景中,如果我直接把 final 修饰符去掉,并提供其对应的 get/set 方法,这样的做法坑在哪里。

先说一下,如果没有特殊说明,本文中的源码都是 JDK 8 版本。

我们看一下这个 put 方法:

image.png

主要看这个被框起来的部分。

while 条件里面的 capacity 我们知道代表的是当前容量。

那么 count.get 是个什么玩意呢?

image.png

就是当前队列里面有多少个元素。

count.get == capacity 就是说队列已经满了,然后执行 notFull.await() 把当前的这个 put 操作挂起来。

来个简单的例子验证一下:

image.png

申请一个长度为 5 的队列,然后在循环里面调用 put 方法,当队列满了之后,程序就阻塞住了。

通过 dump 当前线程可以知道主线程确实是阻塞在了我们前面分析的地方:


image.png

image.png

现在我们把队列换成我修改后的队列验证一下。

下面验证程序的思路就是在一个子线程中执行队列的 put 操作,直到容量满了,被阻塞。

然后主线程把容量修改为 100。

image.png

上面的程序其实我想要达到的效果是当容量扩大之后,子线程不应该继续阻塞。

但是经过前面的分析,我们知道这里并不会去唤醒子线程。

所以,输出结果是这样的:

image.png

子线程还是阻塞着,所以并没有达到预期。

所以这个时候我们应该怎么办呢?

当然是去主动唤醒一下啦。

也就是修改一下 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 方法:

image.png

唤醒一下被 park 起来的线程。

如果看到这里你觉得你有点懵,不知道 LinkedBlockingQueue 的这几个玩意是干啥的:

image.png

赶紧去花一小时时间补充一下 LinkedBlockingQueue 相关的知识点。这样玩意,面试也经常考的。

好了,我们说回来。

修改完我们自定义的 setCapacity 方法后,再次执行程序,就出现了我们预期的输出:

image.png

目录
相关文章
|
2月前
|
Prometheus 监控 Cloud Native
JAVA线程池监控以及动态调整线程池
【10月更文挑战第22天】在 Java 中,线程池的监控和动态调整是非常重要的,它可以帮助我们更好地管理系统资源,提高应用的性能和稳定性。
209 64
|
3月前
|
监控 Java
G1垃圾回收器的哪些配置参数对性能影响最大,如何调整这些参数
G1垃圾回收器的哪些配置参数对性能影响最大,如何调整这些参数
239 0
|
8月前
|
安全 Java 调度
【C/C++ 线程池设计思路 】设计与实现支持优先级任务的C++线程池 简要介绍
【C/C++ 线程池设计思路 】设计与实现支持优先级任务的C++线程池 简要介绍
241 2
|
8月前
|
Java 关系型数据库 MySQL
线程池高级理论总结
线程池高级理论总结
58 0
|
8月前
|
监控 Java API
【C/C++ 线程池设计思路】如何在C++跨平台应用中精准调节线程池:一个动态适应策略的实践指南
【C/C++ 线程池设计思路】如何在C++跨平台应用中精准调节线程池:一个动态适应策略的实践指南
341 0
|
监控 Java 调度
设置动态线程池参数原理与实践
设置动态线程池参数原理与实践
205 0
设置动态线程池参数原理与实践
|
消息中间件 缓存 资源调度
动态调整线程池参数实践
动态调整线程池参数实践
1236 0
|
缓存 NoSQL Java
线程池:第三章:线程池的手写改造和拒绝策略以及线程池配置合理线程数
线程池:第三章:线程池的手写改造和拒绝策略以及线程池配置合理线程数
199 0
线程池:第三章:线程池的手写改造和拒绝策略以及线程池配置合理线程数
|
缓存 Java 调度
1线程池的七大参数以及他们之间是怎么工作的?
1线程池的七大参数以及他们之间是怎么工作的?
|
监控 Java
填个坑!再谈线程池动态调整那点事。(中)
填个坑!再谈线程池动态调整那点事。(中)
406 0
填个坑!再谈线程池动态调整那点事。(中)

热门文章

最新文章

相关实验场景

更多