乐趣区

关于java:Spring-Boot中有多个Async异步任务时记得做好线程池的隔离

通过上一篇:配置 @Async 异步工作的线程池的介绍,你应该曾经理解到异步工作的执行背地有一个线程池来治理执行工作。为了管制异步工作的并发不影响到利用的失常运作,咱们必须要对线程池做好相应的配置,避免资源的过渡应用。除了默认线程池的配置之外,还有一类场景,也是很常见的,那就是多任务状况下的线程池隔离。

什么是线程池的隔离,为什么要隔离

可能有的小伙伴还不太理解 什么是线程池的隔离,为什么要隔离?。所以,咱们先来看看上面的场景案例:

@RestController
public class HelloController {

    @Autowired
    private AsyncTasks asyncTasks;
        
    @GetMapping("/api-1")
    public String taskOne() {CompletableFuture<String> task1 = asyncTasks.doTaskOne("1");
        CompletableFuture<String> task2 = asyncTasks.doTaskOne("2");
        CompletableFuture<String> task3 = asyncTasks.doTaskOne("3");
        
        CompletableFuture.allOf(task1, task2, task3).join();
        return "";
    }
    
    @GetMapping("/api-2")
    public String taskTwo() {CompletableFuture<String> task1 = asyncTasks.doTaskTwo("1");
        CompletableFuture<String> task2 = asyncTasks.doTaskTwo("2");
        CompletableFuture<String> task3 = asyncTasks.doTaskTwo("3");
        
        CompletableFuture.allOf(task1, task2, task3).join();
        return "";
    }
    
}

下面的代码中,有两个 API 接口,这两个接口的具体执行逻辑中都会把执行过程拆分为三个异步工作来实现。

好了,思考一分钟,想一下。如果这样实现,会有什么问题吗?


下面这段代码,在 API 申请并发不高,同时如果每个工作的处理速度也够快的时候,是没有问题的。但如果并发上来或其中某几个处理过程扯后腿了的时候。这两个提供不相干服务的接口可能会相互影响。比方:假如以后线程池配置的最大线程数有 2 个,这个时候 /api- 1 接口中 task1 和 task2 处理速度很慢,阻塞了;那么此时,当用户调用 api- 2 接口的时候,这个服务也会阻塞!

造成这种现场的起因是:默认状况下,所有用 @Async 创立的异步工作都是共用的一个线程池,所以当有一些异步工作碰到性能问题的时候,是会间接影响其余异步工作的。

为了解决这个问题,咱们就须要对异步工作做肯定的线程池隔离,让不同的异步工作互不影响。

不同异步工作配置不同线程池

上面,咱们就来实际操作一下!

第一步:初始化多个线程池,比方上面这样:

@EnableAsync
@Configuration
public class TaskPoolConfig {

    @Bean
    public Executor taskExecutor1() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);
        executor.setMaxPoolSize(2);
        executor.setQueueCapacity(10);
        executor.setKeepAliveSeconds(60);
        executor.setThreadNamePrefix("executor-1-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        return executor;
    }

    @Bean
    public Executor taskExecutor2() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);
        executor.setMaxPoolSize(2);
        executor.setQueueCapacity(10);
        executor.setKeepAliveSeconds(60);
        executor.setThreadNamePrefix("executor-2-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        return executor;
    }
}

留神:这里顺便用 executor.setThreadNamePrefix 设置了线程名的前缀,这样能够不便察看前面具体执行的程序。

第二步:创立异步工作,并指定要应用的线程池名称

@Slf4j
@Component
public class AsyncTasks {public static Random random = new Random();

    @Async("taskExecutor1")
    public CompletableFuture<String> doTaskOne(String taskNo) throws Exception {log.info("开始工作:{}", taskNo);
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        log.info("实现工作:{},耗时:{} 毫秒", taskNo, end - start);
        return CompletableFuture.completedFuture("工作实现");
    }

    @Async("taskExecutor2")
    public CompletableFuture<String> doTaskTwo(String taskNo) throws Exception {log.info("开始工作:{}", taskNo);
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        log.info("实现工作:{},耗时:{} 毫秒", taskNo, end - start);
        return CompletableFuture.completedFuture("工作实现");
    }

}

这里 @Async 注解中定义的 taskExecutor1taskExecutor2就是线程池的名字。因为在第一步中,咱们没有具体写两个线程池 Bean 的名称,所以默认会应用办法名,也就是 taskExecutor1taskExecutor2

第三步:写个单元测试来验证下,比方上面这样:

@Slf4j
@SpringBootTest
public class Chapter77ApplicationTests {

    @Autowired
    private AsyncTasks asyncTasks;

    @Test
    public void test() throws Exception {long start = System.currentTimeMillis();

        // 线程池 1
        CompletableFuture<String> task1 = asyncTasks.doTaskOne("1");
        CompletableFuture<String> task2 = asyncTasks.doTaskOne("2");
        CompletableFuture<String> task3 = asyncTasks.doTaskOne("3");

        // 线程池 2
        CompletableFuture<String> task4 = asyncTasks.doTaskTwo("4");
        CompletableFuture<String> task5 = asyncTasks.doTaskTwo("5");
        CompletableFuture<String> task6 = asyncTasks.doTaskTwo("6");

        // 一起执行
        CompletableFuture.allOf(task1, task2, task3, task4, task5, task6).join();

        long end = System.currentTimeMillis();

        log.info("工作全副实现,总耗时:" + (end - start) + "毫秒");
    }

}

在下面的单元测试中,一共启动了 6 个异步工作,前三个用的是线程池 1,后三个用的是线程池 2。

先不执行,依据设置的外围线程 2 和最大线程数 2,来剖析一下,大略会是怎么样的执行状况?

  1. 线程池 1 的三个工作,task1 和 task2 会先取得执行线程,而后 task3 因为没有可调配线程进入缓冲队列
  2. 线程池 2 的三个工作,task4 和 task5 会先取得执行线程,而后 task6 因为没有可调配线程进入缓冲队列
  3. 工作 task3 会在 task1 或 task2 实现之后,开始执行
  4. 工作 task6 会在 task4 或 task5 实现之后,开始执行

剖析好之后,执行下单元测试,看看是否是这样的:

2021-09-15 23:45:11.369  INFO 61670 --- [executor-1-1] com.didispace.chapter77.AsyncTasks       : 开始工作:1
2021-09-15 23:45:11.369  INFO 61670 --- [executor-2-2] com.didispace.chapter77.AsyncTasks       : 开始工作:5
2021-09-15 23:45:11.369  INFO 61670 --- [executor-2-1] com.didispace.chapter77.AsyncTasks       : 开始工作:4
2021-09-15 23:45:11.369  INFO 61670 --- [executor-1-2] com.didispace.chapter77.AsyncTasks       : 开始工作:2
2021-09-15 23:45:15.905  INFO 61670 --- [executor-2-1] com.didispace.chapter77.AsyncTasks       : 实现工作:4,耗时:4532 毫秒
2021-09-15 23:45:15.905  INFO 61670 --- [executor-2-1] com.didispace.chapter77.AsyncTasks       : 开始工作:6
2021-09-15 23:45:18.263  INFO 61670 --- [executor-1-2] com.didispace.chapter77.AsyncTasks       : 实现工作:2,耗时:6890 毫秒
2021-09-15 23:45:18.263  INFO 61670 --- [executor-1-2] com.didispace.chapter77.AsyncTasks       : 开始工作:3
2021-09-15 23:45:18.896  INFO 61670 --- [executor-2-2] com.didispace.chapter77.AsyncTasks       : 实现工作:5,耗时:7523 毫秒
2021-09-15 23:45:19.842  INFO 61670 --- [executor-1-2] com.didispace.chapter77.AsyncTasks       : 实现工作:3,耗时:1579 毫秒
2021-09-15 23:45:20.551  INFO 61670 --- [executor-1-1] com.didispace.chapter77.AsyncTasks       : 实现工作:1,耗时:9178 毫秒
2021-09-15 23:45:24.117  INFO 61670 --- [executor-2-1] com.didispace.chapter77.AsyncTasks       : 实现工作:6,耗时:8212 毫秒
2021-09-15 23:45:24.117  INFO 61670 --- [main] c.d.chapter77.Chapter77ApplicationTests  : 工作全副实现,总耗时:12762 毫秒

好了,明天的学习就到这里!如果您学习过程中如遇艰难?能够退出咱们超高品质的 Spring 技术交换群,参加交换与探讨,更好的学习与提高!更多 Spring Boot 教程能够点击中转!,欢送珍藏与转发反对!

代码示例

本文的残缺工程能够查看上面仓库中 2.x 目录下的 chapter7-7 工程:

  • Github:https://github.com/dyc87112/SpringBoot-Learning/
  • Gitee:https://gitee.com/didispace/SpringBoot-Learning/

如果您感觉本文不错,欢送 Star 反对,您的关注是我保持的能源!

欢送关注我的公众号:程序猿 DD,分享其余中央看不到的常识与思考

退出移动版