1.概述

本教程中我们将分别介绍几种实现Java 异步编程的方法。一种是通过Java原生方法实现,另一种使用第三方库实现异步编程。

2. Java 原生实现方法

2.1. Thread

第一种方式,我们可以通过创建一个新线程来执行所有的异步操作。Java8后使用 lambda表达式,可以使代码变的更简洁,更可读。

下面示例中,我们创建一个新线程来计算和打印给定数字的阶乘:

int number = 20;
Thread newThread = new Thread(() -> {
    System.out.println("Factorial of " + number + " is: " + factorial(number));
});
newThread.start();

2.2. Futuretask

自Java5,我们可以使用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 ,它实现了 FutureCompletionStage 接口。 并提供了多种实现异步编程的方法,如supplyAsync, runAsync以及thenApplyAsync

下面我们使用CompletableFuture来实现上面的例子:

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 来处理异步任务。这使得我们的代码变的更简洁。

目前我们介绍了几种使用Java原生方法实现的异步编程,下面介绍如何使用第三方库实现。

3. Guava

Guava 提供了 ListenableFuture 类来执行异步操作

首先我们需要添加 guava 的maven依赖:

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>28.2-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方法,类似于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 库,它使得我们可以在 Java 中实现 .NET 的 async-await 功能

该库允许按顺序编写异步(非阻塞)代码。 因此,它使异步编程变的更容易、更自然。

首先,我们需要在pom.xml中添加最新的 ea-async maven 依赖:

<dependency>
    <groupId>com.ea.async</groupId>
    <artifactId>ea-async</artifactId>
    <version>1.2.3</version>
</dependency>

然后,我们把之前使用CompletableFuture实现的代码,改用EA提供的Async类实现。

    static { 
        Async.init(); 
    }
    
    public long factorialUsingEAAsync(int number) {
        CompletableFuture<Long> completableFuture = CompletableFuture.supplyAsync(() -> factorial(number));
        long result = Async.await(completableFuture);
    }

Here, we make a call to the Async.init method in the static block to initialize the Async runtime instrumentation.

Async instrumentation transforms the code at runtime and rewrites the call to the await method, to behave similarly to using the chain of CompletableFuture.

Therefore, the call to the await method is similar to calling Future.join.

We can use the – javaagent JVM parameter for compile-time instrumentation. This is an alternative to the Async.init method:

java -javaagent:ea-async-1.2.3.jar -cp <claspath> <MainClass>

Let's examine another example of writing asynchronous code sequentially.

First, we'll perform a few chain operations asynchronously using the composition methods like thenComposeAsync and thenAcceptAsync of the CompletableFuture class:

CompletableFuture<Void> completableFuture = hello()
  .thenComposeAsync(hello -> mergeWorld(hello))
  .thenAcceptAsync(helloWorld -> print(helloWorld))
  .exceptionally(throwable -> {
      System.out.println(throwable.getCause()); 
      return null;
  });
completableFuture.get();

Then, we can transform the code using EA's Async.await():

try {
    String hello = await(hello());
    String helloWorld = await(mergeWorld(hello));
    await(CompletableFuture.runAsync(() -> print(helloWorld)));
} catch (Exception e) {
    e.printStackTrace();
}

The implementation resembles the sequential blocking code. However, the await method doesn't block the code.

As discussed, all calls to the await method will be rewritten by the Async instrumentation to work similarly to the Future.join method.

So, once the asynchronous execution of the hello method is finished, the Future result is passed to the mergeWorld method. Then, the result is passed to the last execution using the CompletableFuture.runAsync method.

5. Cactoos

Cactoos 是一个基于面向对象原则的 Java 库

It is an alternative to Google Guava and Apache Commons that provides common objects for performing various operations.

First, let's add the latest cactoos Maven dependency:

<dependency>
    <groupId>org.cactoos</groupId>
    <artifactId>cactoos</artifactId>
    <version>0.43</version>
</dependency>

The library provides an Async class for asynchronous operations.

So, we can find the factorial of a number using the instance of Cactoos's Async class:

Async<Integer, Long> asyncFunction = new Async<Integer, Long>(input -> factorial(input));
Future<Long> asyncFuture = asyncFunction.apply(number);
long result = asyncFuture.get();

Here, the apply method executes the operation using the ExecutorService.submit method and returns an instance of the Future interface.

Similarly, the Async class has the exec method that provides the same feature without a return value.

Note: the Cactoos library is in the initial stages of development and may not be appropriate for production use yet.

6. Jcabi-Aspects

Jcabi-Aspects 通过AspectJ AOP aspects 支持以 @Async 注解的方式实现异步编程。

首先,添加 jcabi-aspects 的 maven 依赖:

<dependency>
    <groupId>com.jcabi</groupId>
    <artifactId>jcabi-aspects</artifactId>
    <version>0.22.6</version>
</dependency>

jcabi-aspects 库依赖于 AspectJ 运行时支持。所以我们还需要添加 aspectjrt 依赖:

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.9.5</version>
</dependency>

Next, we'll add the jcabi-maven-plugin plugin that weaves the binaries with AspectJ aspects. The plugin provides the ajc goal that does all the work for us:

<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>

So, we're all set to use the AOP aspects for asynchronous programming:

@Async
@Loggable
public Future<Long> factorialUsingAspect(int number) {
    Future<Long> factorialFuture = CompletableFuture.completedFuture(factorial(number));
    return factorialFuture;
}

When we compile the code, the library will inject AOP advice in place of the @Async annotation through AspectJ weaving, for the asynchronous execution of the factorialUsingAspect method.

So, let's compile the class using the Maven command:

mvn install

The output from the jcabi-maven-plugin may look like:

 --- 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)

We can verify if our class is woven correctly by checking the logs in the jcabi-ajc.log file, generated by the Maven plugin:

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))

Then, we'll run the class as a simple Java application, and the output will look like:

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

So, we can see a new daemon thread jcabi-async is created by the library that performed the task asynchronously.

Similarly, the logging is enabled by the @Loggable annotation provided by the library.

7. 总结

In this article, we've seen a few ways of asynchronous programming in Java.

To begin with, we explored Java's in-built features like FutureTask and CompletableFuture for asynchronous programming. Then, we've seen a few libraries like EA Async and Cactoos with out-of-the-box solutions.

Also, we examined the support of performing tasks asynchronously using Guava's ListenableFuture and Futures classes. Last, we explored the jcabi-AspectJ library that provides AOP features through its @Async annotation for asynchronous method calls.

As usual, all the code implementations are available over on GitHub.