一、前言

在我们平时的开发过程中,会遇到一个流程中各个逻辑并非紧密相连的业务,比如查询文章详情后更新文章阅读量,其实对于用户来说,最关心的是能快速获取文章,至于更新文章阅读量,用户可能并不关心。

因此,对于这类逻辑并非紧密相连的业务,可以将逻辑进行拆分,让用户无需等待更新文章阅读量,查询时直接返回文章信息,缩短同步请求的耗时,进一步提升了用户体验。要实现这种效果,我们可能立刻想到,采用异步线程来更新文章阅读量。

二、方案实践

Spring提供了@Async注解,将其添加在方法上,可以自动实现这个方法异步调用的效果,但是需要在启动类或者配置类上加上@EnableAsync注解来注明启动异步操作。

示例操作如下:

1.创建服务类

创建一个普通方法和一个异步方法

package com.example.dataproject.service;

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

import java.util.concurrent.TimeUnit;

/**
 * @author qx
 * @date 2024/8/12
 * @des
 */
@Service
@Slf4j
public class BusinessService {

    public void readNews() {
        log.info("查询新闻");
    }

    @Async
    public void updateCount() {
        try {
            TimeUnit.SECONDS.sleep(1L);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        log.info("更新阅读量");
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.

2.创建控制层

package com.example.dataproject.controller;

import com.example.dataproject.service.BusinessService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author qx
 * @date 2024/8/12
 * @des
 */
@RestController
@Slf4j
public class NewsController {

    @Autowired
    private BusinessService businessService;

    @GetMapping("/readNews")
    public String read() {
        log.info("开始阅读");
        businessService.readNews();
        //调用异步方法
        businessService.updateCount();
        log.info("结束阅读");
        return "success";
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.

3.在启动类上加上@EnableAsync注解

@SpringBootApplication
@EnableAsync
public class DataProjectApplication {

    public static void main(String[] args) {
        SpringApplication.run(DataProjectApplication.class, args);

    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

4.启动程序进行测试

SpringBoot实现方法异步调用的正确方式_SpringBoot

我们在控制台上看到更新阅读量的日志属于子线程的操作并没有阻塞主线程的执行,异步调用效果明显。其他操作属于主线程。

SpringBoot实现方法异步调用的正确方式_异步_02

SpringBoot实现方法异步调用的正确方式_线程池_03

三、自定义线程池

被@Async注解标注的方法,默认采用SimpleAsyncTaskExecutor线程池来执行。这个线程池有一个特点就是,每来一个请求任务就会创建一个线程去执行,如果系统不断的创建线程,最终可能导致 CPU 和内存占用过高,引发OutOfMemoryError错误。

实际上,SimpleAsyncTaskExecutor并不是严格意义上的线程池,因为它达不到线程复用的效果。因此,在实际开发中,建议自定义线程池来执行异步方法。

实现步骤也很简单,首先,注入自定义线程池对象到 Spring Bean 中;然后,在@Async注解中指定线程池,即可实现指定线程池来异步执行任务。

1.配置自定义线程池类

spring.task.executor.core-pool-size=100
spring.task.executor.max-pool-size=300
spring.task.executor.queue-capacity=99999
  • 1.
  • 2.
  • 3.
package com.example.dataproject.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

/**
 * @author qx
 * @date 2024/8/1
 * @des 线程池配置类
 */
@Configuration
public class ExecutorConfig {

    @Value("${spring.task.executor.core-pool-size}")
    private int corePoolSize;

    @Value("${spring.task.executor.max-pool-size}")
    private int maxPoolSize;

    @Value("${spring.task.executor.queue-capacity}")
    private int queueCapacity;

    @Bean("customExecutor")
    public ThreadPoolTaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        //设置核心线程数
        executor.setCorePoolSize(corePoolSize);
        //设置最大线程数
        executor.setMaxPoolSize(maxPoolSize);
        //设置队列大小
        executor.setQueueCapacity(queueCapacity);
        // 设置线程活跃时间(秒)
        executor.setKeepAliveSeconds(30);
        // 设置线程名前缀+分组名称
        executor.setThreadNamePrefix("customThread-");
        executor.setThreadGroupName("customThreadGroup");
        // 所有任务结束后关闭线程池
        executor.setWaitForTasksToCompleteOnShutdown(true);
        // 初始化
        executor.initialize();
        return executor;
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.

2.在异步方法上指定线程池

package com.example.dataproject.service;

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

import java.util.concurrent.TimeUnit;

/**
 * @author qx
 * @date 2024/8/12
 * @des
 */
@Service
@Slf4j
public class BusinessService {

    public void readNews() {
        log.info("查询新闻");
    }

    @Async(value = "customExecutor")
    public void updateCount() {
        try {
            TimeUnit.SECONDS.sleep(1L);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        log.info("更新阅读量");
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.

3.服务测试

最后启动服务,重新发起请求,输出结果如下:

SpringBoot实现方法异步调用的正确方式_SpringBoot_04

从日志上可以清晰的看到,更新方法采用了customThread-1线程来异步执行任务。

四、全局默认线程池

从上文中我们得知,被@Async注解标注的方法,默认采用SimpleAsyncTaskExecutor线程池来执行。

某些场景下,如果希望系统统一采用自定义配置线程池来执行任务,但是又不想在被@Async注解的方法上一个一个的去指定线程池,如何处理呢?

此时可以重写AsyncConfigurer接口的getAsyncExecutor()方法,配置默认线程池。

实现也很简单,示例如下:

package com.example.dataproject.config;

import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

/**
 * @author qx
 * @date 2024/8/12
 * @des 全局默认线程池
 */
@Configuration
public class AsyncConfig implements AsyncConfigurer {

    @Value("${spring.task.executor.core-pool-size}")
    private int corePoolSize;

    @Value("${spring.task.executor.max-pool-size}")
    private int maxPoolSize;

    @Value("${spring.task.executor.queue-capacity}")
    private int queueCapacity;

    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 设置核心线程数
        executor.setCorePoolSize(corePoolSize);
        // 设置最大线程数
        executor.setMaxPoolSize(maxPoolSize);
        // 设置队列大小
        executor.setQueueCapacity(queueCapacity);
        // 设置线程活跃时间(秒)
        executor.setKeepAliveSeconds(30);
        // 设置线程名前缀+分组名称
        executor.setThreadNamePrefix("asyncThread-");
        executor.setThreadGroupName("asyncThreadGroup");
        // 所有任务结束后关闭线程池
        executor.setWaitForTasksToCompleteOnShutdown(true);
        // 初始化
        executor.initialize();
        return executor;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return (throwable, method, obj) -> {
            System.out.println("异步调用,异常捕获---------------------------------");
            System.out.println("Exception message - " + throwable.getMessage());
            System.out.println("Method name - " + method.getName());
            for (Object param : obj) {
                System.out.println("Parameter value - " + param);
            }
            System.out.println("异步调用,异常捕获---------------------------------");
        };
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.

接下来去掉服务类中的指定线程池

package com.example.dataproject.service;

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

import java.util.concurrent.TimeUnit;

/**
 * @author qx
 * @date 2024/8/12
 * @des
 */
@Service
@Slf4j
public class BusinessService {

    public void readNews() {
        log.info("查询新闻");
    }

    @Async
    public void updateCount() {
        try {
            TimeUnit.SECONDS.sleep(1L);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        log.info("更新阅读量");
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.

我们重新启动程序进行测试

SpringBoot实现方法异步调用的正确方式_线程池_05

从日志上可以清晰的看到,更新方法采用了asyncThread-1线程来异步执行任务。

五、注意事项

在使用@Async注解的时候,可能会失效,总结下来主要有以下几个场景。

  • 场景一:异步方法使用static修饰,此时不会生效
  • 场景二:调用的异步方法,在同一个类中,此时不会生效。因为 Spring 在启动扫描时会为其创建一个代理类,而同类调用时,还是调用本身的代理类的,所以还是同步调用
  • 场景三:异步类没有使用@Component、@Service等注解,导致 spring 无法扫描到异步类,此时不会生效
  • 场景四:采用SpringBoot框架开发时,没有在启动类上添加@EnableAsync注解,此时不会生效

其次,关于事务机制的一些问题,直接在@Async方法上再标注@Transactional是会失效的,此时可以在方法内采用编程式事务方式来提交数据。但是,在@Async方法调用其它类的方法上标注的@Transactional注解有效。