springBoot 异步编程指南

这篇文章可以了解到这些知识点:

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

Future 模式

异步编程在处理耗时操作以及多任务处理的场景下非常实用,可以让系统最大程度利用好机器的 CPU 和 内存,提高它们的利用率。
Future 模式的核心思想是 异步调用 。当我们执行一个方法时,如果方法中有多个耗时的任务需要同时去做,而且又不着急等待这个结果时可以让客户端立即返回然后,后台慢慢去计算任务。当然也可以选择等这些任务都执行完了,再返回给客户端。

SpringBoot 异步编程实战

SpringBoot 实现异步编程的话,通过 Spring 提供的两个注解会让这件事情变的非常简单。

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

1、定义线程池配置类

package com.sync.config;

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;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * <h3>springboot-study</h3>
 * <p>线程池配置类</p>
 *
 * @author : ZhangYuJie
 * @date : 2022-04-10 17:39
 **/
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {

    private static final int CORE_POOL_SIZE = 3;
    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("springboot-sync-");
        executor.initialize();
        return executor;
    }
}


ThreadPoolTaskExecutor 常见概念

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

如果队列已满并且当前同时运行的线程数达到最大线程数的时候,要知道会发生的一下情况:
Spring 默认使用的是 ThreadPoolExecutor.AbortPolicy。在Spring的默认情况下,ThreadPoolExecutor 将抛出 RejectedExecutionException 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用 ThreadPoolExecutor.CallerRunsPolicy。当最大池被填满时,此策略为我们提供可伸缩队列。

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

2、编写一个异步的方法
模拟一个查找对应字符开头电影的方法,这个方法加上了 @Async注解来告诉 Spring 它是一个异步的方法。另外,这个方法的返回值 CompletableFuture.completedFuture(results)这代表我们需要返回结果,也就是说程序必须把任务执行完成之后再返回给调用方。
留意completableFutureTask方法中的第一行打印日志这句代码,分析程序时会用到,很重要!

package com.sync.service;

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

import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;

/**
 * <h3>springboot-study</h3>
 * <p>模拟查找对应字符开头电影的方法</p>
 *
 * @author : ZhangYuJie
 * @date : 2022-04-10 17:51
 **/
@Service
@Slf4j
public class AsyncService {
    private final List<String> movies =
            List.of(
                    "Forrest Gump",
                    "Titanic",
                    "Spirited Away",
                    "The Shawshank Redemption",
                    "Zootopia",
                    "Farewell ",
                    "Joker",
                    "Crawl");

    /**
     * 示范使用:找到特定字符开头的电影
     */
    @Async
    public CompletableFuture<List<String>> completableFutureTask1(String start) {
        // 打印日志
        log.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);
    }

    /**
     * 示范使用:找到特定字符开头的电影
     */
    @Async
    public void completableFutureTask2(String start) {
        // 打印日志
        log.warn(Thread.currentThread().getName() + "start this task!");
       // 这可以是插入数据库的操作等等
    }
}


3、测试编写的异步方法

package com.sync.controller;

import com.sync.service.AsyncService;
import lombok.RequiredArgsConstructor;
import org.springframework.util.StopWatch;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;

/**
 * <h3>springboot-study</h3>
 * <p> 测试编写的异步方法</p>
 *
 * @author : ZhangYuJie
 * @date : 2022-04-10 17:55
 **/
@RestController
@RequestMapping("/async")
@RequiredArgsConstructor
public class AsyncController {

    private final AsyncService asyncService;

    @GetMapping("/movies1")
    public String completableFutureTask1() throws ExecutionException, InterruptedException {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        // 开始执行大量的异步任务
        List<String> words = List.of("F", "T", "S", "Z", "J", "C");
        List<CompletableFuture<List<String>>> completableFutureList =
                words.stream()
                        .map(asyncService::completableFutureTask1)
                        .collect(Collectors.toList());
        // CompletableFuture.join()方法可以获取他们的结果并将结果连接起来
        List<List<String>> results = completableFutureList.stream()
                .map(CompletableFuture::join).collect(Collectors.toList());
        stopWatch.stop();
        // 打印结果以及运行程序运行花费时间
        System.out.println("耗时: " + stopWatch.getTotalTimeMillis());
        return results.toString();
    }
}

请求接口后打印的日志:

2022-04-10 18:05:06.492  INFO 42372 --- [nio-8080-exec-4] o.s.web.servlet.DispatcherServlet        : Completed initialization in 1 ms
2022-04-10 18:05:20.456  WARN 42372 --- [ringboot-sync-2] com.sync.service.AsyncService            : springboot-sync-2start this task!
2022-04-10 18:05:20.456  WARN 42372 --- [ringboot-sync-3] com.sync.service.AsyncService            : springboot-sync-3start this task!
2022-04-10 18:05:20.456  WARN 42372 --- [ringboot-sync-5] com.sync.service.AsyncService            : springboot-sync-5start this task!
2022-04-10 18:05:20.456  WARN 42372 --- [ringboot-sync-1] com.sync.service.AsyncService            : springboot-sync-1start this task!
2022-04-10 18:05:20.456  WARN 42372 --- [ringboot-sync-4] com.sync.service.AsyncService            : springboot-sync-4start this task!
2022-04-10 18:05:20.456  WARN 42372 --- [ringboot-sync-6] com.sync.service.AsyncService            : springboot-sync-6start this task!
耗时: 1032

可以看到处理所有任务花费的时间大概是 1 s。这与我们自定义的 ThreadPoolTaskExecutor 有关,我们配置的核心线程数是 6 ,然后通过通过下面的代码模拟分配了 6 个任务给系统执行。这样每个线程都会被分配到一个任务,每个任务执行花费时间是 1 s ,所以处理 6 个任务的总花费时间是 1 s。
可以验证一下,试着去把核心线程数的数量改为 3 ,再次请求这个接口你会发现处理所有任务花费的时间大概是 2 s。

改成3之后请求日志

2022-04-10 18:09:13.469  INFO 41328 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2022-04-10 18:09:13.471  INFO 41328 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 2 ms
2022-04-10 18:09:13.503  WARN 41328 --- [ringboot-sync-3] com.sync.service.AsyncService            : springboot-sync-3start this task!
2022-04-10 18:09:13.503  WARN 41328 --- [ringboot-sync-1] com.sync.service.AsyncService            : springboot-sync-1start this task!
2022-04-10 18:09:13.503  WARN 41328 --- [ringboot-sync-2] com.sync.service.AsyncService            : springboot-sync-2start this task!
2022-04-10 18:09:14.507  WARN 41328 --- [ringboot-sync-3] com.sync.service.AsyncService            : springboot-sync-3start this task!
2022-04-10 18:09:14.507  WARN 41328 --- [ringboot-sync-2] com.sync.service.AsyncService            : springboot-sync-2start this task!
2022-04-10 18:09:14.508  WARN 41328 --- [ringboot-sync-1] com.sync.service.AsyncService            : springboot-sync-1start this task!
耗时: 2030

从上面的运行结果可以看出,当所有任务执行完成之后才返回结果。

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

    /**
     * 示范使用:void返回
     */
    @Async
    public void completableFutureTask2(String start) {
        // 打印日志
        log.warn(Thread.currentThread().getName() + "start this task!");
       // 这可以是插入数据库的操作等等
    }
}

Controller 代码修改如下:

 @GetMapping("/movies2")
    public String completableFutureTask2() throws ExecutionException, InterruptedException {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        List<String> words = List.of("F", "T", "S", "Z", "J", "C");
        words.forEach(asyncService::completableFutureTask2);
        stopWatch.stop();
        // 打印结果以及运行程序运行花费时间
        System.out.println("耗时: " + stopWatch.getTotalTimeMillis());
        return "Done";
    }

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

耗时: 3 
2022-04-10 18:18:07.093  WARN 34460 --- [ringboot-sync-1] com.sync.service.AsyncService            : springboot-sync-1start this task!
2022-04-10 18:18:07.093  WARN 34460 --- [ringboot-sync-3] com.sync.service.AsyncService            : springboot-sync-3start this task!
2022-04-10 18:18:07.093  WARN 34460 --- [ringboot-sync-2] com.sync.service.AsyncService            : springboot-sync-2start this task!
2022-04-10 18:18:07.093  WARN 34460 --- [ringboot-sync-3] com.sync.service.AsyncService            : springboot-sync-3start this task!
2022-04-10 18:18:07.093  WARN 34460 --- [ringboot-sync-2] com.sync.service.AsyncService            : springboot-sync-2start this task!
2022-04-10 18:18:07.093  WARN 34460 --- [ringboot-sync-1] com.sync.service.AsyncService            : springboot-sync-1start this task!

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

代码示例github地址

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值