Java 动态线程池

322 阅读10分钟

前言:
线上项目频繁崩溃,进行项目优化,发现之前项目中异步任务都使用new Thread方式执行,没有统一使用线程池进行管理和监控,new Thread的方式消耗系统资源、不易管理监控甚至在任务大量并发时存在导致系统OOM的风险。现优化使用线程进行统一管理和监控,并扩展研究其他公司成熟优秀的动态线程池的思想和实现。

1.多线程异步执行任务

比较顺序执行与异步执行,使用多线程异步执行任务能够提高效率,降低阻塞

ttt.png

直接创建线程并启动线程执行任务的方式存在弊端

  • 每次new Thread新建线程性能差,线程创建销毁消耗资源
  • 线程缺乏统一的管理,可以无限制新建线程,相互之间竞争,还可能占用过多系统资源导致死机或者OOM(Out of Memory);
  • 缺乏更多的功能,如定时执行、定期执行、线程中断等。

2.线程池

2.1 为什么要使用线程池?

池化,线程的创建和管理交由线程池进行

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

  • 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。

  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

线程池使用广泛,在Spring和很多组件中如:RabbitMQ等广泛使用。

2.2 java中的线程池

java中的线程池及相关内容自1.5引入,位于concurrent包下,ThreadPoolExecutor的UML类图

image.png

此外concurrent包下还有ScheduledThreadPoolExecutor定时任务线程池。

image.png

2.3 线程池最佳实践

2.3.1 使用 ThreadPoolExecutor 的构造函数创建线程池

不要使用Executors来创建线程池:
  • FixedThreadPoolSingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM。

  • CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。

使用 ThreadPoolExecutor 构造函数创建线程池:

线程池构造函数涉及了线程池的核心参数

/**
 * 用给定的初始参数创建一个新的ThreadPoolExecutor。
 */
public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量
						  int maximumPoolSize,//线程池的最大线程数
						  long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间
						  TimeUnit unit,//时间单位
						  BlockingQueue<Runnable> workQueue,//任务队列,用来储存等待执行任务的队列
						  ThreadFactory threadFactory,//线程工厂,用来创建线程
						  RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,可以定制策略来处理任务
						   ) {
	if (corePoolSize < 0 ||
		maximumPoolSize <= 0 ||
		maximumPoolSize < corePoolSize ||
		keepAliveTime < 0)
		throw new IllegalArgumentException();
	if (workQueue == null || threadFactory == null || handler == null)
		throw new NullPointerException();
	this.corePoolSize = corePoolSize;
	this.maximumPoolSize = maximumPoolSize;
	this.workQueue = workQueue;
	this.keepAliveTime = unit.toNanos(keepAliveTime);
	this.threadFactory = threadFactory;
	this.handler = handler;
}
corePoolSize: 核心线程数

线程池中会维护一个最小的线程数量,即使这些线程处于空闲状态,它们也不会被销毁,除非设置了allowCoreThreadTimeOut。allowCoreThreadTimeOut默认为false,不销毁核心线程,设置为true时,当核心线程空闲时也会进行销毁。任务提交到线程池后,首先会检查当前线程数是否达到了corePoolSize,如果没有达到的话,则会创建一个新线程来处理这个任务。线程池创建后如果没有接受到任务,线程池中不会有线程,线程池预热可以使用prestartCoreThread来预热创建一个核心线程或者prestartAllCoreThreads预热创建所有的核心线程。

maximumPoolSize: 最大线程数量

当前线程数达到corePoolSize后,如果继续有任务被提交到线程池,会将任务缓存到工作队列中。如果队列也已满,则会去创建一个新线程来出来这个处理。线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制,这个数量即由maximunPoolSize指定。???

keepAliveTime: 空闲线程存活时间

一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁,这里的指定时间由keepAliveTime来设定。

unit: 空闲线程存活时间单位

keepAliveTime的时间单位。

workQueue: 工作队列

新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。jdk中提供了四种工作队列:

①ArrayBlockingQueue

基于数组的有界阻塞队列,按FIFO排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。

②LinkedBlockingQuene

基于链表的无界阻塞队列(其实最大容量为Interger.MAX),按照FIFO排序。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而基本不会去创建新线程直到maxPoolSize(很难达到Interger.MAX这个数),因此使用该工作队列时,参数maxPoolSize其实是不起作用的。

③SynchronousQuene

一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。

④PriorityBlockingQueue

具有优先级的无界阻塞队列,优先级通过参数Comparator实现。

threadFactory: 线程工厂

创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等。

handler: 拒绝策略

当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,该如何处理呢?这时将执行拒绝策略,拒绝策略实现了RejectedExecutionHandler接口,拒绝任务时将执行rejectedExecution方法,可以自己实现拒绝策略。

public interface RejectedExecutionHandler {
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}

jdk中提供了4种拒绝策略:

①CallerRunsPolicy

在调用者线程中直接执行被拒绝的任务的run方法,除非线程池已经shutdown,则直接抛弃任务。???

②AbortPolicy

该策略下,直接丢弃任务,并抛出RejectedExecutionException异常。

③DiscardPolicy 该策略下,直接丢弃任务,什么都不做。

④DiscardOldestPolicy

该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列。

向线程池提交任务

有两种方式向线程池提交任务

execute 方法提交任务,execute方法定义在Executor接口中,是Executor接口的唯一方法。该方法接受一个runnable,然后返回为空。任务通过execute提交后就基本和主线程脱离关系了。

void execute(Runnable command);

submit 方法提交任务,submit的参数可以是Callable(也可以是Runnable),并且有返回值,返回的是一个Future对象,可以通过future对象的get方法获取任务执行的结果。

<T> Future<T> submit(Callable<T> var1);

<T> Future<T> submit(Runnable var1, T var2);

Future<?> submit(Runnable var1);

线程池通过submit方式提交任务,任务会被封装成FutrueTask这点要注意。 使用submit提交任务还可能存在异常无法抛出的问题。如下代码在execute执行任务时会抛出NullPointerException异常,正常打印异常信息,但是使用submit提交任务日志并没有打印异常相关信息。

threadPoolExecutor.submit(new Runnable() {
    @Override
    public void run() {
        List<String> list = null;
        list.stream().collect(Collectors.toList());
    }
});

submit提交的任务会被包装成FutureTask实例,所以最终是调用FutureTask的run方法,在FutureTask的run方法中对异常进行了捕获,并且没有进行处理和抛出。所以在使用submit提交任务时,需要在任务中进行异常捕获、处理和日志打印。 每次任务都写try catch块如果觉得麻烦,可以设置线程的UncaughtExceptionHandler来处理异常,使用线程池时可以在线程工厂创建线程时统一设置线程的UncaughtExceptionHandler处理异常如下:

@Override
public Thread newThread(Runnable r) {
    Thread t = delegate.newThread(r);
    t.setName(name + "-" + threadNum.incrementAndGet());
    t.setUncaughtExceptionHandler((thread, e) -> {
       // 异常打印和处理
    });
    return t;
}

或者重写线程池的afterExecute方法在每个任务执行完成后进行处理如下:

@Override
protected void afterExecute(Runnable r, Throwable t) {
	super.afterExecute(r, t);
	if (t != null) {
		t.printStackTrace();
	} else {
		if (r instanceof Future<?>) {
			try {
				//get这里会首先检查任务的状态,然后将上面的异常包装成ExecutionException
				Object result = ((Future<?>) r).get();
			} catch (CancellationException ce) {
				t = ce;
			} catch (ExecutionException ee) {
				t = ee.getCause();
				t.printStackTrace();
			} catch (InterruptedException ie) {
				Thread.currentThread().interrupt(); // ignore/reset
			}
		}
	}
}

2.3.2 监测线程池运行状态

线程池监控能帮助发现线程池参数设置不合理的情况,并对线上异步任务执行情况进行监控。

线程池运行状态指标

常用线程池监控指标如下,使用ThreadPoolExecutor的相关api来计算监控指标会一定程度影响线程池性能(某些参数获取时会用到线程池mainLock主锁进行加锁、释放锁),不需要进行全部监控,选取一些核心参数进行监控即可。

  • 线程数相关指标:
    • 核心线程数
    • 活动线程数
    • 最大线程数
    • 线程池活跃度
  • 阻塞队列的相关指标:监控任务积压情况和积压风险
    • 队列大小
    • 当前排队任务数
    • 队列剩余大小
    • 队列使用度
  • 异常指标:
    • 运行时抛出的异常数量
  • 拒绝策略指标:
    • 拒绝策略执行次数
  • 峰值负载指标:
    • 最大线程活跃度
    • 最大队列实用度
  • 任务执行:
    • 任务执行时间
线程池运行状态监控方案
  • 使用ThreadPoolExecutor 预留的钩子函数打印日志进行监控,重写ThreadPoolExecutor的beforeExecute和afterExecute方法,这两个方法会在任务执行前后进行执行,可以在beforeExecute执行时计算相关指标、打印日志并记录任务开始执行时间(使用ThreadLocal配合nanoTime记录),在afterExecute记录任务执行结束时间记录任务执行耗时。
  • 单独使用一个线程作为线程池监控线程定时打印线程池运行指标信息。
SpringBoot 中的 Actuator 组件

2.3.3 建议不同类别的业务用不同的线程池

不同的业务的并发以及对资源的使用情况都不同,一个应用中可创建多个不同用处的线程池。

2.3.4 线程规范命名

设置线程池前缀

  • 使用其他包实现的线程工厂并设置线程池前缀,如guava、hutool等
  • 自己实现 ThreadFactory线程工厂:实现ThreadFactory接口的newThread方法

2.3.5 正确配置线程池参数

线程数太大增加了上下文切换成本,增加线程的执行时间,影响了整体执行效率;线程数太少导致任务堆积在队列中,队列和线程满了新任务无法处理,甚至任务堆积导致OOM。

根据是CPU密集型还是IO密集型任务有线程数计算公式,但实际上部署应用的机器上可能存在不止一个应用,而且一个应用中可能存在多个线程池,实际参数要运行一段时间通过日志/监控数据来进行调整。而且存在应用在运行过程中请求的高并发导致线程池队列爆满,任务大量堆积、新任务被拒绝情况,需要我们能监控到线程池运行状态并适时调整线程池参数。

image.png

3. 动态线程池实现

image.png 参考美团技术团队文章进行实现
Java线程池实现原理及其在美团业务中的实践
美团技术团队的思路是对线程池的核心参数进行自定义配置:
image.png 最主要的三个核心参数是:

  • corePoolSize
  • maximumPoolSize
  • workQueue

这几个核心参数基本决定了线程池对于任务的处理策略

3.1 核心参数设置

corePoolSize的设置

image.png

maximumPoolSize

workQueue任务队列长度设置

LinkedBlockedQueue的capacity参数为final,在初始化后队列的capacity不可再次修改,可以复制LinkedBlockedQueue类代码创建新类ResizableCapabilityLinkedBlockingQueue将capacity改为volatile修饰,并添加getter、setter方法即可。

3.2 实现demo

动态线程池代码

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.*;

/**
 * 线程池
 * <p>继承ThreadPoolExecutor,实现beforeExecute、afterExecute用于进行线程池监控等</p>
 *
 * @author liurong
 * @date 2022/10/9 13:17
 */
@Slf4j
public class DynamicThreadPoolExecutor extends ThreadPoolExecutor {

    /**
     * ThreadLocal记录任务开始时间,计算线程任务执行时间
     */
    private final ThreadLocal<Long> startTime = new ThreadLocal<>();

    /**
     * 构造函数
     *
     * @param corePoolSize    核心线程数
     * @param maximumPoolSize 最大线程数
     * @param keepAliveTime   线程空闲时间
     * @param unit            线程空闲时间单位
     * @param workQueue       任务队列
     * @param threadFactory   线程工厂
     */
    public DynamicThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
        log.info("DynamicThreadPoolExecutor init, corePoolSize:{}, maximumPoolSize:{}, keepAliveTime:{}, unit:{}, workQueue:{}, threadFactory:{}, handler:{}",
                corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, getRejectedExecutionHandler());
    }

    /**
     * 构造函数
     *
     * @param corePoolSize    核心线程数
     * @param maximumPoolSize 最大线程数
     * @param keepAliveTime   线程空闲时间
     * @param unit            线程空闲时间单位
     * @param workQueue       任务队列
     * @param threadFactory   线程工厂
     * @param handler         拒绝策略
     */
    public DynamicThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
        log.info("DynamicThreadPoolExecutor init, corePoolSize:{}, maximumPoolSize:{}, keepAliveTime:{}, unit:{}, workQueue:{}, threadFactory:{}, handler:{}",
                corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
    }

    /**
     * 执行任务的线程在任务执行前后调用beforeExecute和afterExecute方法,用于线程池线程任务执行计时、线程池状态监视、统计
     *
     * @param t 执行任务的线程
     * @param r 执行的任务
     */
    @Override
    protected void beforeExecute(Thread t, Runnable r) {
        // 记录任务开始时间,使用nanoTime()方法,不受系统时间影响
        startTime.set(System.nanoTime());
        BlockingQueue<Runnable> queue = getQueue();
        // 打印线程池状态监视日志
        log.info("线程池状态监控:"
                + "核心线程数:" + getCorePoolSize()
                + ",活动线程数:" + getActiveCount()
                + ",最大线程数:" + getMaximumPoolSize()
                + ",线程池活跃度:" + divide(getActiveCount(), getMaximumPoolSize())
                + ",任务完成数:" + getCompletedTaskCount()
                + ",队列大小:" + (getQueue().size() + getQueue().remainingCapacity())
                + ",当前排队线程数:" + queue.size()
                + ",队列剩余大小:" + queue.remainingCapacity()
                + ",队列使用度:" + divide(queue.size(), queue.size() + queue.remainingCapacity()));
    }

    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        // 记录任务执行结束时间,使用nanoTime()方法,不受系统时间影响
        long endTime = System.nanoTime();
        // 计算任务执行耗时,单位秒
        long taskTimeInSeconds = TimeUnit.SECONDS.convert(endTime - startTime.get(), TimeUnit.NANOSECONDS);
        // remove()方法移除线程本地变量,避免内存泄漏
        startTime.remove();
        log.info("线程池任务执行耗时:" + taskTimeInSeconds + "s");
    }

    /**
     * 在线程池关闭时调用terminated
     */
    @Override
    protected void terminated() {
        super.terminated();
    }

    /**
     * 计算百分比(保留两位小数)
     *
     * @param num1 分子
     * @param num2 分母
     * @return 百分比
     */
    private String divide(int num1, int num2) {
        return String.format("%1.2f%%",
                Double.parseDouble(num1 + "") / Double.parseDouble(num2 + "") * 100);
    }
}

线程池bean配置类

使用Spring自带的ThreadPoolProperty加载yml中线程池配置信息

/**
 * 线程池配置
 *
 * @author liurong
 * @version 1.0
 * @date 2022/10/8 10:46
 */
@Slf4j
@Configuration
public class ThreadPoolConfig {

    /**
     * 线程池配置
     */
    @Resource
    private ThreadPoolProperty threadPoolProperty;

    /**
     * 线程池bean
     *
     * @return ThreadPoolExecutor bean
     */
    @Bean
    public ThreadPoolExecutor threadPoolExecutor() {
        ThreadPoolProperty.Pool pool = threadPoolProperty.getPool();
        String rejectHandlerClassPath = pool.getRejectHandlerClassPath();
        RejectedExecutionHandler handler = null;
        if (StringUtils.isNotEmpty(rejectHandlerClassPath) && ClassUtils.isPresent(rejectHandlerClassPath, ClassUtils.getDefaultClassLoader())) {
            try {
                handler = (RejectedExecutionHandler) Class.forName(rejectHandlerClassPath).newInstance();
            } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
                log.error("RejectedExecutionHandler class not found", e);
            }
        }

        return new DynamicThreadPoolExecutor(pool.getCoreSize(),
                pool.getMaxSize(),
                // https://juejin.cn/post/7052957407793807368 spring yml 解析 duration变量
                pool.getKeepAlive().getSeconds(),
                TimeUnit.SECONDS,
                new ResizableCapabilityLinkedBlockingQueue<>(pool.getQueueCapacity()),
                // hutool 包中的线程工厂
                new NamedThreadFactory(threadPoolProperty.getThreadNamePrefix(), false),
                // 拒绝策略: 最大线程数+队列容量满了之后,再有新任务进来,就会执行拒绝策略,抛出RejectedExecutionException异常
                null != handler ? handler : new ThreadPoolExecutor.AbortPolicy());
    }

}

使用nacos配置中心动态修改线程池配置

nacos 动态线程池配置

image.png

配置监听器

编写配置修改监听器,当监听到线程池配置修改时重设线程池参数

package com.aaron.config.threadpool;

import cn.hutool.setting.yaml.YamlUtil;
import com.alibaba.cloud.nacos.NacosConfigProperties;
import com.alibaba.cloud.nacos.refresh.NacosContextRefresher;
import com.alibaba.fastjson.JSON;
import com.alibaba.nacos.api.annotation.NacosInjected;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.config.annotation.NacosConfigListener;
import com.alibaba.nacos.api.config.listener.Listener;
import com.alibaba.nacos.api.exception.NacosException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
import org.springframework.boot.convert.DurationStyle;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.endpoint.event.RefreshEvent;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.scheduling.annotation.Async;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.Constructor;
import org.yaml.snakeyaml.representer.Representer;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.time.Duration;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 线程池配置修改监听
 * @author liurong
 * @version 1.0
 * @date 2022/10/12 11:10
 */
@Slf4j
@Configuration
@RefreshScope
public class ThreadPoolConfigListener {

    @Resource
    private ThreadPoolExecutor threadPoolExecutor;

    @NacosConfigListener(dataId = "${spring.application.name}-threadpool.yaml")
    public void onConfigEdit(String config) {
        log.info("线程池配置变更:{}", JSON.toJSONString(config));
        // 使用SpringBoot的YamlPropertiesFactoryBean解析yaml
        Properties properties = null;
        try {
            YamlPropertiesFactoryBean yamlPropertiesFactoryBean = new YamlPropertiesFactoryBean();
            yamlPropertiesFactoryBean.setResources(new org.springframework.core.io.ByteArrayResource(config.getBytes()));
            properties = yamlPropertiesFactoryBean.getObject();
        } catch (RuntimeException e) {
            log.error("线程池配置解析失败", e);
            return;
        }

        // 如果新的核心线程数大于之前最大线程数,先设置最大线程数
        int coreSize = 0;
        int maxSize = 0;
        Duration duration = null;
        int queueCapacity = 0;
        try {
            coreSize = Integer.parseInt(properties.getProperty("spring.task.execution.pool.core-size"));
            maxSize = Integer.parseInt(properties.getProperty("spring.task.execution.pool.max-size"));
            duration = DurationStyle.SIMPLE.parse(properties.getProperty("spring.task.execution.pool.keep-alive"));
            queueCapacity = Integer.parseInt(properties.getProperty("spring.task.execution.pool.queue-capacity"));
        } catch (NumberFormatException e) {
            log.error("线程池配置解析失败", e);
            return;
        }

        maxSize = Math.max(coreSize, maxSize);

        if (coreSize >= threadPoolExecutor.getMaximumPoolSize()) {
            threadPoolExecutor.setMaximumPoolSize(maxSize);
            threadPoolExecutor.setCorePoolSize(coreSize);
        }
        // 如果新的最大线程数小于之前的核心线程数,先设置核心线程
        else if (maxSize < threadPoolExecutor.getCorePoolSize()) {
            threadPoolExecutor.setCorePoolSize(coreSize);
            threadPoolExecutor.setMaximumPoolSize(maxSize);
        } else {
            threadPoolExecutor.setCorePoolSize(coreSize);
            threadPoolExecutor.setMaximumPoolSize(maxSize);
        }

        threadPoolExecutor.setKeepAliveTime(duration.getSeconds(), TimeUnit.SECONDS);
        ((ResizableCapabilityLinkedBlockingQueue) threadPoolExecutor.getQueue()).setCapacity(queueCapacity);
        log.info("线程池配置变更成功");
    }
}
测试动态修改线程池参数

/test/testThreadPool提交到线程池2000个任务,然后观察日志,到nacos修改线程池参数,查看日志中线程池运行指标变化

@SneakyThrows
@GetMapping("/test/testThreadPool")
public CommonResult<String> test(Integer num) {
    // num 默认2000
    num = Optional.ofNullable(num).orElse(2000);
    for (int i = 0; i < num; i++) {
        threadPoolExecutor.execute(() -> {
            Random random = new Random();
            try {
                int sleepTime = random.nextInt(11);
                TimeUnit.SECONDS.sleep(sleepTime);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
    }
    return ResultUtils.success();
}

@GetMapping("/test/testNewThread")
public CommonResult<String> testNewThread(Integer num) {
	// num 默认2000
	num = Optional.ofNullable(num).orElse(2000);
	for (int i = 0; i < num; i++) {
		new Thread(() -> {
			Random random = new Random();
			try {
				int sleepTime = random.nextInt(11);
				TimeUnit.SECONDS.sleep(sleepTime);
			} catch (InterruptedException e) {
				throw new RuntimeException(e);
			}
		});
	}
	return ResultUtils.success();
}

image.png

image.png

日志中可以看到动态修改线程池参数成功,并且活动线程从之前的2提升到10 image.png

actuator 查看线程池指标

引入actuator依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

yml配置actuator

management:
  endpoints:
    web:
      exposure:
        include: "*"
  endpoint:
    health:
      show-details: always

实现 HealthIndicator 接口,线程池健康状态监控器

/**
 * 线程池健康状态监控
 * 
 * @author liurong
 * @version 1.0
 * @date 2022/10/18 10:00
 */
public class ThreadPoolHealthContributor implements HealthIndicator {

    private final ThreadPoolExecutor executor;

    public ThreadPoolHealthContributor(ThreadPoolExecutor executor) {
        this.executor = executor;
    }

    @Override
    public Health health() {

        //核心线程数
        int corePoolSize = executor.getCorePoolSize();
        //活跃线程数
        int activeCount = executor.getActiveCount();
        //最大线程数
        int maximumPoolSize = executor.getMaximumPoolSize();
        //当前池中线程数
        int poolSize = executor.getPoolSize();
        // 线程池活跃度
        String activeRate = divide(activeCount, maximumPoolSize);
        //完成的任务数
        long completedTaskCount = executor.getCompletedTaskCount();
        BlockingQueue<Runnable> queue = executor.getQueue();
        //队列中的任务
        int queueSize = queue.size();
        //队列剩余大小
        int remainingCapacity = queue.remainingCapacity();
        //队列使用度
        String queueRate = divide(queue.size(), queue.size() + queue.remainingCapacity());
        //队列总大小
        int queueCapacity = queue.size() + queue.remainingCapacity();
        //线程数历史高峰线
        int largestPoolSize = executor.getLargestPoolSize();

        //如果当前活跃线程数 大于  80%的最大线程数,就认证是down
        double rate = BigDecimal.valueOf(activeCount)
                .divide(BigDecimal.valueOf(maximumPoolSize), 2, BigDecimal.ROUND_HALF_UP)
                .doubleValue();

        Map<String, Object> infoMap = new HashMap<>();
        infoMap.put("核心线程数", corePoolSize);
        infoMap.put("活跃线程数", activeCount);
        infoMap.put("最大线程数", maximumPoolSize);
        infoMap.put("当前池中线程数", poolSize);
        infoMap.put("线程池活跃度", activeRate);
        infoMap.put("线程峰值", largestPoolSize);
        infoMap.put("完成的任务数", completedTaskCount);
        infoMap.put("队列中的任务数", queueSize);
        infoMap.put("队列总大小", queueCapacity);
        infoMap.put("队列剩余大小", remainingCapacity);
        infoMap.put("队列使用度", queueRate);
        if (rate > 0.8) {
            return Health.down().withDetails(infoMap).build();
        } else {
            return Health.up().withDetails(infoMap).build();
        }
    }

    private String divide(int num1, int num2) {
        return String.format("%1.2f%%",
                Double.parseDouble(num1 + "") / Double.parseDouble(num2 + "") * 100);
    }
}

实现CompositeHealthContributor,注册为bean管理监控器

@Component
public class CommonHealthContributor implements CompositeHealthContributor {

    private final Map<String, HealthContributor> healthIndicators = new HashMap<>();

    @Override
    public HealthContributor getContributor(String name) {
        return healthIndicators.get(name);
    }

    @NotNull
    @Override
    public Iterator<NamedContributor<HealthContributor>> iterator() {
        List<NamedContributor<HealthContributor>> contributors = new ArrayList<>();
        healthIndicators.forEach((name, contributor) -> contributors.add(NamedContributor.of(name, contributor)));
        return contributors.iterator();
    }

    public void addHealthIndicator(String name, HealthContributor healthIndicator) {
        healthIndicators.put(name, healthIndicator);
    }
}

在线程池bean注册时添加线程池健康状态监控器

try {
    commonHealthContributor.addHealthIndicator(threadPoolProperty.getThreadNamePrefix(), new ThreadPoolHealthContributor(threadPoolExecutor));
}catch (RuntimeException e){
    log.error("add threadPool HealthIndicator error", e);
}

打开health监控页面:http://host:port/actuator/health 查看线程池健康监控信息

image.png

3.3 动态线程池组件介绍

网上有一些基于美团思路实现的开源动态线程池组件,hippo4j是star数量较多的一个。

HIPPO-4J | HIPPO-4J (hippo4j.cn) 3.1k star Hippo-4J 通过对 JDK 线程池增强,以及扩展三方框架底层线程池等功能,为业务系统提高线上运行保障能力。

  • 🏗 全局管控 - 管理应用线程池实例。
  • ⚡️ 动态变更 - 应用运行时动态变更线程池参数,包括不限于:核心、最大线程数、阻塞队列容量、拒绝策略等。
  • 🐳 通知报警 - 内置四种报警通知策略,线程池活跃度、容量水位、拒绝策略以及任务执行时间超长。
  • 👀 运行监控 - 实时查看线程池运行时数据,最近半小时线程池运行数据图表展示。
  • 👐 功能扩展 - 支持线程池任务传递上下文;项目关闭时,支持等待线程池在指定时间内完成任务。
  • 👯‍♀️ 多种模式 - 内置两种使用模式:依赖配置中心 和 无中间件依赖
  • 🛠 容器管理 - Tomcat、Jetty、Undertow 容器线程池运行时查看和线程数变更。
  • 🌈 中间件适配 - Apache RocketMQ、Dubbo、RabbitMQ、Hystrix 消费线程池运行时数据查看和线程数变更。

运行模式

轻量级依赖配置中心以及无中间件依赖版本

image.png

简单使用

无中间件依赖版本

下载最新包

Releases · opengoofy/hippo4j (github.com) 解压,执行/conf/hippo4j_manager.sql数据库脚本创建应用数据库

/conf/application.properties 修改数据库配置

bin/startup.cmd 运行启动,默认端口6691

进入后台管理页面

localhost:6691/index.html,用户名密码:admin 123456

项目使用

引入依赖

```
<dependency>
   <groupId>cn.hippo4j</groupId>
   <artifactId>hippo4j-spring-boot-starter</artifactId>
   <version>1.4.1</version>
</dependency>
```

项目配置:

spring:
    dynamic:
      thread-pool:
        # 服务端地址
        server-addr: http://localhost:6691
        # 用户名
        username: admin
        # 密码
        password: 123456
        # 租户 id, 对应 tenant 表
        namespace: threadPoolManager
        # 项目 id, 对应 item 表
        item-id: ${spring.application.name}

hippo4j配置类

/**
 * Hippo4j 动态线程池配置类
 * 
 * @author liurong
 */
@Configuration
@EnableDynamicThreadPool
public class Hippo4jThreadPoolConfig {

    @Bean
    @DynamicThreadPool
    public ThreadPoolExecutor hippo4jThreadPool() {
        String threadPoolId = "hippo4j-thread-pool";
        return ThreadPoolBuilder.builder()
                .threadFactory(threadPoolId)
                .threadPoolId(threadPoolId)
                .dynamicPool()
                .build();
    }
}
线程池配置和监控

新建租户 image.png

新建项目

image.png

新建线程池管理

image.png

根据租户、项目、线程池查看hippo4j管理的线程池实例 image.png

查看线程池实时监控 image.png

实时修改线程池参数

image.png 可以看到调整过后监控图像曲线的变化(证明hippo4j设置最大线程数也不会影响workers,所以动态调整时建议将核心线程数、最大线程数设置成相同值) image.png

附: 参考文档:
Java线程池实现原理及其在美团业务中的实践

如何设置线程池参数?美团给出了一个让面试官虎躯一震的回答。 (qq.com)

线程池最佳实践!安排! - 掘金 (juejin.cn)

今天,说一说线程池 “动态更新”(一)-开源基础软件社区-51CTO.COM

简介 | HIPPO-4J (hippo4j.cn)