Haoyuan's blog Haoyuan's blog
首页
导航站
  • Java基础

    • Java基础
    • Java集合
    • Java反射
    • JavaJUC
    • JavaJVM
  • Java容器

    • JavaWeb
  • Java版本新特性

    • Java新特性
  • SQL 数据库

    • MySQL
    • Oracle
  • NoSQL 数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • ActiveMQ
    • RabbitMQ
    • RocketMQ
    • Kafka
  • 进阶服务

    • Nginx
  • Spring
  • Spring Boot
  • Spring Security
  • Spring Cloud
  • 设计模式
  • 算法
  • 知识
  • 管理

    • Maven
    • Git
  • 部署

    • Linux
    • Docker
    • Jenkins
    • Kubernetes
  • 进阶

    • TypeScript
  • 框架

    • React
    • Vue2
    • Vue3
  • 轮子工具
  • 项目工程
  • 友情链接
  • 本站

    • 分类
    • 标签
    • 归档
  • 我的

    • 收藏
    • 关于
留言区
GitHub (opens new window)

Somnus Haoyuan

Word is cheap, show me the code.
首页
导航站
  • Java基础

    • Java基础
    • Java集合
    • Java反射
    • JavaJUC
    • JavaJVM
  • Java容器

    • JavaWeb
  • Java版本新特性

    • Java新特性
  • SQL 数据库

    • MySQL
    • Oracle
  • NoSQL 数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • ActiveMQ
    • RabbitMQ
    • RocketMQ
    • Kafka
  • 进阶服务

    • Nginx
  • Spring
  • Spring Boot
  • Spring Security
  • Spring Cloud
  • 设计模式
  • 算法
  • 知识
  • 管理

    • Maven
    • Git
  • 部署

    • Linux
    • Docker
    • Jenkins
    • Kubernetes
  • 进阶

    • TypeScript
  • 框架

    • React
    • Vue2
    • Vue3
  • 轮子工具
  • 项目工程
  • 友情链接
  • 本站

    • 分类
    • 标签
    • 归档
  • 我的

    • 收藏
    • 关于
留言区
GitHub (opens new window)
  • Spring

  • Spring MVC

  • Spring Boot

    • Spring Boot - 生命周期与事件
    • Spring Boot - 事件驱动
    • Spring Boot - Bean 加载方式
    • Spring Boot - Aware
    • Spring Boot - 多环境配置
    • Spring Boot - 定时任务
    • Spring Boot - 异步任务
      • 异步任务
        • 什么叫异步
      • Java 线程处理异步
      • SpringBoot 异步任务
        • 异步任务相关限制
        • 自定义异步线程池
        • 应用层级
        • 方法层级
        • 异常处理
    • Spring Boot - 内置日志
    • Spring Boot - 函数式 Web
    • Spring Boot - 响应式远程调用
    • Spring Boot - 接口文档
    • Spring Boot - 单元测试
    • Spring Boot - 内容协商
    • Spring Boot - 参数校验
    • Spring Boot - RestTemplate
    • Spring Boot - 映射请求
    • Spring Boot - 请求参数接收
    • Spring Boot - 通用响应类
    • Spring Boot - 全局异常处理
  • Spring Security

  • Spring Cloud

  • Spring生态
  • Spring Boot
Haoyuan
2023-10-30
目录

Spring Boot - 异步任务

  • 异步任务
    • 什么叫异步
  • Java 线程处理异步
  • SpringBoot 异步任务
    • 异步任务相关限制
    • 自定义异步线程池
    • 异常处理

# 异步任务

有时候,前端可能提交了一个耗时任务,如果后端接收到请求后,直接执行该耗时任务,那么前端需要等待很久一段时间才能接受到响应。如果该耗时任务是通过浏览器直接进行请求,那么浏览器页面会一直处于转圈等待状态。

事实上,当后端要处理一个耗时任务时,通常都会将耗时任务提交到一个异步任务中进行执行,此时前端提交耗时任务后,就可直接返回,进行其他操作。

# 什么叫异步

异步:如果方法中有休眠任务,不用等任务执行完,直接执行下一个任务

简单来说:客户端发送请求,可以跳过方法,执行下一个方法,如果其中一个A方法有休眠任务,不需要等待,直接执行下一个方法,异步任务(A 方法)会在后台得到执行,等 A 方法的休眠时间到了再去执行 A 方法。

同步:一定要等任务执行完了,得到结果,才执行下一个任务。

# Java 线程处理异步

在 Java 中,开启异步任务最常用的方式就是开辟线程执行异步任务,如下所示:

@RestController
@RequestMapping("async")
public class AsyncController {
  @GetMapping("/")
  public String index() {
    new Thread(new Runnable() {
      @Override
      public void run() {
        try {
          // 模拟耗时操作
          Thread.sleep(TimeUnit.SECONDS.toMillis(5));
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    }).start();
    return "consuming time behavior processing!";
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

这时浏览器请求 localhost:8080/async/,就可以很快得到响应,并且耗时任务会在后台得到执行。

一般来说,前端不会关注耗时任务结果,因此前端只需负责提交该任务给到后端即可。但是如果前端需要获取耗时任务结果,则可通过 Future 等方式将结果返回,详细内容如下

public class MyReturnableTask implements Callable<String> {
    @Override
    public String call() throws Exception {
        long startTime = System.currentTimeMillis();
        System.out.println(Thread.currentThread().getName()+"线程运行开始");
        Thread.sleep(5000);
        System.out.println(Thread.currentThread().getName()+"线程运行结束");
        return "result";
    }
}
1
2
3
4
5
6
7
8
9
10
@GetMapping("/task")
public void task() throws ExecutionException, InterruptedException {
  MyReturnableTask myReturnableTask = new MyReturnableTask();
  FutureTask<String> futureTask = new FutureTask<String>(myReturnableTask);
  Thread thread = new Thread(futureTask, "returnableThread");
  thread.start();
  String s = futureTask.get();
  System.out.println(s);
}
1
2
3
4
5
6
7
8
9

事实上,在 Spring Boot 中,我们不需要手动创建线程异步执行耗时任务,因为 Spring 框架已提供了相关异步任务执行解决方案,本文主要介绍下在 Spring Boot 中执行异步任务的相关内容。

# SpringBoot 异步任务

在主程序使用注解 @EnableAsync 开启异步任务支持。

@SpringBootApplication
@EnableAsync // 开启异步任务支持
public class ApplicationStarter {
  public static void main(String[] args) {
      SpringApplication.run(ApplicationStarter.class,args);
  }
}
1
2
3
4
5
6
7

使用 @Async 注解标记要进行异步执行的方法

@Service
public class AsyncServiceImpl {
    // 使用 @Async 注解标记的方法 会提交到一个异步任务中进行执行,第一次不会执行该方法
    // 如果不添加该注解,controller 中调用该方法会等待 5 秒在响应
    @Async
    public void t1() {
        // 模拟耗时任务
        try {
            Thread.sleep(TimeUnit.SECONDS.toMillis(5));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 因为该异步方法中使用了休眠,所以过 5 秒才会执行下面代码
        System.out.println("异步方法中:耗时时间已走完");
    }

    @Async
    public Future<String> t2(){
        // 模拟耗时任务
        try {
            Thread.sleep(TimeUnit.SECONDS.toMillis(5));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return new AsyncResult<>("async tasks done!");
    }
}
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

被 @Async 注解的方法可以接受任意类型参数,但只能返回 void 或 Future 类型数据。

所以当异步方法返回数据时,需要使用 Future 包装异步任务结果,上述代码使用 AsyncResult 包装异步任务结果,AsyncResult 间接继承 Future,是 Spring 提供的一个可用于追踪异步方法执行结果的包装类。其他常用的 Future 类型还有 Spring 4.2 提供的 ListenableFuture,或者 JDK 8 提供的 CompletableFuture,这些类型可提供更丰富的异步任务操作。

如果前端需要获取耗时任务结果,则异步任务方法应当返回一个 Future 类型数据,此时 Controller 相关接口需要调用该 Future 的 get() 方法获取异步任务结果,get() 方法是一个阻塞方法,因此该操作相当于将异步任务转换为同步任务,浏览器同样会面临我们前面所讲的转圈等待过程,但是异步执行还是有他的好处的,因为我们可以控制 get() 方法的调用时序,因此可以先执行其他一些操作后,最后再调用 get() 方法。

# 异步任务相关限制

被 @Async 注解的异步任务方法存在相关限制:

  • 被 @Async 注解的方法必须是 public 的,这样方法才可以被代理

  • 不能在同一个类中调用 @Async 方法,因为同一个类中调用会绕过方法代理,调用的是实际的方法

  • 被 @Async 注解的方法不能是 static

  • @Async 注解不能与 Bean 对象的生命周期回调函数(比如 @PostConstruct)一起注解到同一个方法中

  • 异步类必须注入到 Spring IOC 容器中(也即异步类必须被 @Component / @Service 等进行注解)

  • 其他类中使用异步类对象必须通过 @Autowired 等方式进行注入,不能手动 new 对象

# 自定义异步线程池

默认情况下,Spring 会自动搜索相关线程池定义:要么是一个唯一 TaskExecutor Bean 实例,要么是一个名称为 taskExecutor 的 Executor Bean 实例。如果这两个 Bean 实例都不存在,就会使用 SimpleAsyncTaskExecutor 来异步执行被 @Async 注解的方法。

综上,可以知道,默认情况下,Spring 使用的 Executor 是 SimpleAsyncTaskExecutor,SimpleAsyncTaskExecutor 每次调用都会创建一个新的线程,不会重用之前的线程。很多时候,这种实现方式不符合我们的业务场景,因此通常我们都会自定义一个 Executor 来替换 SimpleAsyncTaskExecutor。

对于自定义 Executor(自定义线程池),可以分为如下两个层级:

  • 应用层级:即全局生效的 Executor。依据 Spring 默认搜索机制,其实就是配置一个全局唯一的 TaskExecutor 实例或者一个名称为 taskExecutor 的Executor实例即可
  • 方法层级:即为单独一个或多个方法指定运行线程池,其他未指定的异步方法运行在默认线程池

# 应用层级

实现 AsyncConfigurer 接口,此时 @Async 方法默认就会运行在该 Executor 中。

@Configuration
public class ExcuterConfig implements AsyncConfigurer {
     /**
     * 自定义线程池
     */
    @Bean
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 设置核心线程数
        int cores = Runtime.getRuntime().availableProcessors();
        executor.setCorePoolSize(cores);
        // 设置最大线程数
        executor.setMaxPoolSize(20);
        // 等待所有任务结束后再关闭线程池
        executor.setWaitForTasksToCompleteOnShutdown(true);
        // 设置线程默认前缀名
        executor.setThreadNamePrefix("Application-Level-Async-");
        executor.setQueueCapacity(10);
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
        return executor;
    }
  
    /**
     * 自定义异常处理器
     */
    @Bean
    @Override
    public AsyncUncaughtExceptionHandler asyncUncaughtExceptionHandler() {
        AsyncUncaughtExceptionHandler syncUncaughtExceptionHandler = (ex, method, params) -> ex.printStackTrace();
        return syncUncaughtExceptionHandler;
    }
}
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

# 方法层级

@Configuration
public class ExcuterConfig {
  
  @Bean("asyncExecutor")
  public TaskExecutor getAsyncExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    // 设置核心线程数
    executor.setCorePoolSize(4);
    // 设置最大线程数
    executor.setMaxPoolSize(20);
    // 等待所有任务结束后再关闭线程池
    executor.setWaitForTasksToCompleteOnShutdown(true);
    // 设置线程默认前缀名
    executor.setThreadNamePrefix("Method-Level-Async1-");
    return executor;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

使用

@Service
public class AsyncService {
    @Async("asyncExecutor")
    public void t1() throws InterruptedException {
        // 模拟耗时任务
        Thread.sleep(TimeUnit.SECONDS.toMillis(5));
    }
}
1
2
3
4
5
6
7
8

可以看到 @Async 里使用了 asyncExecutor 名的 Bean,也就是上面方法层级定义的线程池,如果使用 @Async 不指定名字,则使用默认的方法层级的线程池。

# 异常处理

看上面的 应用层级 的代码就有异常处理。

编辑此页 (opens new window)
#Spring Boot
更新时间: 2025/04/06, 01:04:59
Spring Boot - 定时任务
Spring Boot - 内置日志

← Spring Boot - 定时任务 Spring Boot - 内置日志→

最近更新
01
技术随笔 - Element Plus 修改包名 原创
11-02
02
Reactor - 扩展性
11-02
03
Reactor - 最佳实践
11-02
更多文章>
Theme by Vdoing | Copyright © 2021-2025 Somnus Haoyuan | MIT License
京公网安备 11010802034042号 京ICP备2025120692号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式