前言

在真实的项目中,有一些业务需要及时处理请求并及时返回响应,但是当遇到耗时的业务时,通常会使用异步来实现。实现异步调用的方式有很多,如多线程、定时任务、消息队列等。本文是以多线程的方式来实现,并且基于 springboot2.6.2 的 @Async 来实现。废话不多说,来看代码!!!

摘要:

  • 同步和异步的区别。
  • 不正确使用 @Async 的影响。
  • 自定义 springboot 配置文件
  • 自定义线程池、自定义线程工厂、自定义拒绝策略
  • @Async 配置不同的线程池

一、同步VS异步

同步调用:每行程序需要等待上一行执行完成之后才能继续执行。
异步调用:程序在可以不等待异步语句块执行完或者返回结果才可以顺序执行,等程序结束之后,异步语句块可以继续在后端执行。

二、@Async 可能引入的问题

但是直接使用 @Async 会有风险,如果使用其默认 SimpleAsyncTaskExecutor 线程池,它会不断的创建线程,当并发大的时候会严重影响性能,因为默认的最大线程数是 Interer.MAX_VALUE。(源码见:org.springframework.boot.autoconfigure.task.TaskExecutionProperties.Pool)

es async 线程安全 async 线程池_es async 线程安全


如图,每个任务都会创建一个线程,并发高的时候很容易导致系统奔溃。

es async 线程安全 async 线程池_spring boot_02


如果重写或这自定义线程池则不会出现类似问题,使用的是自定义的线程池。

es async 线程安全 async 线程池_java_03

三、为 @Async 配置线程池或自定义线程池

1. 线程池参数介绍

线程池核心配置参数有7个:核心线程数最大线程数超时时间超时时间单位阻塞队列线程工厂拒绝策略。该配置文件是基于使用 SpringBoot 的线程池配置参数,其中超时时间单位是秒,没有配置线程工厂及拒绝策略,但是多了一个线程前缀的配置(可以自定义线程工厂的方式配置前缀);对于线程工厂及拒绝策略也可以自定义或者枚举的方式进行可配置化。

2. 基于源码配置线程池

在高版本的 springboot 类中 org.springframework.boot.autoconfigure.task.TaskExecutionProperties 中就是线程池的配置属性类,可以对线程池进行配置。根据运行环境可做如下配置

spring:
  task:
    execution:
      thread-name-prefix: THREAD-
      pool:
        core-size: 4
        max-size: 8
        queue-capacity: 16
        keep-alive: 60
        allow-core-thread-timeout: false

多次执行任务,其线程池都在最大线程数之内,能很好的使用线程池

es async 线程安全 async 线程池_线程池_04

3. 自定义线程池

除了使用 springboot 提供的线程池配置,我们还可以自定义线程池。

自定义线程池配置属性
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

/**
 * @author zixing
 */
@Setter
@Getter
@Configuration
@ConfigurationProperties("thead.pool")
public class ThreadPoolConfig {
    /**
     * 核心线程数
     */
    private int corePoolSize;

    /**
     * 最大线程数
     */
    private int maxPoolSize;

    /**
     * 线程空闲时间 秒
     */
    private int keepAliveSeconds;

    /**
     * 阻塞队列容量
     */
    private int queueCapacity;

    /**
     * 线程名称前缀
     */
    private String threadNamePrefix;
}
application.yml 配置
thead:
  pool:
    core-pool-size: 4
    max-pool-size: 8
    queue-capacity: 20
    keep-alive-seconds: 60
    thread-name-prefix: zixing-
自定义线程工厂

可以对增强线程的属性,如绑定线程组等功能,方便更好的管理线程。

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author zixing
 */
@Slf4j
public class MyThreadFactory implements ThreadFactory {
    private final AtomicInteger threadId = new AtomicInteger(1);

    /**
     * 自定义线程工厂
     */
    @Override
    public Thread newThread(Runnable r) {
        log.info("自定义线程工厂");
        return new Thread(r, "MyThread-" + threadId.getAndDecrement()  * 10);
    }
}
自定义拒绝策略

除了四种默认的拒绝策略,也可以自定义拒绝策略。

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * 自定义线程池拒绝策略
 *
 * @author zixing
 */
@Slf4j
public class MyRejectedPolicy implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        log.info(executor + "===" + r.toString() + "=== has bean rejected");
    }
}
创建线程池方式一:

实现 AsyncConfigurer 接口将会是 @Async 默认的线程池。

import ai.zixing.config.ThreadPoolConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import javax.annotation.Resource;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * @author zixing
 *
 * 自定义线程池
 */
@Slf4j
@EnableAsync
@Configuration
public class MyDefinedThreadPool implements AsyncConfigurer {

    @Resource
    private ThreadPoolConfig config;

    @Override
    public Executor getAsyncExecutor() {
        // 创建线程池
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 核心线程数
        executor.setCorePoolSize(config.getCorePoolSize());
        // 最大线程数
        executor.setMaxPoolSize(config.getMaxPoolSize());
        // 阻塞队列容量
        executor.setQueueCapacity(config.getQueueCapacity());
        // 活跃时间-秒
        executor.setKeepAliveSeconds(config.getKeepAliveSeconds());
        // 设置线程前缀 My_Thread_
        executor.setThreadNamePrefix(config.getThreadNamePrefix());
        // 自定义线程工厂
//        executor.setThreadFactory(new MyThreadFactory());
        // 当poolSize已达到maxPoolSize,线程池拒接策略
        // CallerRunsPolicy:不在新线程中执行任务,而是由调用者所在的线程来执行
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
//        executor.setRejectedExecutionHandler(new MyRejectedPolicy());
        // 不是作为Bean的时候,需要自己手动去调用initialize()方法 不然报错
        // 参考 
        executor.initialize();
        return executor;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return (ex, method, params) -> log.error("the thread : {} Execution method {} is error : ",
                Thread.currentThread().getName(), method.getName(), ex);
    }
}
创建线程池方式二:

将线程池直接注入 spring 进行管理。

import ai.zixing.config.ThreadPoolConfig;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import javax.annotation.Resource;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * @author zixing
 */
@Configuration
public class CustomizeThreadPool {

    @Resource
    private ThreadPoolConfig config;

    @Bean("customizeThreadPoolBean")
    public Executor getThreadPoolBean() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(config.getCorePoolSize());
        // 和 ai.zixing.pool.MyDefinedThreadPool.getAsyncExecutor 基本一致
        executor.setMaxPoolSize(config.getMaxPoolSize());
        executor.setQueueCapacity(config.getQueueCapacity());
        executor.setKeepAliveSeconds(config.getKeepAliveSeconds());
//        executor.setThreadNamePrefix(config.getThreadNamePrefix());
        executor.setThreadNamePrefix("customizeThreadPoolBean-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        // 不是作为Bean的时候,需要自己手动去调用initialize()方法 不然报错
        // 参考 
        // 可以不执行 executor.initialize(); 注入 spring 中 spring 会对该线程池初始化
//        executor.initialize();
        return executor;
    }
}

四、测试代码

代码结构

es async 线程安全 async 线程池_线程池_05

模拟业务代码

提供两个接口用于测试

public interface AnalyseService {

    /**
     * 分析处理
     * 
     * @param start start
     * @param end end
     */
    void analyseHandle(String start, String end);

    /**
     * 分析处理
     *
     * @param end end
     */
    void analyseHandle(String end);

    /**
     * 获取数据类别
     *
     * @return String
     */
    String getDataType();
}

为了方便测试,多加一个 manager 类,如果异步方法使用不同的线程池,可以指定,如 @Async(“customizeThreadPoolBean”)

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;

@Slf4j
public abstract class AbstractDataAnalyseManager implements AnalyseService {

    @Async("customizeThreadPoolBean")
    @SneakyThrows
    @Override
    public void analyseHandle(String start, String end) {
        Thread.sleep(2 * 1000);
        log.info(getDataType() + "在 customizeThreadPoolBean 线程池" + Thread.currentThread().getName());
    }

    @SneakyThrows
    @Async
    @Override
    public void analyseHandle(String end) {
        Thread.sleep(3 * 1000);
        log.info(getDataType() + "在 MyDefinedThreadPool 线程池" + Thread.currentThread().getName());
    }
}

模拟多个消费者,只是类名及 dataType 不同,此处以 AaaDataAnalyseServiceImpl 为例

import ai.zixing.service.AbstractDataAnalyseManager;
import org.springframework.stereotype.Service;

/**
 * @author zixing
 */
@Service
public class AaaDataAnalyseServiceImpl extends AbstractDataAnalyseManager {
    @Override
    public String getDataType() {
        return "AAA";
    }
}

接口层

import ai.zixing.service.AnalyseService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.List;

/**
 * @author zixing
 */
@RestController
public class AnalyseController {

    @Resource
    private List<AnalyseService> analyseServiceList;

    @GetMapping("/sayHelloAsync")
    public String sayHello() throws InterruptedException {
        for (AnalyseService analyseService : analyseServiceList) {
            analyseService.analyseHandle(null, null);
            analyseService.analyseHandle(null);
        }
        return "say hello";
    }
}

api调用

GET http://localhost:8088/sayHelloAsync
启动类

需要注意的是需要使用 @EnableAsync 注解

mport org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;

/**
 * @author zixing
 */
@EnableAsync
@SpringBootApplication
public class ThreadPoolApplication {
    public static void main(String[] args) {
        SpringApplication.run(ThreadPoolApplication.class, args);
    }
}
 

总结

其实如果点进去 @EnableAsync 查看源码时,它的注释就有说明创建线程池的这两种方式,有些时候,最好的资料就在源码中,只不过是平时没有注意到。生活中处处有美好的事物,只是缺少发现美的眼睛。

es async 线程安全 async 线程池_spring_06