Springboot线程池最佳实践

一、问题描述

频繁的创建、销毁线程和线程池,会给系统带来额外的开销。未经池化及统一管理的线程,则会导致系统内线程数上限不可控

例如如下代码,每次发送邮件都会创建一个新的线程池,并且业务结束之后线程池也未随之销毁。

public static boolean sendMail(MailInfo mailInfo, MailServerInfo mailServerInfo) {
    try {
        ExecutorService executorService = Executors.newCachedThreadPool();
        Future<Boolean> future = executorService.submit(() -> {
            try {
                return asyncSendEmail(mailInfo, mailServerInfo);
            } catch (Exception e) {
                return false;
            }
        });
        return future.get(10, TimeUnit.SECONDS);
    } catch (Exception e) {
        LOG.error(e.getMessage(), e);
        return false;
    }
}

这种情况下,随着访问数增加,系统内线程数持续增长,CPU负载逐步提高。极端情况下,甚至可能会导致CPU资源被吃满,整个服务不可用。

为了解决上述问题,可增加统一线程池配置,替换掉自建线程和线程池。

二、自建线程、线程池

2.1. 线程池

2.1.1. 自建线程池的方式

  1. 使用ThreadPoolExecutor手动创建线程池。

    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(20, 100, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
    
  2. 使用线程池工厂类Executors,调用newFixedThreadPool、newSingleThreadExecutor等方法创建线程池。

    ExecutorService executorService = Executors.newFixedThreadPool(10);
    

2.1.2. 自建线程池的识别

答:搜索关键字:ThreadPoolExecutor、Executors、ExecutorService。

2.2. 线程

2.2.1. 自建线程的方式

  1. 继承Thread类,实现run方法。

    public class Test {
        public static void main(String[] args) {
            new MyThread().start();
        }
    }
    
    class MyThread extends Thread {
        @Override
        public void run() {
            // 业务逻辑
        }
    }
    
  2. new一个Thread类,传入Runnable实例。

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
    			// 业务逻辑
            }
        }).start();
    }
    
  3. new一个Thread类,传入FutureTask实例,可通过get方法获取返回值。

    public static void main(String[] args) throws Exception {
        FutureTask<Object> futureTask = new FutureTask<>(new Callable<Object>() {
            @Override
            public Object call() throws Exception {
                // 业务逻辑
                return null;
            }
        });
        Thread thread = new Thread(futureTask);
        thread.start();
        Object result = futureTask.get();
    }
    

2.2.2. 自建线程的识别

答:搜索关键字"new Thread"、“Thread”。

2.3. Async注解

2.3.1. Async概述

Spring@Async注解应用于方法上,可将该方法变为一个异步方法。该注解默认使用SpringSimpleAsyncTaskExecutor 线程池,但它并不是一个真正的线程池,每次方法调用,都会创建一个新线程,因此不推荐直接使用

如果一定要使用,可在注解中指定自定义线程池,示例代码如下:

@Async("asyncServiceExecutor")
public Future<List<OpenstackFlavorInfo4Get>> getExchangeDatas() {
    // 业务逻辑
}

三、线程池配置推荐

本文档基于Spring框架提供的线程池,提供两种线程池配置方案,详细介绍如下。

3.1. 常用线程池

该线程池核心线程20,缓存队列60,最大线程100。

核心线程满载之后,新任务会先放到缓存队列中。适用于触发频率高、耗时短的任务。因为这种任务会迅速释放掉核心线程,然后继续消化缓存队列中的任务。这样既能保证队列中任务不会等太久,也降低了频繁创建销毁线程的资源开销。

/**
 * SpringBoot线程池配置类
 */
@Configuration
public class SpringBootThreadPoolConfiguration {

    /**
     * 常用线程池:用以执行触发频率高、耗时短的任务
     */
    @Bean("commonTaskExecutor")
    public ThreadPoolTaskExecutor commonTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 核心线程:核心线程一直存活
        executor.setCorePoolSize(30);
        // 队列容量:任务将队列塞满之后,扩展核心线程,线程总数最多不超过最大线程数
        executor.setQueueCapacity(60);
        // 最大线程
        executor.setMaxPoolSize(100);
        // 闲置线程存活时长
        executor.setKeepAliveSeconds(60);
        // 拒绝策略:任务超出线程池容量后,新任务交还主线程处理
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        // 线程名前缀
        executor.setThreadNamePrefix("common-task-worker-");
        // 初始化线程池
        executor.initialize();
        return executor;
    } 
}

3.2. 长耗时线程池

对于耗时长的任务,如果使用常用线程池处理,会存在耗时长任务逐渐打满线程池负载,而触发频率高、耗时短的任务没有空闲线程可用的可能性。因此有必要单独为耗时长的任务配置一个线程池。

该线程池核心线程20,缓存队列0,最大线程100。

核心线程满载之后,会立即开启新线程。适用于触发频率低、耗时长的任务。因为这类任务占用单个线程时间长,缓存队列为0,有利于缩短任务平均执行时间。

/**
 * SpringBoot线程池配置类
 */
@Configuration
public class SpringBootThreadPoolConfiguration {   
    /**
     * 高负载线程池:用以执行出发频率低、耗时长任务
     */
    @Bean("expensiveTaskExecutor")
    public ThreadPoolTaskExecutor expensiveTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 核心线程:核心线程一直存活
        executor.setCorePoolSize(20);
        // 队列容量:任务将队列塞满之后,扩展核心线程,线程总数最多不超过最大线程数
        executor.setQueueCapacity(0);
        // 最大线程
        executor.setMaxPoolSize(100);
        // 闲置线程存活时长
        executor.setKeepAliveSeconds(60);
        // 拒绝策略:任务超出线程池容量后,新任务交还主线程处理
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        // 线程名前缀
        executor.setThreadNamePrefix("expensive-task-worker-");
        // 初始化线程池
        executor.initialize();
        return executor;
    }
}

注:以上两个线程池配置,是基于网络模块自身业务特征,在实践中总结出来的,并不完全适用于其他模块,请根据各自模块业务特征进行微调。

四、FAQ

4.1. Spring的线程池ThreadPoolTaskExecutor,和JDK自带线程池ThreadPoolExecutor的区别是什么?

答:ThreadPoolTaskExecutorThreadPoolExecutor的基础上进行了一层封装,增加了submitListenable方法。

submitListenable方法返回的ListenableFuture接口对象,可以用来添加线程执行完毕后成功和失败的回调方法。

Future.get()方法相比,通过回调的方式,不会阻塞主线程,线程执行结果或抛出的异常通过回调函数异步处理。

示例代码如下:

final ListenableFuture<String> listenableFuture = threadPoolTaskExecutor
    .submitListenable(() -> {
    	TimeUnit.SECONDS.sleep(2);
    	return "result";
	});
listenableFuture.addCallback(data -> log.info(data), e -> log.error(e.getMessage(), e));

4.2. 是否有办法动态调整线程池配置?

答:可以将线程池的三个核心参数corePoolSizequeueCapacitymaxPoolSize,配置到配置中心中,项目内线程池从配置中心中取这个配置。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值