先赞后看,养成习惯 🌹 欢迎微信关注[Java编程之道],每天进步一点点,沉淀技术分享知识。
预祝各位正在高考的小学弟学妹们考上理想的大学,
高考加油
!
学长忠告:报志愿千万
别
选计算机啊~🙄
今天我们聊一下SpringBoot中的异步技术中的
异步线程池
,这一块的内容深入的聊内容还是很多的,所以暂时分为三个部分
-
使用
@Async
实现异步调用以及自定义线程池的实现。 -
SpringBoot中异步调用线程池
内部实现原理
。 -
我是如何通过线程池技术将
10s
的任务降低到ms
级别。
话不多说跟紧我,老司机要发车了!
异步调用
异步调用这个概念对于学过Java基础的同学来说并不陌生,下面我们以两端代码来直观看看异步和同步的区别以及SpringBoot中实现异步调用的方式。
同步任务
/**
* @Auther: 爱唠嗑的阿磊
* @Company: Java编程之道
* @Date: 2020/7/7 20:12
* @Version 1.0
*/
@Component
public class MyTask {
public static Random random =new Random();
public void doTaskOne() throws Exception {
System.out.println("开始做任务一");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(5000));
long end = System.currentTimeMillis();
System.out.println("完成任务一,耗时:" + (end - start) + "毫秒");
}
public void doTaskTwo() throws Exception {
System.out.println("开始做任务二");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(5000));
long end = System.currentTimeMillis();
System.out.println("完成任务二,耗时:" + (end - start) + "毫秒");
}
public void doTaskThree() throws Exception {
System.out.println("开始做任务三");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(5000));
long end = System.currentTimeMillis();
System.out.println("完成任务三,耗时:" + (end - start) + "毫秒");
}
}
复制代码
注入MyTask对象,执行三个函数。
@RestController
class Test{
@Autowired
MyTask myTask;
@GetMapping("/")
public void contextLoads() throws Exception {
myTask.doTaskOne();
myTask.doTaskTwo();
myTask.doTaskThree();
}
}
复制代码
访问http://127.0.0.1:8080/可以看到类似如下输出:
开始做任务一
完成任务一,耗时:3387毫秒
开始做任务二
完成任务二,耗时:621毫秒
开始做任务三
完成任务三,耗时:4395毫秒
复制代码
异步调用
接下来就通过SpringBoot中的异步调用技术,使三个不存在依赖关系的任务实现并发执行。在Spring Boot中,最简单的方式是通过@Async注解将原来的同步函数变为异步函数.
/**
* @Auther: 爱唠嗑的阿磊
* @Company: Java编程之道
* @Date: 2020/7/7 20:12
* @Version 1.0
*/
@Component
public class MyTask {
public static Random random =new Random();
@Async
public void doTaskOne() throws Exception {
System.out.println("开始做任务一");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(5000));
long end = System.currentTimeMillis();
System.out.println("完成任务一,耗时:" + (end - start) + "毫秒");
}
@Async
public void doTaskTwo() throws Exception {
System.out.println("开始做任务二");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(5000));
long end = System.currentTimeMillis();
System.out.println("完成任务二,耗时:" + (end - start) + "毫秒");
}
@Async
public void doTaskThree() throws Exception {
System.out.println("开始做任务三");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(5000));
long end = System.currentTimeMillis();
System.out.println("完成任务三,耗时:" + (end - start) + "毫秒");
}
}
复制代码
同时需要在Spring Boot的主程序中配置@EnableAsync使@Async注解能够生效
@EnableAsync
@SpringBootApplication
public class ThreaddemoApplication {
public static void main(String[] args) {
SpringApplication.run(ThreaddemoApplication.class, args);
}
}
复制代码
再次测试执行你会发现响应结果明显快了不少,但是数据的顺序是乱的。原因是三个函数候已经是异步执行了。主程序在异步调用执行之后,线程的执行顺序得不到保障。
这里可以想到为什么我在 V-LoggingTool 使用可配置的开启的线程池了,因为我存储日志并不关心线程任务的返回值,我需要程序立即往下执行,耗时任务交给线程池去执行就行了。
如果一定要拿到线程执行的结果,对于这个问题怎么处理简单来说
看场景
,可以使用Future的get来阻塞获取结果从而保证得到正确的数据。对于一些超时任务的场景可以在get中设置超时时间。
异步回调
接着上文所说的解决思路我们可以通过Future来返回异步调用的结果来感知线程是否执行结束并且获取返回值。知道Future/Callable的同学应该不会感到很陌生。
将三个方法都这样处理一下
/**
* @Auther: 爱唠嗑的阿磊
* @Company: Java编程之道
* @Date: 2020/7/7 20:12
* @Version 1.0
*/
@Component
public class MyTask {
public static Random random =new Random();
@Async
public Future<String> doTaskOne() throws Exception {
System.out.println("开始做任务一");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(5000));
long end = System.currentTimeMillis();
System.out.println("完成任务一,耗时:" + (end - start) + "毫秒");
return new AsyncResult<>("任务一完成");
}
@Async
public Future<String> doTaskTwo() throws Exception {
System.out.println("开始做任务二");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(5000));
long end = System.currentTimeMillis();
System.out.println("完成任务二,耗时:" + (end - start) + "毫秒");
return new AsyncResult<>("任务二完成");
}
@Async
public Future<String> doTaskThree() throws Exception {
System.out.println("开始做任务三");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(5000));
long end = System.currentTimeMillis();
System.out.println("完成任务三,耗时:" + (end - start) + "毫秒");
return new AsyncResult<>("任务三完成");
}
}
复制代码
改造一下测试类
@RestController
class Test{
@Autowired
MyTask myTask;
@GetMapping("/")
public void contextLoads() throws Exception {
/*myTask.doTaskOne();
myTask.doTaskTwo();
myTask.doTaskThree();*/
long start = System.currentTimeMillis();
Future<String> task1 = myTask.doTaskOne();
Future<String> task2 = myTask.doTaskTwo();
Future<String> task3 = myTask.doTaskThree();
task1.get();
task2.get();
task3.get();
long end = System.currentTimeMillis();
System.out.println("任务全部完成,总耗时:" + (end - start) + "毫秒");
}
}
复制代码
执行一下
开始做任务三
开始做任务一
开始做任务二
完成任务三,耗时:1125毫秒
完成任务二,耗时:1520毫秒
完成任务一,耗时:4344毫秒
任务全部完成,总耗时:4354毫秒
复制代码
当然我只是举一个获取异步回调的例子,实质上,上诉这种写法不可取,因为get是一个阻塞方法,task1如果一直不执行完的话就会一直阻塞在这里。同理还可以使用其他技术来保证一个合理的返回值如:
CountDownLatch
等。
自定义线程池
在SpirngBoot中实现自定义线程池很简单,没有接触通过注解实现异步的时候,大家都是自己去写一个线程池然后注入到容器中,最后暴露一下任务提交的方法...但是SpringBoot为你省去了很多繁杂的操作。
- 第一步,先在配置类中定义一个线程池
@EnableAsync
@Configuration
class TaskPoolConfig {
@Bean("taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10