Java中的异步编程
1.概述
随着对编写非阻塞代码的需求越来越大,我们需要有办法异步执行代码。
在本教程中,我们将研究在 Java 中实现异步编程的几种方法。我们还将探讨一些提供开箱即用解决方案的Java库。
2.Java中的异步编程
2.1. Thread
我们可以创建一个新的线程,以异步方式执行任何操作。随着Java 8中lambda表达式的发布,它变得更干净、更易读。
让我们创建一个新的线程,计算并打印一个数字的阶乘:
int number = 20;
Thread newThread = new Thread(() -> {
System.out.println("Factorial of " + number + " is: " + factorial(number));
});
newThread.start();
2.2. FutureTask
自Java 5以来,Future接口提供了一种使用FutureTask的方法来执行异步操作。
我们可以使用ExecutorService得submit方法来异步执行任务,并返回FutureTask的实例。
因此,让我们来寻找一个数字的阶乘:
ExecutorService threadpool = Executors.newCachedThreadPool();
Future<Long> futureTask = threadpool.submit(() -> factorial(number));
while (!futureTask.isDone()) {
System.out.println("FutureTask is not finished yet...");
}
long result = futureTask.get();
threadpool.shutdown();
这里我们使用了isDone方法,由Future接口提供,以检查任务是否已经完成。一旦完成,我们可以使用get方法来检索结果。
2.3. CompletableFuture
Java 8引入了CompletableFuture,结合了Future和CompletionStage。它提供了各种方法,如supplyAsync、runAsync和thenApplyAsync,用于异步程序设计。
现在让我们用CompletableFuture代替FutureTask来求一个数字的阶乘:
CompletableFuture<Long> completableFuture = CompletableFuture.supplyAsync(() -> factorial(number));
while (!completableFuture.isDone()) {
System.out.println("CompletableFuture is not finished yet...");
}
long result = completableFuture.get();
我们不需要明确地使用ExecutorService。CompletableFuture 在内部使用ForkJoinPool来异步处理任务。因此,它使我们的代码更加简洁。
3. Guava
Guava提供了ListenableFuture类,以执行异步操作。
首先,我们要添加最新的guavaMaven依赖项:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.0.1-jre</version>
</dependency>
然后让我们用ListenableFuture来寻找一个数字的阶乘:
ExecutorService threadpool = Executors.newCachedThreadPool();
ListeningExecutorService service = MoreExecutors.listeningDecorator(threadpool);
ListenableFuture<Long> guavaFuture = (ListenableFuture<Long>) service.submit(()-> factorial(number));
long result = guavaFuture.get();
这里MoreExecutors类提供了ListeningExecutorService类的实例。然后ListeningExecutorService.submit方法异步地执行任务,并返回ListenableFuture的实例。
Guava还有一个Futures类,它提供了submitAsync、scheduleAsync和transformAsync等方法,以连锁ListenableFutures,类似于CompletableFuture.。
例如,让我们看看如何使用Futures.submitAsync来代替ListeningExecutorService.submit方法:
ListeningExecutorService service = MoreExecutors.listeningDecorator(threadpool);
AsyncCallable<Long> asyncCallable = Callables.asAsyncCallable(new Callable<Long>() {
public Long call() {
return factorial(number);
}
}, service);
ListenableFuture<Long> guavaFuture = Futures.submitAsync(asyncCallable, service);
这里submitAsync方法需要一个AsyncCallable的参数,这个参数是用Callables类创建的。
此外,Futures类提供了addCallback方法,以注册成功和失败的回调:
Futures.addCallback(
factorialFuture,
new FutureCallback<Long>() {
public void onSuccess(Long factorial) {
System.out.println(factorial);
}
public void onFailure(Throwable thrown) {
thrown.getCause();
}
},
service);
4. EA Async
Electronic Arts通过ea-async库将.NET中的async-await功能带到了Java生态系统中。
这个库允许按顺序编写异步(非阻塞)代码。因此,它使异步编程更容易,并能自然扩展。
首先,我们要在pom.xml中添加最新的ea-asyncMaven依赖项:
<dependency>
<groupId>com.ea.async</groupId>
<artifactId>ea-async</artifactId>
<version>1.2.3</version>
</dependency>
然后,我们将通过使用EA的Async类提供的await方法,对之前讨论的CompletableFuture代码进行改造:
static {
Async.init();
}
public long factorialUsingEAAsync(int number) {
CompletableFuture<Long> completableFuture = CompletableFuture.supplyAsync(() -> factorial(number));
long result = Async.await(completableFuture);
}
在这里,我们对static块中的Async.init方法进行了调用,以初始化Async运行时检测。
Async检测在运行时转变了代码,并重写了对await方法的调用,使其行为类似于使用CompletableFuture的链。
因此,对await 方法的调用类似于调用Future.join 。
我们可以使用–javaagentJVM参数来进行编译时检测。这是对Async.init 方法的一种替代:
java -javaagent:ea-async-1.2.3.jar -cp <claspath> <MainClass>
现在让我们来看看另一个按顺序编写异步代码的例子。
首先,我们将使用CompletableFuture类的thenComposeAsync和thenAcceptAsync等组成方法,异步地执行一些链式操作:
CompletableFuture<Void> completableFuture = hello()
.thenComposeAsync(hello -> mergeWorld(hello))
.thenAcceptAsync(helloWorld -> print(helloWorld))
.exceptionally(throwable -> {
System.out.println(throwable.getCause());
return null;
});
completableFuture.get();
然后,我们可以使用EA的Async.await()来转换该代码:
try {
String hello = await(hello());
String helloWorld = await(mergeWorld(hello));
await(CompletableFuture.runAsync(() -> print(helloWorld)));
} catch (Exception e) {
e.printStackTrace();
}
这个实现类似于顺序阻塞的代码;但是,await方法并没有阻塞代码。
正如所讨论的,对await方法的所有调用将被Async工具化重写,以类似于Future.join 方法的方式工作。
因此,一旦hello方法的异步执行完成,Future的结果就会传递给mergeWorld方法。然后,该结果被传递给使用CompletableFuture.runAsync方法的最后一次执行。
5. Cactoos
Cactoos是一个基于面向对象的原则的Java库。
它是谷歌Guava和Apache Commons的替代方案,提供了执行各种操作的通用对象。
首先,让我们添加最新的cactoos Maven依赖项:
<dependency>
<groupId>org.cactoos</groupId>
<artifactId>cactoos</artifactId>
<version>0.43</version>
</dependency>
这个库提供了一个Async类,用于异步操作。
因此,我们可以使用Cactoos的Async类的实例来查找一个数字的阶乘:
Async<Integer, Long> asyncFunction = new Async<Integer, Long>(input -> factorial(input));
Future<Long> asyncFuture = asyncFunction.apply(number);
long result = asyncFuture.get();
这里的apply方法使用ExecutorService.submit方法来执行操作,并返回一个Future接口的实例。
同样,Async类也有exec方法,它提供了相同的功能,但没有返回值。
注意:Cactoos库处于开发的初始阶段,可能还不适合在生产中使用。
6. Jcabi-Aspects
Jcabi-Aspects通过AspectJAOP方面为异步编程提供了@Async注解。
首先,让我们添加最新的jcabi-aspects Maven依赖项:
<dependency>
<groupId>com.jcabi</groupId>
<artifactId>jcabi-aspects</artifactId>
<version>0.22.6</version>
</dependency>
jcabi-aspects库需要AspectJ运行时支持,所以我们要添加aspectjrt Maven依赖:
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.5</version>
</dependency>
接下来,我们将添加jcabi-maven-plugin插件,将二进制文件与AspectJ方面结合起来。该插件提供了ajc目标,为我们做了所有的工作:
<plugin>
<groupId>com.jcabi</groupId>
<artifactId>jcabi-maven-plugin</artifactId>
<version>0.14.1</version>
<executions>
<execution>
<goals>
<goal>ajc</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjtools</artifactId>
<version>1.9.1</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.1</version>
</dependency>
</dependencies>
</plugin>
现在,我们已经准备好使用AOP方面的异步编程了:
@Async
@Loggable
public Future<Long> factorialUsingJcabiAspect(int number) {
Future<Long> factorialFuture = CompletableFuture.supplyAsync(() -> factorial(number));
return factorialFuture;
}
当我们编译代码时,库将通过AspectJ编织注入AOP建议以代替@Async注解,用于factorialUsingJcabiAspect方法的异步执行。
让我们用Maven命令来编译该类:
mvn install
jcabi-maven-plugin的输出结果可能是这样的:
--- jcabi-maven-plugin:0.14.1:ajc (default) @ java-async ---
[INFO] jcabi-aspects 0.18/55a5c13 started new daemon thread jcabi-loggable for watching of @Loggable annotated methods
[INFO] Unwoven classes will be copied to /tutorials/java-async/target/unwoven
[INFO] jcabi-aspects 0.18/55a5c13 started new daemon thread jcabi-cacheable for automated cleaning of expired @Cacheable values
[INFO] ajc result: 10 file(s) processed, 0 pointcut(s) woven, 0 error(s), 0 warning(s)
我们可以通过检查Maven插件生成的jcabi-ajc.log文件中的日志来验证我们的类是否被正确编织了:
Join point 'method-execution(java.util.concurrent.Future
com.baeldung.async.JavaAsync.factorialUsingJcabiAspect(int))'
in Type 'com.baeldung.async.JavaAsync' (JavaAsync.java:158)
advised by around advice from 'com.jcabi.aspects.aj.MethodAsyncRunner'
(jcabi-aspects-0.22.6.jar!MethodAsyncRunner.class(from MethodAsyncRunner.java))
然后我们将该类作为一个简单的Java应用程序来运行,输出结果会是这样的:
17:46:58.245 [main] INFO com.jcabi.aspects.aj.NamedThreads -
jcabi-aspects 0.22.6/3f0a1f7 started new daemon thread jcabi-loggable for watching of @Loggable annotated methods
17:46:58.355 [main] INFO com.jcabi.aspects.aj.NamedThreads -
jcabi-aspects 0.22.6/3f0a1f7 started new daemon thread jcabi-async for Asynchronous method execution
17:46:58.358 [jcabi-async] INFO com.baeldung.async.JavaAsync -
#factorialUsingJcabiAspect(20): '[email protected][Completed normally]' in 44.64µs
我们可以看到,一个新的守护线程,jcabi-async,是由异步执行任务的库创建的。
同样,日志记录也是由库提供的@Loggable注解来启用的。
7.结语
在这篇文章中,我们学习了Java中异步编程的一些方法。
首先,我们探索了Java的内置功能,如FutureTask和CompletableFuture,用于异步编程。然后,我们研究了一些库,如EA Async和Cactoos,它们有开箱即用的解决方案。
我们还讨论了使用 Guava 的 ListenableFuture 和 Futures 类异步执行任务的支持。最后,我们介绍了 jcabi-AspectJ 库,该库通过其用于异步方法调用的 @Async 注释提供 AOP 功能。
像往常一样,所有的代码实现都可以在GitHub上找到。