【SpringBoot技术专题】「Async&Future」异步编程机制以及功能分析讲解

简介: 【SpringBoot技术专题】「Async&Future」异步编程机制以及功能分析讲解

本文内容


  • Future 模式介绍以及核心思想
  • 核心线程数、最大线程数的区别,队列容量代表什么;
  • ThreadPoolTaskExecutor 饱和策略;
  • SpringBoot 异步编程实战,搞懂代码的执行逻辑。




Future 模式


  • 异步编程在处理耗时操作以及多任务处理的场景下非常有用,我们可以更好的让我们的系统利用好机器的CPU和内存,提高它们的利用率。


  • 多线程设计模式有很多种,Future模式是多线程开发中较为常见的一种设计模式,本文也是基于这种模式来说明 SpringBoot 对于异步编程的知识。


实战之前我先简单介绍一下Future模式的核心思想吧!。



Future的核心思想


Future模式的核心思想是异步调用 。当我们执行一个方法时,假如这个方法中有多个耗时的任务需要同时去做,而且又不着急等待这个结果时可以让客户端立即返回然后,后台慢慢去计算任务。当然你也可以选择等这些任务都执行完了,再返回给客户端。



SpringBoot 异步编程实战


如果我们需要在Spring/SpringBoot实现异步编程的话,通过Spring提供的两个注解会让这件事情变的非常简单。


  1. @EnableAsync:通过在配置类或者Main类上加@EnableAsync开启对异步方法的支持。
  2. @Async 可以作用在类上或者方法上,作用在类上代表这个类的所有方法都是异步方法。



TaskExecutor


  • 很多人对于TaskExecutor 不是太了解,所以我们花一点篇幅先介绍一下这个东西。从名字就能看出它是任务的执行者,它领导执行着线程来处理任务,就像司令官一样,而我们的线程就好比一只只军队一样,这些军队可以异步对敌人进行打击。
  • Spring提供了TaskExecutor接口作为任务执行者的抽象,它和java.util.concurrent包下的Executor接口很像。稍微不同的 TaskExecutor接口用到了 Java 8 的语法@FunctionalInterface声明这个接口是一个函数式接口。
org.springframework.core.task.TaskExecutor
@FunctionalInterface
public interface TaskExecutor extends Executor {
    void execute(Runnable var1);
}
复制代码


如果没有自定义Executor, Spring 将创建一个 SimpleAsyncTaskExecutor 并使用它。



自定义AsyncConfigurer


import org.springframework.context.annotation.Bean;
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 java.util.concurrent.Executor;
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
  private static final int CORE_POOL_SIZE = 6;
  private static final int MAX_POOL_SIZE = 10;
  private static final int QUEUE_CAPACITY = 100;
  @Bean
  public Executor taskExecutor() {
    // Spring 默认配置是核心线程数大小为1,最大线程容量大小不受限制,队列容量也不受限制。
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    // 核心线程数
    executor.setCorePoolSize(CORE_POOL_SIZE);
    // 最大线程数
    executor.setMaxPoolSize(MAX_POOL_SIZE);
    // 队列大小
    executor.setQueueCapacity(QUEUE_CAPACITY);
    // 当最大池已满时,此策略保证不会丢失任务请求,但是可能会影响应用程序整体性能。
    executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
    executor.setThreadNamePrefix("My ThreadPoolTaskExecutor-");
    executor.initialize();
    return executor;
  }
}
复制代码



ThreadPoolTaskExecutor常见概念


  • Core Pool Size : 核心线程数线程数定义了最小可以同时运行的线程数量。
  • Queue Capacity : 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,信任就会被存放在队列中。
  • Maximum Pool Size : 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。


一般情况下不会将队列大小设为:Integer.MAX_VALUE,也不会将核心线程数和最大线程数设为同样的大小,这样的话最大线程数的设置都没什么意义了,你也无法确定当前CPU和内存利用率具体情况如何。


如果队列已满并且当前同时运行的线程数达到最大线程数的时候,如果再有新任务过来会发生什么呢?Spring默认使用的是 ThreadPoolExecutor.AbortPolicy(ThreadPoolExecutor 将抛出 RejectedExecutionException 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。)


对于可伸缩的应用程序,建议使用 ThreadPoolExecutor.CallerRunsPolicy,当最大池被填满时,此策略为我们提供可伸缩队列。



ThreadPoolTaskExecutor 饱和策略定义:


如果当前同时运行的线程数量达到最大线程数量时,ThreadPoolTaskExecutor 定义一些策略:


  • ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理。
  • ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务。您不会任务请求。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。
  • 另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。
  • ThreadPoolExecutor.DiscardPolicy: 不处理新任务,直接丢弃掉。
  • ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。
  1. 编写一个异步的方法



给这个方法加上了@Async注解来告诉 Spring 它是一个异步的方法。另外,这个方法的返回值CompletableFuture.completedFuture(results)这代表我们需要返回结果,也就是说程序必须把任务执行完成之后再返回给用户。



请留意completableFutureTask方法中的第一行打印日志这句代码,后面分析程序中会用到,很重要!

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
@Service
public class AsyncService {
  private static final Logger logger = LoggerFactory.getLogger(AsyncService.class);
  private List<String> movies =
      new ArrayList<>(
          Arrays.asList(
              "Forrest Gump",
              "Titanic",
              "Spirited Away",
              "The Shawshank Redemption",
              "Zootopia",
              "Farewell ",
              "Joker",
              "Crawl"));
  /** 示范使用:找到特定字符/字符串开头的电影 */
  @Async
  public CompletableFuture<List<String>> completableFutureTask(String start) {
    // 打印日志
    logger.warn(Thread.currentThread().getName() + "start this task!");
    // 找到特定字符/字符串开头的电影
    List<String> results =
        movies.stream().filter(movie -> movie.startsWith(start)).collect(Collectors.toList());
    // 模拟这是一个耗时的任务
    try {
      Thread.sleep(1000L);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    //返回一个已经用给定值完成的新的CompletableFuture。
    return CompletableFuture.completedFuture(results);
  }
}
复制代码



  1. 测试编写的异步方法
@RestController
@RequestMapping("/async")
public class AsyncController {
  @Autowired 
  AsyncService asyncService;
  @GetMapping("/movies")
  public String completableFutureTask() throws ExecutionException, InterruptedException {
    //开始时间
    long start = System.currentTimeMillis();
    // 开始执行大量的异步任务
    List<String> words = Arrays.asList("F", "T", "S", "Z", "J", "C");
    List<CompletableFuture<List<String>>> completableFutureList = words.stream()
            .map(word -> asyncService.completableFutureTask(word))
            .collect(Collectors.toList());
    // CompletableFuture.join() 方法可以获取他们的结果并将结果连接起来
    List<List<String>> results = completableFutureList.stream().map(CompletableFuture::join).collect(Collectors.toList());
    // 打印结果以及运行程序运行花费时间
    System.out.println("Elapsed time: " + (System.currentTimeMillis() - start));
    return results.toString();
  }
}
复制代码

请求这个接口,控制台打印出下面的内容:

2019-10-01 13:50:17.007  WARN 18793 --- [lTaskExecutor-1] g.j.a.service.AsyncService               : My ThreadPoolTaskExecutor-1start this task!
2019-10-01 13:50:17.007  WARN 18793 --- [lTaskExecutor-6] g.j.a.service.AsyncService               : My ThreadPoolTaskExecutor-6start this task!
2019-10-01 13:50:17.007  WARN 18793 --- [lTaskExecutor-5] g.j.a.service.AsyncService               : My ThreadPoolTaskExecutor-5start this task!
2019-10-01 13:50:17.007  WARN 18793 --- [lTaskExecutor-4] g.j.a.service.AsyncService               : My ThreadPoolTaskExecutor-4start this task!
2019-10-01 13:50:17.007  WARN 18793 --- [lTaskExecutor-3] g.j.a.service.AsyncService               : My ThreadPoolTaskExecutor-3start this task!
2019-10-01 13:50:17.007  WARN 18793 --- [lTaskExecutor-2] g.j.a.service.AsyncService               : My ThreadPoolTaskExecutor-2start this task!
Elapsed time: 1010
复制代码


首先我们可以看到处理所有任务花费的时间大概是 1 s。这与我们自定义的

ThreadPoolTaskExecutor 有关,我们配置的核心线程数是 6 ,然后通过通过下面的代码模拟分配了 6 个任务给系统执行。这样每个线程都会被分配到一个任务,每个任务执行花费时间是 1 s ,所以处理 6 个任务的总花费时间是 1 s。


List<String> words = Arrays.asList("F", "T", "S", "Z", "J", "C");  
List<CompletableFuture<List<String>>> completableFutureList =
        words.stream()
            .map(word -> asyncService.completableFutureTask(word))
            .collect(Collectors.toList());
复制代码

试着去把核心线程数的数量改为 3 ,再次请求这个接口你会发现处理所有任务花费的时间大概是 2 s。




特殊情况 无需返回值

另外,从上面的运行结果可以看出,当所有任务执行完成之后才返回结果。这种情况对应于我们需要返回结果给客户端请求的情况下,假如我们不需要返回任务执行结果给客户端的话呢? 就比如我们上传一个大文件到系统,上传之后只要大文件格式符合要求我们就上传成功。普通情况下我们需要等待文件上传完毕再返回给用户消息,但是这样会很慢。采用异步的话,当用户上传之后就立马返回给用户消息,然后系统再默默去处理上传任务。这样也会增加一点麻烦,因为文件可能会上传失败,所以系统也需要一点机制来补偿这个问题,比如当上传遇到问题的时候,发消息通知用户。

下面会演示一下客户端不需要返回结果的情况:


将completableFutureTask方法变为 void 类型
@Async
public void completableFutureTask(String start) {
  ......
  //这里可能是系统对任务执行结果的处理,比如存入到数据库等等......
  //doSomeThingWithResults(results);
}
复制代码



Controller 代码修改如下:
@GetMapping("/movies")
  public String completableFutureTask() throws ExecutionException, InterruptedException {
    // Start the clock
    long start = System.currentTimeMillis();
    // Kick of multiple, asynchronous lookups
    List<String> words = Arrays.asList("F", "T", "S", "Z", "J", "C");
        words.stream()
            .forEach(word -> asyncService.completableFutureTask(word));
    // Wait until they are all done
    // Print results, including elapsed time
    System.out.println("Elapsed time: " + (System.currentTimeMillis() - start));
    return "Done";
  }
复制代码



请求这个接口,控制台打印出下面的内容:

Elapsed time: 0
2019-10-01 14:02:44.052  WARN 19051 --- [lTaskExecutor-4] g.j.a.service.AsyncService               : My ThreadPoolTaskExecutor-4start this task!
2019-10-01 14:02:44.052  WARN 19051 --- [lTaskExecutor-3] g.j.a.service.AsyncService               : My ThreadPoolTaskExecutor-3start this task!
2019-10-01 14:02:44.052  WARN 19051 --- [lTaskExecutor-2] g.j.a.service.AsyncService               : My ThreadPoolTaskExecutor-2start this task!
2019-10-01 14:02:44.052  WARN 19051 --- [lTaskExecutor-1] g.j.a.service.AsyncService               : My ThreadPoolTaskExecutor-1start this task!
2019-10-01 14:02:44.052  WARN 19051 --- [lTaskExecutor-6] g.j.a.service.AsyncService               : My ThreadPoolTaskExecutor-6start this task!
2019-10-01 14:02:44.052  WARN 19051 --- [lTaskExecutor-5] g.j.a.service.AsyncService               : My ThreadPoolTaskExecutor-5start this task!
复制代码

可以看到系统会直接返回给用户结果,然后系统才真正开始执行任务。



相关文章
|
27天前
|
Java 关系型数据库 MySQL
创建一个SpringBoot项目,实现简单的CRUD功能和分页查询
【9月更文挑战第6天】该内容介绍如何使用 Spring Boot 实现具备 CRUD 功能及分页查询的项目。首先通过 Spring Initializr 创建项目并选择所需依赖;其次配置数据库连接,并创建实体类与数据访问层;接着构建服务层处理业务逻辑;最后创建控制器处理 HTTP 请求。分页查询可通过添加 URL 参数实现。
|
4天前
|
存储 前端开发 Java
Spring Boot 集成 MinIO 与 KKFile 实现文件预览功能
本文详细介绍如何在Spring Boot项目中集成MinIO对象存储系统与KKFileView文件预览工具,实现文件上传及在线预览功能。首先搭建MinIO服务器,并在Spring Boot中配置MinIO SDK进行文件管理;接着通过KKFileView提供文件预览服务,最终实现文档管理系统的高效文件处理能力。
|
1天前
|
缓存 NoSQL Java
Springboot自定义注解+aop实现redis自动清除缓存功能
通过上述步骤,我们不仅实现了一个高度灵活的缓存管理机制,还保证了代码的整洁与可维护性。自定义注解与AOP的结合,让缓存清除逻辑与业务逻辑分离,便于未来的扩展和修改。这种设计模式非常适合需要频繁更新缓存的应用场景,大大提高了开发效率和系统的响应速度。
8 2
|
6天前
|
前端开发 Java easyexcel
SpringBoot操作Excel实现单文件上传、多文件上传、下载、读取内容等功能
SpringBoot操作Excel实现单文件上传、多文件上传、下载、读取内容等功能
14 6
|
8天前
|
存储 缓存 Java
在Spring Boot中使用缓存的技术解析
通过利用Spring Boot中的缓存支持,开发者可以轻松地实现高效和可扩展的缓存策略,进而提升应用的性能和用户体验。Spring Boot的声明式缓存抽象和对多种缓存技术的支持,使得集成和使用缓存变得前所未有的简单。无论是在开发新应用还是优化现有应用,合理地使用缓存都是提高性能的有效手段。
15 1
|
1月前
|
NoSQL 前端开发 Java
使用 Spring Boot + Neo4j 实现知识图谱功能开发
在数据驱动的时代,知识图谱作为一种强大的信息组织方式,正逐渐在各个领域展现出其独特的价值。本文将围绕使用Spring Boot结合Neo4j图数据库来实现知识图谱功能开发的技术细节进行分享,帮助读者理解并掌握这一技术栈在实际项目中的应用。
102 4
|
2月前
|
机器学习/深度学习 文字识别 前端开发
基于 Spring Boot 3.3 + OCR 实现图片转文字功能
【8月更文挑战第30天】在当今数字化信息时代,图像中的文字信息越来越重要。无论是文档扫描、名片识别,还是车辆牌照识别,OCR(Optical Character Recognition,光学字符识别)技术都发挥着关键作用。本文将围绕如何使用Spring Boot 3.3结合OCR技术,实现图片转文字的功能,分享工作学习中的技术干货。
78 2
|
2月前
|
安全 Java 应用服务中间件
如何在 Spring Boot 3.3 中实现请求 IP 白名单拦截功能
【8月更文挑战第30天】在构建Web应用时,确保应用的安全性是至关重要的。其中,对访问者的IP地址进行限制是一种常见的安全措施,特别是通过实施IP白名单策略,可以只允许特定的IP地址或IP段访问应用,从而有效防止未授权的访问。在Spring Boot 3.3中,我们可以通过多种方式实现这一功能,下面将详细介绍几种实用的方法。
87 1
|
2月前
|
安全 Java UED
掌握SpringBoot单点登录精髓,单点登录是一种身份认证机制
【8月更文挑战第31天】单点登录(Single Sign-On,简称SSO)是一种身份认证机制,它允许用户只需在多个相互信任的应用系统中登录一次,即可访问所有系统,而无需重复输入用户名和密码。在微服务架构日益盛行的今天,SSO成为提升用户体验和系统安全性的重要手段。本文将详细介绍如何在SpringBoot中实现SSO,并附上示例代码。
43 0
|
2月前
|
Java 数据库连接 数据库
告别繁琐 SQL!Hibernate 入门指南带你轻松玩转 ORM,解锁高效数据库操作新姿势
【8月更文挑战第31天】Hibernate 是一款流行的 Java 持久层框架,简化了对象关系映射(ORM)过程,使开发者能以面向对象的方式进行数据持久化操作而无需直接编写 SQL 语句。本文提供 Hibernate 入门指南,介绍核心概念及示例代码,涵盖依赖引入、配置文件设置、实体类定义、工具类构建及基本 CRUD 操作。通过学习,你将掌握使用 Hibernate 简化数据持久化的技巧,为实际项目应用打下基础。
66 0
下一篇
无影云桌面