异步编程
JUC基础概念
- Java.Util.Concurrent包简称JUC,它主要是负责处理线程,实现多线程通信、线程安全、线程间高并发的工具包。
- 进程与线程
- 进程:一个程序,由一个或多个进程组成,每个进程都有自己独立的内存空间,进程间通信需要通过操作系统的机制来实现。
进程状态:新建态、就绪态、运行态、阻塞态、终止态、(阻塞挂起、就绪挂起) - 线程:一个进程可以有多个线程,每个线程都有自己独立的内存空间,线程间通信不需要操作系统的机制来实现。
线程状态:新建态、就绪态、运行态、阻塞态、终止态
- 进程:一个程序,由一个或多个进程组成,每个进程都有自己独立的内存空间,进程间通信需要通过操作系统的机制来实现。
- 并发与并行
- 并发:一段时间内,多个进程或线程交替运行
- 并行:多个进程或线程同时运行
- 串行:一个进程或线程依次一个一个地运行
应用场景
-
场景: 内控系统中获取一个单据的完整信息:
①获取单据主表信息
②获取单据明细
③获取单据附件
④获取单据审批记录
⑤获取单据审批人信息
⑥计算金额等操作
其中每个操作都是一个任务,需要串行执行,才能获取到完整单据信息。在数据量大的情况下假设一个任务需要执行0.5s
,那么整个流程需要执行3-4s
,用户加载出页面数据需要等待就4s往上了。
如果使用异步多线程并行操作,可以同时获取到单据信息,同时计算金额等操作,同时获取单据附件,同时获取单据审批记录,同时获取单据审批人信息,同时获取单据主表信息,同时获取单据明细。 -
场景:数据预处理
在数据正式进入审核流程之前,可能需要进行预处理,如数据清洗、格式校验等
JDK中的线程池
线程池继承关系
JDK中提供的几类线程池
//创建一个核心线程数和最大线程数相同的线程池
ExecutorService executorService = Executors.newFixedThreadPool(4);
executorService.execute(()->{System.out.println("this is fixed thread pool");});
//创建一个单线程的线程池
ExecutorService singleExecutor = Executors.newSingleThreadExecutor();
singleExecutor.execute(() -> System.out.println("this is single thread pool"));
//创建一个缓存线程池
ExecutorService cacheExecutor = Executors.newCachedThreadPool();
cacheExecutor.execute(()-> System.out.println("this is cache thread pool"));
//创建一个延时执行任务的线程池
ScheduledExecutorService scheduleExecutor = Executors.newScheduledThreadPool(4);
scheduleExecutor.schedule(()-> System.out.println("this is schedule thread pool"),2,TimeUnit.SECONDS);
每种线程池的特点一览
线程池名称 | 使用阻塞队列 | 特点 |
---|---|---|
newFixedThreadPool | LinkedBlockingQueue() | 1. 核心线程数和最大线程数相同 2. 由于keepAliveTime设置为0,当线程创建后会一直存在 3. 由于用的是无界队列所以可能会导致OOM |
newSingleThreadExecutor | LinkedBlockingQueue() | 1. 核心线程数和最大线程数都为1单线程 2. 无界队列可能导致OOM |
newCachedThreadPool | SynchronousQueue() | 1. 核心线程数为0,最大线程数为Integer.MAX_VALUE 2. 当没任务时线程存活时间为60秒 3. 使用的是0大小的队列,所以不存储任务,只做任务转发 |
newScheduledThreadPool | DelayedWorkQueue() | 1. 执行周期任务 2. 无界队列,可能会导致OOM |
从上面可以看出他们各有各的特点,但是阿里巴巴开发守则却不推荐使用以上线程池,因为它们可能会对服务资源的浪费,所以推荐使用通过ThreadPoolExecutor自定线程池。
Spring中将Java中的线程池进行了封装,而且提供了默认实现,也能自定义线程池
ThreadPoolTaskExecutor类
特点:
-
线程池配置: ThreadPoolTaskExecutor 允许你配置核心线程数、最大线程数、队列容量等线程池属性。
-
线程创建和销毁: 它会根据任务的需求自动创建和销毁线程,避免不必要的线程创建和销毁开销。
-
线程复用: 线程池中的线程可以被复用,从而减少线程创建的开销。
-
队列管理: 当线程池达到最大线程数时,新任务会被放入队列中等待执行。
-
拒绝策略: 当线程池已满并且队列也已满时,可以配置拒绝策略来处理新任务的方式。
- AbortPolicy,直接抛出RejectedExecutionException
- CallerRunsPolicy,直接在主线程中执行
- DiscardOldestPolicy 抛弃队列头的任务,然后重试execute。
- DiscardPolicy,直接丢弃
使用spring提供的ThreadPoolTaskExecutor类,有两种方式,一种是在代码中显式注入ThreadPoolTaskExecutor
,另一种是使用@Async注解
- 使用Async注解
分两步 1. 在启动类上添加@EnableAsync注解,开启异步注解功能 2. 在方法上添加@Async注解,标识为异步方法 即可使用异步多线程功能
@Slf4j // 日志
@EnableAsync // 开启异步
@EnableCaching // 开启缓存
@EnableTransactionManagement // 开启事务
@EnableScheduling // 开启定时
@ComponentScan(basePackages={"com.cxnet"})
//@MapperScan("com.cxnet") // Mapper扫描
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class BootstrapApplication {
public static void main(String[] args) {
SpringApplication.run(BootstrapApplication.class, args);
log.info(">>>>>> The expenses service started successfully. >>>>>> 内控服务启动成功!");
}
@Bean
public RequestContextListener requestContextListener(){
return new RequestContextListener();
}
}
当然 使用注解很方便 需要注意@Async的失效场景
- 异步方法使用static修饰
- 异步类没有使用@Component注解(或其他注解)导致spring无法扫描到异步类
- 异步方法不能与被调用的异步方法在同一个类中
- 类中需要使用@Autowired或@Resource等注解自动注入,不能自己手动new对象
- 如果使用SpringBoot框架必须在启动类中增加@EnableAsync注解
在很多场景下对于代码的细粒度控制不足,这时候就需要使用到显式注入ThreadPoolTaskExecutor
Spring 提供了 ThreadPoolTaskExecutor 类,这是一个 TaskExecutor 接口的实现,可以用来创建线程池
示例:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@Configuration
public class TaskExecutorConfig {
@Bean
public ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程数,默认为1
executor.setCorePoolSize(10);
// 最大线程数,默认为Integer.MAX_VALUE
executor.setMaxPoolSize(20);
// 队列容量,默认为Integer.MAX_VALUE
executor.setQueueCapacity(200);
// 线程活跃时间,当线程数大于核心线程数时,这是多余空闲线程在终止前等待新任务的最长时间。
executor.setKeepAliveSeconds(60);
// 线程名称前缀
executor.setThreadNamePrefix("MyExecutor-");
// 设置拒绝策略,当pool已经达到max size的时候,如何处理新任务
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 初始化执行器,以确保其在启动时立即开始
executor.initialize();
return executor;
}
}
代码中使用直接通过@Autowired注入线程池ThreadPoolTaskExecutor
@Autowired
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
ThreadPoolTaskExecutor提供主要提供两种任务提交模式,分别为:
- 无返回值的任务使用
execute(Runnable)
- 有返回值的任务使用
submit(Runnable)
threadPoolTaskExecutor.execute(() -> {
// 无返回值
});
threadPoolTaskExecutor.submit(() -> {
// 有返回值
return "Hello World";
});
ThreadPoolTaskExecutor处理流程
当往线程池中提交新任务时,线程池主要流程如下:
核心线程数 -> 线程队列 -> 最大线程数 -> 拒绝策略
-
当一个任务被提交到线程池时,首先查看线程池的核心线程是否都在执行任务,否就选择一条线程执行任务,是就执行第二步。
-
查看核心线程池是否已满,不满就创建一条线程执行任务,否则执行第三步。
-
查看任务队列是否已满,不满就将任务存储在任务队列中,否则执行第四步。
-
查看线程池是否已满,不满就创建一条线程执行任务,否则就按照策略处理无法执行的任务。
在ThreadPoolExecutor中表现为:
- 如果当前运行的线程数小于corePoolSize,那么就创建线程来执行任务(执行时需要获取全局锁)。
- 如果运行的线程大于或等于corePoolSize,那么就把task加入BlockQueue。
- 如果创建的线程数量大于BlockQueue的最大容量,那么创建新线程来执行该任务。
- 如果创建线程导致当前运行的线程数超过maximumPoolSize,就根据饱和策略来拒绝该任务。
Future与CompletableFuture 函数式异步编程
Future
是一个接口,它表示一个异步计算任务,可以通过 get()
方法获取计算结果。但是,如果任务尚未完成,get()
方法会阻塞当前线程,直到任务完成。
CompletableFuture
是一个类,它提供了比 Future
更丰富的 API,可以更方便的处理异步任务。例如,可以使用 thenApply()
方法将一个异步任务转换为另一个异步任务,使用 thenCompose()
方法将两个异步任务组合成一个新的异步任务,使用 thenAccept()
方法将一个异步任务的结果作为另一个异步任务的输入。
Future 和 CompletableFuture 都是 Java 5 中引入的,它们之间的主要区别在于:
- CompletableFuture 提供了更丰富的 API,包括链式调用、异常处理等。
- CompletableFuture 可以通过回调的方式处理异步任务的结果,而 Future 只能通过
get()
方法获取结果。 - CompletableFuture 可以通过
thenApply()
、thenCompose()
等方法组合多个异步任务,而 Future 只能通过get()
方法等待单个任务的完成。 - CompletableFuture 可以通过
whenComplete()
、exceptionally()
等方法处理异步任务的完成情况,而 Future 只能通过isDone()
方法判断任务是否完成。 - CompletableFuture 可以通过
allOf()
、anyOf()
等方法等待多个异步任务的完成,而 Future 只能通过get()
方法等待单个任务的完成。 - CompletableFuture 可以通过
runAsync()
、supplyAsync()
等方法异步执行任务,而 Future 只能通过submit()
方法提交任务。 - CompletableFuture 可以通过
join()
方法等待任务完成并获取结果,而 Future 只能通过get()
方法等待任务完成。 - CompletableFuture 可以通过
handle()
方法处理异步任务的结果和异常,而 Future 只能通过whenComplete()
方法处理异步任务的完成情况。 - CompletableFuture 可以通过
thenCombine()
、thenAcceptBoth()
等方法将多个异步任务的结果合并成一个新的异步任务,而 Future 只能通过thenApply()
、thenAccept()
等方法将单个异步任务的结果转换为另一个异步任务。 - CompletableFuture 可以通过
orTimeout()
方法设置超时时间,而 Future 只能通过setTimeout()
方法设置超时时间。
Future代码示例(不常用):
public void buyCoffeeAndOthers() throws ExecutionException, InterruptedException {
goShopping();
// 子线程中去处理做咖啡这件事,返回future对象
Future<Coffee> coffeeTicket = threadPool.submit(this::makeCoffee);
// 主线程同步去做其他的事情
Bread bread = buySomeBread();
// 主线程其他事情并行处理完成,阻塞等待获取子线程执行结果
Coffee coffee = coffeeTicket.get();
// 子线程结果获取完成,主线程继续执行
eatAndDrink(bread, coffee);
}
Future在应对一些简单且相互独立的异步执行场景很便捷,但是在一些复杂的场景,比如同时需要多个有依赖关系的异步独立处理的时候,或者是一些类似流水线的异步处理场景时,就显得力不从心了。
比如:
- 同时执行多个并行任务,等待最快的一个完成之后就可以继续往后处理
- 多个异步任务,每个异步任务都需要依赖前一个异步任务执行的结果再去执行下一个异步任务,最后只需要一个最终的结果
- 等待多个异步任务全部执行完成后触发下一个动作执行
| 方法名称 | 作用描述 |
| -------------- | --------------------------------------------------------------------------------- |
| supplyAsync | 静态方法,用于构建一个CompletableFuture对象,并异步执行传入的函数,允许执行函数有返回值T。 |
| runAsync | 静态方法,用于构建一个CompletableFuture对象,并异步执行传入函数,仅执行,没有返回值。 |
使用示例:
public void testCreateFuture(String product) {
// supplyAsync, 执行逻辑有返回值PriceResult
CompletableFuture<PriceResult> supplyAsyncResult =
CompletableFuture.supplyAsync(() -> HttpRequestMock.getMouBaoPrice(product));
// runAsync, 执行逻辑没有返回值
CompletableFuture<Void> runAsyncResult =
CompletableFuture.runAsync(() -> System.out.println(product));
}
在流水线处理场景中,往往都是一个任务环节处理完成后,下一个任务环节接着上一环节处理结果继续处理。
CompletableFuture用于这种流水线环节驱动类的方法有很多,相互之间主要是在返回值或者给到下一环节的入参上有些许差异,使用时需要注意区分:
- thenApply:将前一个任务的结果作为参数,传入后一个任务,并返回后一个任务的结果。
- thenAccept:将前一个任务的结果作为参数,传入后一个任务,但后一个任务没有返回值。
- thenRun:不接收前一个任务的结果,直接传入后一个任务,但后一个任务没有返回值。
- thenCompose:将前一个任务的结果作为参数,传入后一个任务,并返回后一个任务的CompletableFuture对象,后一个任务可以返回一个CompletableFuture对象,也可以返回一个普通对象。
- thenCombine:将前一个任务的结果和后一个任务的结果作为参数,传入后一个任务,并返回后一个任务的结果。
- anyOf:将多个任务的CompletableFuture对象作为参数,返回一个CompletableFuture对象,当任意一个任务的结果返回时,返回该任务的结果。
注意:
如果使用thenApply,thenAccept,thenRun,thenCompose,thenCombine,anyOf,allOf等方法,一定要注意返回值类型,否则会抛出异常。
CompletableFuture提供了 handle
和 whenComplete
等方法,用于处理异步任务的完成情况,包括成功和失败的情况。
- handle:将前一个任务的结果作为参数,传入后一个任务,并返回后一个任务的结果。与thenApply类似,区别点在于handle执行函数的入参有两个,一个是CompletableFuture执行的实际结果,一个是是Throwable对象,这样如果前面执行出现异常的时候,可以通过handle获取到异常并进行处理。
- whenComplete:将前一个任务的结果作为参数,传入后一个任务,但后一个任务没有返回值。
使用示例:
public void testExceptionHandle() {
CompletableFuture.supplyAsync(() -> {
throw new RuntimeException("supplyAsync excetion occurred...");
}).handle((obj, e) -> {
if (e != null) {
System.out.println("thenApply executed, exception occurred...");
}
return obj;
}).join();
}
异步结果等待与获取
在执行线程中将任务放到工作线程中进行处理的时候,执行线程与工作线程之间是异步执行的模式,
如果执行线程需要获取到共工作线程的执行结果,则可以通过get或者join方法,阻塞等待并从CompletableFuture中获取对应的值。
对get和join的方法功能含义说明归纳如下:
- get()等待CompletableFuture执行完成并获取其具体执行结果,可能会抛出异常,需要代码调用的地方手动try…catch进行处理。
- get(long, TimeUnit)与get()相同,只是允许设定阻塞等待超时时间,如果等待超过设定时间,则会抛出异常终止阻塞等待。
- join()等待CompletableFuture执行完成并获取其具体执行结果,可能会抛出运行时异常,无需代码调用的地方手动try…catch进行处理。
使用示例:
public void testGetAndJoin(String product) {
// join无需显式try...catch...
PriceResult joinResult = CompletableFuture.supplyAsync(() -> HttpRequestMock.getMouXiXiPrice(product))
.join();
try {
// get显式try...catch...
PriceResult getResult = CompletableFuture.supplyAsync(() -> HttpRequestMock.getMouXiXiPrice(product))
.get(5L, TimeUnit.SECONDS);
} catch (Exception e) {
e.printStackTrace();
}
}
CompletableFuture配合自定义线程池使用
CompletableFuture使用的默认线程池为ForkJoinPool
, 它默认的线程数量为CPU核数,可以通过修改ForkJoinPool
的构造函数来修改默认的线程数量。
或者可以使用自定义线程池,通过CompletableFuture.supplyAsync(Runnable runnable, Executor executor)
方法来指定自定义线程池。
例如:
public PriceResult getCheapestPlatAndPrice4(String product) {
// 构造自定义线程池
ExecutorService executor = Executors.newFixedThreadPool(5);
return CompletableFuture.supplyAsync(
() -> HttpRequestMock.getMouXiXiPrice(product),
executor
).thenCombineAsync(
CompletableFuture.supplyAsync(() -> HttpRequestMock.getMouXiXiDiscounts(product)),
this::computeRealPrice,
executor
).join();
}