一、使用@Async实现异步调用
在Spring Boot中,我们只需要通过使用@Async注解就能简单的将原来的同步函数变为异步函数,Task类实现如下:
package com.example.demospringboot;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
@Slf4j
@Component
public class AsyncTasks {
public static Random random = new Random();
@Async
public CompletableFuture<String> doTaskOne() throws Exception {
log.info("开始做任务一");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
log.info("完成任务一,耗时:" + (end - start) + "毫秒");
return CompletableFuture.completedFuture("任务一完成");
}
@Async
public CompletableFuture<String> doTaskTwo() throws Exception {
log.info("开始做任务二");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
log.info("完成任务二,耗时:" + (end - start) + "毫秒");
return CompletableFuture.completedFuture("任务二完成");
}
@Async
public CompletableFuture<String> doTaskThree() throws Exception {
log.info("开始做任务三");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
log.info("完成任务三,耗时:" + (end - start) + "毫秒");
return CompletableFuture.completedFuture("任务三完成");
}
}
注:@Async所修饰的函数不要定义为static类型,这样异步调用不会生效
为了让@Async注解能够生效,还需要在Spring Boot的主程序中配置@EnableAsync,如下所示:
package com.example.demospringboot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
@EnableAsync
@SpringBootApplication
public class DemospringbootApplication {
public static void main(String[] args) {
SpringApplication.run(DemospringbootApplication.class, args);
}
}
测试类如下:
package com.example.demospringboot;
import lombok.extern.slf4j.Slf4j;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.cache.CacheManager;
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class DemospringbootApplicationTests {
@Autowired
private AsyncTasks asyncTasks;
@Test
public void test() throws Exception {
asyncTasks.doTaskOne();
asyncTasks.doTaskTwo();
asyncTasks.doTaskThree();
}
}
此时可以反复执行单元测试,您可能会遇到各种不同的结果,比如:
2023-08-01 21:32:46.064 INFO 1764 --- [ task-1] com.example.demospringboot.AsyncTasks : 开始做任务一
2023-08-01 21:32:46.064 INFO 1764 --- [ task-3] com.example.demospringboot.AsyncTasks : 开始做任务三
2023-08-01 21:32:46.064 INFO 1764 --- [ task-2] com.example.demospringboot.AsyncTasks : 开始做任务二
二、 异步回调
那么我们如何判断上述三个异步调用是否已经执行完成呢?我们需要使用CompletableFuture来返回异步调用的结果,就像如下方式改造doTaskOne函数:
package com.example.demospringboot;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
@Slf4j
@Component
public class AsyncTasks {
public static Random random = new Random();
@Async
public CompletableFuture<String> doTaskOne() throws Exception {
log.info("开始做任务一");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
log.info("完成任务一,耗时:" + (end - start) + "毫秒");
return CompletableFuture.completedFuture("任务一完成");
}
@Async
public CompletableFuture<String> doTaskTwo() throws Exception {
log.info("开始做任务二");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
log.info("完成任务二,耗时:" + (end - start) + "毫秒");
return CompletableFuture.completedFuture("任务二完成");
}
@Async
public CompletableFuture<String> doTaskThree() throws Exception {
log.info("开始做任务三");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
log.info("完成任务三,耗时:" + (end - start) + "毫秒");
return CompletableFuture.completedFuture("任务三完成");
}
}
下面我们改造一下测试用例,让测试在等待完成三个异步调用之后来做一些其他事情
package com.example.demospringboot;
import lombok.extern.slf4j.Slf4j;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.concurrent.CompletableFuture;
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class DemospringbootApplicationTests {
@Autowired
private AsyncTasks asyncTasks;
@Test
public void test() throws Exception {
long start = System.currentTimeMillis();
CompletableFuture<String> task1 = asyncTasks.doTaskOne();
CompletableFuture<String> task2 = asyncTasks.doTaskTwo();
CompletableFuture<String> task3 = asyncTasks.doTaskThree();
CompletableFuture.allOf(task1, task2, task3).join();
long end = System.currentTimeMillis();
log.info("任务全部完成,总耗时:" + (end - start) + "毫秒");
}
}
执行一下上述的单元测试,可以看到如下结果:
2023-08-01 21:41:40.347 INFO 13684 --- [ task-1] com.example.demospringboot.AsyncTasks : 开始做任务一
2023-08-01 21:41:40.347 INFO 13684 --- [ task-3] com.example.demospringboot.AsyncTasks : 开始做任务三
2023-08-01 21:41:40.347 INFO 13684 --- [ task-2] com.example.demospringboot.AsyncTasks : 开始做任务二
2023-08-01 21:41:44.817 INFO 13684 --- [ task-2] com.example.demospringboot.AsyncTasks : 完成任务二,耗时:4470毫秒
2023-08-01 21:41:45.042 INFO 13684 --- [ task-1] com.example.demospringboot.AsyncTasks : 完成任务一,耗时:4695毫秒
2023-08-01 21:41:48.154 INFO 13684 --- [ task-3] com.example.demospringboot.AsyncTasks : 完成任务三,耗时:7807毫秒
2023-08-01 21:41:48.154 INFO 13684 --- [ main] c.e.d.DemospringbootApplicationTests : 任务全部完成,总耗时:7817毫秒
可以看到,通过异步调用,让任务一、二、三并发执行,有效的减少了程序的总运行时间。
三、 配置默认线程池
默认线程池的配置很简单,只需要在application.properties配置文件中完成即可,主要有以下这些参数:
spring.task.execution.pool.core-size=2
spring.task.execution.pool.max-size=5
spring.task.execution.pool.queue-capacity=10
spring.task.execution.pool.keep-alive=60s
spring.task.execution.pool.allow-core-thread-timeout=true
spring.task.execution.shutdown.await-termination=false
spring.task.execution.shutdown.await-termination-period=
spring.task.execution.thread-name-prefix=task-
spring.task.execution.pool.core-size:线程池创建时的初始化线程数,默认为8
spring.task.execution.pool.max-size:线程池的最大线程数,默认为int最大值
spring.task.execution.pool.queue-capacity:用来缓冲执行任务的队列,默认为int最大值
spring.task.execution.pool.keep-alive:线程终止前允许保持空闲的时间
spring.task.execution.pool.allow-core-thread-timeout:是否允许核心线程超时
spring.task.execution.shutdown.await-termination:是否等待剩余任务完成后才关闭应用
spring.task.execution.shutdown.await-termination-period:等待剩余任务完成的最大时间
spring.task.execution.thread-name-prefix:线程名的前缀,设置好了之后可以方便我们在日志中查看处理任务所在的线程池
执行一下上述的单元测试,可以看到如下结果:
2023-08-02 09:13:50.544 INFO 20528 --- [ task-2] com.example.demospringboot.AsyncTasks : 开始做任务二
2023-08-02 09:13:50.544 INFO 20528 --- [ task-1] com.example.demospringboot.AsyncTasks : 开始做任务一
2023-08-02 09:13:55.348 INFO 20528 --- [ task-2] com.example.demospringboot.AsyncTasks : 完成任务二,耗时:4804毫秒
2023-08-02 09:13:55.348 INFO 20528 --- [ task-2] com.example.demospringboot.AsyncTasks : 开始做任务三
2023-08-02 09:13:55.886 INFO 20528 --- [ task-1] com.example.demospringboot.AsyncTasks : 完成任务一,耗时:5342毫秒
2023-08-02 09:14:00.508 INFO 20528 --- [ task-2] com.example.demospringboot.AsyncTasks : 完成任务三,耗时:5160毫秒
2023-08-02 09:14:00.508 INFO 20528 --- [ main] c.e.d.DemospringbootApplicationTests : 任务全部完成,总耗时:9974毫秒
可以看到,日志输出的顺序会变成如下的顺序:
任务一和任务二会马上占用核心线程,任务三进入队列等待;
任务一完成,释放出一个核心线程,任务三从队列中移出,并占用核心线程开始处理。
这里要理解缓冲队列与最大线程间的关系:只有在缓冲队列满了之后才会申请超过核心线程数的线程来进行处理。所以,这里只有缓冲队列中10个任务满了,再来第11个任务的时候,才会在线程池中创建第三个线程来处理。
因此,你应该已经了解到异步任务的执行背后有一个线程池来管理执行任务。为了控制异步任务的并发不影响到应用的正常运作,我们必须要对线程池做好相应的配置,防止资源的过渡使用。除了默认线程池的配置之外,还有一类场景,也是很常见的,那就是多任务情况下的线程池隔离。
四、线程池隔离
假设有两个上述API接口,如果并发上来或其中某几个处理过程扯后腿了的时候,这两个提供不相干服务的接口可能会互相影响。
比如:假设当前线程池配置的最大线程数有2个,这个时候/api-1接口中task1和task2处理速度很慢,阻塞了;那么此时,当用户调用api-2接口的时候,这个服务也会阻塞!
造成这种现场的原因是:默认情况下,所有用@Async创建的异步任务都是共用的一个线程池,所以当有一些异步任务碰到性能问题的时候,是会直接影响其他异步任务的。
为了解决这个问题,我们就需要对异步任务做一定的线程池隔离,让不同异步任务配置不同线程池,互不影响。
第一步:在主类中,初始化多个线程池,比如下面这样:
package com.example.demospringboot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
@EnableAsync
@SpringBootApplication
public class DemospringbootApplication {
public static void main(String[] args) {
SpringApplication.run(DemospringbootApplication.class, args);
}
@EnableAsync
@Configuration
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;
}
}
}
创建了2个taskExecutor,并且设置了核心线程数和最大线程数均为2。
第二步:创建异步任务,并指定要使用的线程池名称
package com.example.demospringboot;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
@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("任务完成");
}
}
原先的task序号修改为通过taskNo传入;
这里@Async
注解中定义的taskExecutor1
和taskExecutor2
就是线程池的名字。由于在第一步中,我们没有具体写两个线程池Bean的名称,所以默认会使用方法名,也就是taskExecutor1
和taskExecutor2
。
第三步:单元测试验证:
package com.example.demospringboot;
import lombok.extern.slf4j.Slf4j;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.concurrent.CompletableFuture;
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class DemospringbootApplicationTests {
@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) + "毫秒");
}
}
执行结果如下:
2023-08-02 09:37:05.430 INFO 17736 --- [ executor-1-1] com.example.demospringboot.AsyncTasks : 开始任务:1
2023-08-02 09:37:05.430 INFO 17736 --- [ executor-2-2] com.example.demospringboot.AsyncTasks : 开始任务:5
2023-08-02 09:37:05.430 INFO 17736 --- [ executor-2-1] com.example.demospringboot.AsyncTasks : 开始任务:4
2023-08-02 09:37:05.430 INFO 17736 --- [ executor-1-2] com.example.demospringboot.AsyncTasks : 开始任务:2
2023-08-02 09:37:06.686 INFO 17736 --- [ executor-2-1] com.example.demospringboot.AsyncTasks : 完成任务:4,耗时:1256 毫秒
2023-08-02 09:37:06.686 INFO 17736 --- [ executor-2-1] com.example.demospringboot.AsyncTasks : 开始任务:6
2023-08-02 09:37:08.578 INFO 17736 --- [ executor-1-1] com.example.demospringboot.AsyncTasks : 完成任务:1,耗时:3148 毫秒
2023-08-02 09:37:08.578 INFO 17736 --- [ executor-1-1] com.example.demospringboot.AsyncTasks : 开始任务:3
2023-08-02 09:37:10.972 INFO 17736 --- [ executor-2-2] com.example.demospringboot.AsyncTasks : 完成任务:5,耗时:5542 毫秒
2023-08-02 09:37:12.771 INFO 17736 --- [ executor-1-1] com.example.demospringboot.AsyncTasks : 完成任务:3,耗时:4193 毫秒
2023-08-02 09:37:13.461 INFO 17736 --- [ executor-1-2] com.example.demospringboot.AsyncTasks : 完成任务:2,耗时:8031 毫秒
2023-08-02 09:37:15.951 INFO 17736 --- [ executor-2-1] com.example.demospringboot.AsyncTasks : 完成任务:6,耗时:9265 毫秒
2023-08-02 09:37:15.951 INFO 17736 --- [ main] c.e.d.DemospringbootApplicationTests : 任务全部完成,总耗时:10527毫秒
可以看到,在上面的单元测试中,一共启动了6个异步任务,前三个用的是线程池1,后三个用的是线程池2。
线程池1的三个任务,task1和task2会先获得执行线程,然后task3因为没有可分配线程进入缓冲队列
线程池2的三个任务,task4和task5会先获得执行线程,然后task6因为没有可分配线程进入缓冲队列
任务task6在task4完成之后,开始执行
任务task3在task1完成之后,开始执行
五、 配置线程池的拒绝策略
其实在上述主类的taskExecutor1中,我们已经配置了拒绝策略:executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
如果我们不配置决绝策略,并将缓冲队列也设为2:setQueueCapacity(2)
@Bean
public Executor taskExecutor1() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(2);
executor.setQueueCapacity(2);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("executor-1-");
//executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}
再跑以下测试用例:
package com.example.demospringboot;
import lombok.extern.slf4j.Slf4j;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.concurrent.CompletableFuture;
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class DemospringbootApplicationTests {
@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");
CompletableFuture<String> task4 = asyncTasks.doTaskOne("4");
CompletableFuture<String> task5 = asyncTasks.doTaskOne("5");
CompletableFuture<String> task6 = asyncTasks.doTaskOne("6");
// 一起执行
CompletableFuture.allOf(task1, task2, task3, task4, task5, task6).join();
long end = System.currentTimeMillis();
log.info("任务全部完成,总耗时:" + (end - start) + "毫秒");
}
}
会出现以下结果:
org.springframework.core.task.TaskRejectedException: Executor [java.util.concurrent.ThreadPoolExecutor@73aae7a[Running, pool size = 2, active threads = 2, queued tasks = 2, completed tasks = 0]] did not accept task: java.util.concurrent.CompletableFuture$AsyncSupply@4adcc981
可以很明确的知道,第5个任务因为超过了执行线程+缓冲队列长度,而被拒绝了。
在ThreadPoolExecutor中提供了4种线程的策略可以供开发者直接使用,你只需要像下面这样设置即可:
// AbortPolicy策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
// DiscardPolicy策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
// DiscardOldestPolicy策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy());
// CallerRunsPolicy策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
这四个策略对应的含义分别是:
- AbortPolicy策略:默认策略,如果线程池队列满了丢掉这个任务并且抛出RejectedExecutionException异常。
- DiscardPolicy策略:如果线程池队列满了,会直接丢掉这个任务并且不会有任何异常。
- DiscardOldestPolicy策略:如果队列满了,会将最早进入队列的任务删掉腾出空间,再尝试加入队列。
- CallerRunsPolicy策略:如果添加到线程池失败,那么主线程会自己去执行该任务,不会等待线程池中的线程去执行。
而如果你要自定义一个拒绝策略,那么可以这样写:
executor.setRejectedExecutionHandler(new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 拒绝策略的逻辑
}
});
当然如果你喜欢用Lamba表达式,也可以这样写:
executor.setRejectedExecutionHandler((r, executor1) -> {
// 拒绝策略的逻辑
});
参考:https://blog.didispace.com/spring-boot-learning-2-7-5/