Java 异步编程的几种方式

cover.jpg

前言

异步编程是让程序并发运行的一种手段。它允许多个事情同时发生,当程序调用需要长时间运行的方法时,它不会阻塞当前的执行流程,程序可以继续运行,当方法执行完成时通知给主线程根据需要获取其执行结果或者失败异常的原因。使用异步编程可以大大提高我们程序的吞吐量,可以更好的面对更高的并发场景并更好的利用现有的系统资源,同时也会一定程度上减少用户的等待时间等。本文我们一起来看看在 Java 语言中使用异步编程有哪些方式。

Thread 方式

Java 语言中最简单使用异步编程的方式就是创建一个 Thread 来实现,如果你使用的 JDK 版本是 8 以上的话,可以使用 Lambda 表达式 会更加简洁。为了能更好的体现出异步的高效性,下面提供同步版本和异步版本的示例作为对照:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/**
* @author mghio
* @since 2021-08-01
*/
public class SyncWithAsyncDemo {

public static void doOneThing() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("doOneThing ---->>> success");
}

public static void doOtherThing() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("doOtherThing ---->>> success");
}

public synchronized static void main(String[] args) throws InterruptedException {
StopWatch stopWatch = new StopWatch("SyncWithAsyncDemo");
stopWatch.start();

// 同步调用版本
// testSynchronize();

// 异步调用版本
testAsynchronize();

stopWatch.stop();
System.out.println(stopWatch);
}

private static void testAsynchronize() throws InterruptedException {
System.out.println("-------------------- testAsynchronize --------------------");

// 创建一个线程执行 doOneThing
Thread doOneThingThread = new Thread(SyncWithAsyncDemo::doOneThing, "doOneThing-Thread");
doOneThingThread.start();

doOtherThing();
// 等待 doOneThing 线程执行完成
doOneThingThread.join();
}

private static void testSynchronize() {
System.out.println("-------------------- testSynchronize --------------------");

doOneThing();
doOtherThing();
}

}

同步执行的运行如下:

1.png

注释掉同步调用版本的代码,得到异步执行的结果如下:

2.png

从两次的运行结果可以看出,同步版本耗时 4002 ms,异步版本执行耗时 2064 ms,异步执行耗时减少将近一半,可以看出使用异步编程后可以大大缩短程序运行时间。

上面的示例的异步线程代码在 main 方法内开启了一个线程 doOneThing-Thread 用来异步执行 doOneThing 任务,在这时该线程与 main 主线程并发运行,也就是任务 doOneThing 与任务 doOtherThing 并发运行,则等主线程运行完 doOtherThing 任务后同步等待线程 doOneThing 运行完毕,整体还是比较简单的。

但是这个示例只能作为示例使用,如果用到了生产环境发生事故后果自负,使用上面这种 Thread 方式异步编程存在两个明显的问题。

  1. 创建线程没有复用。我们知道频繁的线程创建与销毁是需要一部分开销的,而且示例里也没有限制线程的个数,如果使用不当可能会把系统线程用尽,从而引发事故,这个问题使用线程池可以解决。
  2. 异步任务无法获取最终的执行结果。示例中的这种方式是满足不了的,这时候就需要使用下面介绍的第二种 FutureTask 的方式了。

FutureTask 方式

JDK 1.5 开始,引入了 Future 接口和实现 Future 接口的 FutureTask 类来表示异步计算结果。这个 FutureTask 类不仅实现了 Future 接口还实现了 Runnable 接口,表示一种可生成结果的 Runnable。其可以处于这三种状态:

  • 未启动 当创建一个 FutureTask 没有执行 FutureTask.run() 方法之前
  • 已启动FutureTask.run() 方法执行的过程中
  • 已完成FutureTask.run() 方法正常执行结果或者调用了 FutureTask.cancel(boolean mayInterruptIfRunning) 方法以及在调用 FutureTask.run() 方法的过程中发生异常结束后

FutureTask 类实现了 Future 接口的开启和取消任务、查询任务是否完成、获取计算结果方法。要获取 FutureTask 任务的结果,我们只能通过调用 getXXX() 系列方法才能获取,当结果还没出来时候这些方法会被阻塞,同时这了任务可以是 Callable 类型(有返回结果),也可以是 Runnable 类型(无返回结果)。我们修改上面的示例把两个任务方法修改为返回 String 类型,使用 FutureTask 的方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private static void testFutureTask() throws ExecutionException, InterruptedException {
System.out.println("-------------------- testFutureTask --------------------");

// 创建一个 FutureTask(doOneThing 任务)
FutureTask<String> futureTask = new FutureTask<>(FutureTaskDemo::doOneThing);
// 使用线程池执行 doOneThing 任务
ForkJoinPool.commonPool().execute(futureTask);

// 执行 doOtherThing 任务
String doOtherThingResult = doOtherThing();

// 同步等待线程执行 doOneThing 任务结束
String doOneThingResult = futureTask.get();

// 任务执行结果输出
System.out.println("doOneThingResult ---->>> " + doOneThingResult);
System.out.println("doOtherThingResult ---->>> " + doOtherThingResult);
}

使用 FutureTask 异步编程方式的耗时和上面的 Thread 方式是差不多的,其本质都是另起一个线程去做 doOneThing 任务然后等待返回,运行结果如下:

3.png

这个示例中,doOneThingdoOtherThing 都是有返回值的任务(都返回 String 类型结果),我们在主线程 main 中创建一个异步任务 FutureTask 来执行 doOneThing,然后使用 ForkJoinPool.commonPool() 创建线程池(有关 ForkJoinPool 的介绍见 这里),然后调用了线程池的 execute 方法把 futureTask 提交到线程池来执行。

通过示例可以看到,虽然 FutureTask 提供了一些方法让我们获取任务的执行结果、任务是否完成等,但是使用还是比较复杂,在一些较为复杂的场景(比如多个 FutureTask 之间的关系表示)的编码还是比较繁琐,还是当我们调用 getXXX() 系列方法时还是会在任务执行完毕前阻塞调用线程,达不到异步编程的效果,基于这些问题,在 JDK 8 中引入了 CompletableFuture 类,下面来看看如何使用 CompletableFuture 来实现异步编程。

CompletableFuture 方式

JDK 8 中引入了 CompletableFuture 类,实现了 FutureCompletionStage 接口,为异步编程提供了一些列方法,如 supplyAsyncrunAsyncthenApplyAsync 等,除此之外 CompletableFuture 还有一个重要的功能就是可以让两个或者多个 CompletableFuture 进行运算来产生结果。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
* @author mghio
* @since 2021-08-01
*/
public class CompletableFutureDemo {

public static CompletableFuture<String> doOneThing() {
return CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "doOneThing";
});
}

public static CompletableFuture<String> doOtherThing(String parameter) {
return CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return parameter + " " + "doOtherThing";
});
}

public static void main(String[] args) throws ExecutionException, InterruptedException {
StopWatch stopWatch = new StopWatch("CompletableFutureDemo");
stopWatch.start();

// 异步执行版本
testCompletableFuture();

stopWatch.stop();
System.out.println(stopWatch);
}

private static void testCompletableFuture() throws InterruptedException, ExecutionException {
// 先执行 doOneThing 任务,后执行 doOtherThing 任务
CompletableFuture<String> resultFuture = doOneThing().thenCompose(CompletableFutureDemo::doOtherThing);

// 获取任务结果
String doOneThingResult = resultFuture.get();

// 获取执行结果
System.out.println("DoOneThing and DoOtherThing execute finished. result = " + doOneThingResult);
}

}

执行结果如下:

4.png

在主线程 main 中首先调用了方法 doOneThing() 方法开启了一个异步任务,并返回了对应的 CompletableFuture 对象,我们取名为 doOneThingFuture,然后在 doOneThingFuture 的基础上使用 CompletableFuturethenCompose() 方法,让 doOneThingFuture 方法执行完成后,使用其执行结果作为 doOtherThing(String parameter) 方法的参数创建的异步任务返回。

我们不需要显式使用 ExecutorService,在 CompletableFuture 内部使用的是 Fork/Join 框架异步处理任务,因此,它使我们编写的异步代码更加简洁。此外,CompletableFuture 类功能很强大其提供了和很多方便的方法,更多关于 CompletableFuture 的使用请见 这篇

总结

本文介绍了在 Java 中的 JDK 使用异步编程的三种方式,这些是我们最基础的实现异步编程的工具,在其之上的还有 Guava 库提供的 ListenableFutureFutures 类以及 Spring 框架提供的异步执行能力,使用 @Async 等注解实现异步处理,感兴趣的话可以自行学习了解。

-------------本文结束感谢您的阅读-------------
mghio wechat
微信公众号「mghio」
请我吃🍗