异步调用几乎是处理高并发Web应用性能问题的万金油,那么什么是“异步调用”?
“异步调用”对应的是“同步调用”,同步调用指程序按照定义顺序依次执行,每一行程序都必须等待上一行程序执行完成之后才能执行;异步调用指程序在顺序执行时,不等待异步调用的语句返回结果就执行后面的程序。
同步: 同步就是整个处理过程顺序执行,当各个过程都执行完毕,并返回结果。
异步: 异步调用则是只是发送了调用的指令,调用者无需等待被调用的方法完全执行完毕;而是继续执行下面的流程。
那实现异步调用有哪些方式呢?今天我们就来说道说道!
对于异步方法调用,从Spring3开始提供了@Async
注解,该注解可以被标注在方法上,以便异步地调用该方法。调用者将在调用时立即返回,方法的实际执行将提交给Spring TaskExecutor的任务中,由指定的线程池中的线程执行。
SimpleAsyncTaskExecutor
:不是真的线程池,这个类不重用线程,默认每次调用都会创建一个新的线程。
SyncTaskExecutor
:这个类没有实现异步调用,只是一个同步操作。只适用于不需要多线程的地方。
ConcurrentTaskExecutor
:Executor的适配类,不推荐使用。如果ThreadPoolTaskExecutor不满足要求时,才用考虑使用这个类。
SimpleThreadPoolTaskExecutor
:是Quartz的SimpleThreadPool的类。线程池同时被quartz和非quartz使用,才需要使用此类。
ThreadPoolTaskExecutor
:最常使用,推荐。其实质是对java.util.concurrent.ThreadPoolExecutor
的包装。
@Async
的默认线程池为SimpleAsyncTaskExecutor。该线程池默认来一个任务创建一个线程,若系统中不断的创建线程,最终会导致系统占用内存过高,引发OutOfMemoryError错误。在项目应用中,@Async调用线程池,推荐使用自定义线程池的模式。自定义线程池常用方案:重新实现接口AsyncConfigurer
在Spring Boot中,我们只需要通过使用@Async
注解就能简单的将原来的同步函数变为异步函数,当然前提是,这个方法所在的类必须在Spring环境中。使用@Async
注解时,将注解的value指定为你Executor类型的BeanName,就可以使用指定的线程池来作为任务的载体,这样就使用线程池也更加灵活
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
@EnableAsync //开启异步编程
public class SpringBootConfigApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootConfigApplication.class, args);
}
}
@Async
,类上添加@Component
,(确保该bean处于spring环境中)package com.alliance.javaalliance.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Component;
import java.util.Random;
import java.util.concurrent.Future;
@Component
@Slf4j
public class SyncTask {
public static Random random =new Random();
@Async
public Future<String> doTaskOne() throws Exception {
String threadName = Thread.currentThread().getName();
log.info("{}开始做任务一",threadName);
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
log.info("{}完成任务一,耗时:{}毫秒",threadName,(end - start));
return new AsyncResult<>("one done");
}
@Async
public Future<String> doTaskTwo() throws Exception {
String threadName = Thread.currentThread().getName();
log.info("{}开始做任务二",threadName);
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
log.info("{}完成任务二,耗时:{}毫秒",threadName,(end - start));
return new AsyncResult<>("one done");
}
@Async
public Future<String> doTaskThree() throws Exception {
String threadName = Thread.currentThread().getName();
log.info("{}开始做任务三",threadName);
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
log.info("{}完成任务三,耗时:{}毫秒",threadName,(end - start));
return new AsyncResult<>("one done");
}
@Async
public Future<String> doTaskFour() throws Exception {
String threadName = Thread.currentThread().getName();
log.info("{}开始做任务四",threadName);
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
log.info("{}完成任务四,耗时:{}毫秒",threadName,(end - start));
return new AsyncResult<>("one done");
}
}
为什么要写四个一样的方法,是为了后续做测试用。为什么方法的返回值是Future,是为了在测试方法中捕捉到所有方法执行结束。
package com.lara.springbootconfig;
import com.lara.springbootconfig.task.SyncTask;
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.Future;
@RunWith(SpringRunner.class)
@SpringBootTest
public class SyncTaskTest {
@Autowired
private SyncTask syncTask;
@Test
public void test() throws Exception {
long start = System.currentTimeMillis();
Future<String> one = syncTask.doTaskOne();
Future<String> two = syncTask.doTaskTwo();
Future<String> three = syncTask.doTaskThree();
Future<String> four = syncTask.doTaskFour();
//感觉下面的while空转有点不好
while (true) {
//确保所有的任务都完成,if条件才算成立
if (one.isDone() && two.isDone() && three.isDone() && four.isDone()) {
break;
}
}
long end = System.currentTimeMillis();
log.info("全部任务已完成,耗时:{}毫秒",(end - start));
}
}
可以看到四个方法启用了四个线程来执行。为什么会是这种结果呢???
这是因为在我们不指定线程池的情况下,spring默认使用SimpleAsyncTaskExecutor
,它不是真的线程池,这个类不重用线程,默认每次调用都会创建一个新的线程。当异步任务非常多时,后果不堪设想。。。
注:@Async所修饰的函数不要定义为static类型,这样异步调用不会生效
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.TaskExecutor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@Configuration
public class ThreadPoolConfig {
//上述线程池的参数含义为什么设置为2,3,4,没有为什么仅仅是为了测试使用
@Bean
public TaskExecutor taskExecutor(){
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);//核心线程数
executor.setMaxPoolSize(3);//最大线程数
executor.setKeepAliveSeconds(60);//多余线程存活时间
executor.setQueueCapacity(4);//队列容量
//指定线程名称前缀
executor.setThreadNamePrefix("taskExecutor");
return executor;
}
}
加了上述配置后,还要把用到@Async
的地方后面指定线程池名称,即@Async("taskExecutor")
,然后再次运行相同的测试代码,看看结果。
可以看到我们自定义线程池生效了,但是只有两个线程在执行任务。为什么呢?因为我们设置的核心线程是2个,前2个任务到达时,创新新的线程执行。第3个任务到达后,放进任务队列中。第4个任务到达后也放进任务队列。