Baeldung 翻译系列之Java并发基础:
Java中的concurrent包
Java中的Synchronized关键字
Future介绍
ThreadLocal介绍
Java线程的生命周期
如何杀掉一个Java线程
Java中的线程池介绍
实现Runnable接口还是继承Thread类
Java中的wait和notify方法
Runnable vs Callable
wait和sleep的区别
Thread.join方法介绍
Java中使用锁对象
ThreadPoolTaskExecutor中的corePoolSize和maxPoolSize
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();
在这里,我们使用了Future
接口提供的isDone
方法来检查任务是否已完成。一旦完成,我们可以使用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
类来执行异步操作。
首先,我们将添加最新的Guava Maven依赖项:
<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
等方法,用于链式处理ListenableFuture
,类似于CompletableFuture
。
例如,让我们看看如何在ListeningExecutorService.submit
方法的位置使用Futures.submitAsync
:
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-async Maven
依赖项:
<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);
}
在这里,我们在静态块中调用Async.init
方法来初始化Async运行时工具。
Async
工具在运行时转换代码,并重写对await
方法的调用,使其行为类似于使用CompletableFuture
链式调用。
因此,对await
方法的调用类似于调用Future.join
。
我们可以使用-javaagent
JVM参数进行编译时的工具转换。这是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库。
它是Google 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
提供了通过AspectJ AOP
切面来实现异步编程的@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
织入,在factorialUsingJcabiAspect
方法的异步执行中,将AOP建议注入到@Async
注解的位置。
让我们使用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): 'java.util.concurrent.CompletableFuture@14e2d7c1[Completed normally]' in 44.64µs
正如我们所看到的,该库创建了一个名为jcabi-async
的新守护线程,以异步执行任务。
类似地,日志记录由库提供的@Loggable
注解启用。
7. 总结
在本文中,我们学习了Java中几种异步编程的方式。
首先,我们探讨了Java的内置功能,如FutureTask
和CompletableFuture
,用于异步编程。然后,我们研究了一些库,如EA Async
和Cactoos
,提供了开箱即用的解决方案。
我们还讨论了使用Guava的ListenableFuture
和Futures
类执行异步任务的方法。最后,我们简要介绍了jcabi-AspectJ
库,它通过其@Async
注解提供AOP
功能,用于异步方法调用。
评论区