Асинхронно програмиране в Java

1. Общ преглед

С нарастващото търсене на писане на неблокиращ код, имаме нужда от начини за изпълнение на кода асинхронно.

В този урок ще разгледаме няколко начина за постигане на асинхронно програмиране в Java. Също така ще проучим няколко библиотеки на Java, които предлагат готови решения.

2. Асинхронно програмиране в Java

2.1. Тема

Можем да създадем нова нишка за извършване на всяка операция асинхронно. С пускането на ламбда изрази в Java 8 той е по-чист и по-четлив.

Нека създадем нова нишка, която изчислява и отпечатва факториал на число:

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, за да изпълним задачата асинхронно и да върнем екземпляра на FutureTask .

И така, нека намерим факториал на число:

ExecutorService threadpool = Executors.newCachedThreadPool(); Future 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. Изпълним Бъдеще

Java 8 представи CompletableFuture с комбинация от Future и CompletionStage . Той осигурява различни методи като supplyAsync , runAsync и thenApplyAsync за асинхронно програмиране.

И така, нека използваме CompletableFuture вместо FutureTask, за да намерим факториал на число:

CompletableFuture 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 предоставя класа ListenableFuture за извършване на асинхронни операции.

Първо ще добавим най-новата зависимост от гуава Maven:

 com.google.guava guava 28.2-jre 

След това нека намерим факториал на число, използвайки ListenableFuture :

ExecutorService threadpool = Executors.newCachedThreadPool(); ListeningExecutorService service = MoreExecutors.listeningDecorator(threadpool); ListenableFuture guavaFuture = (ListenableFuture) 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 asyncCallable = Callables.asAsyncCallable(new Callable() { public Long call() { return factorial(number); } }, service); ListenableFuture guavaFuture = Futures.submitAsync(asyncCallable, service);

Тук, submitAsync метод изисква аргумент на AsyncCallable , който е създаден с помощта на Callables клас.

Освен това класът Futures осигурява метода addCallback за регистриране на обратно извикване на успех и неуспех:

Futures.addCallback( factorialFuture, new FutureCallback() { public void onSuccess(Long factorial) { System.out.println(factorial); } public void onFailure(Throwable thrown) { thrown.getCause(); } }, service);

4. EA Async

Electronic Arts донесе функцията async-await от .NET в екосистемата на Java чрез библиотеката ea-async .

Библиотеката позволява последователно писане на асинхронен (неблокиращ) код. Поради това прави асинхронното програмиране по-лесно и се мащабира естествено.

Първо ще добавим най-новата зависимост на ea-async Maven към pom.xml :

 com.ea.async ea-async 1.2.3 

След това нека трансформираме обсъдения по-рано код CompletableFuture, като използваме метода await , предоставен от Async класа на EA :

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

Тук се обаждаме на метода Async.init в статичния блок, за да инициализираме инструментите за изпълнение на Async .

Асинхронната апаратура преобразува кода по време на изпълнение и пренаписва повикването към метода await , за да се държи подобно на използването на веригата на CompletableFuture .

Поради това, поканата към очакват Методът е подобен на повикване Future.join.

Можем да използваме параметъра - javaagent JVM за измерване на времето за компилация. Това е алтернатива на метода Async.init :

java -javaagent:ea-async-1.2.3.jar -cp  

Нека разгледаме друг пример за последователно писане на асинхронен код.

Първо, ще извършим няколко верижни операции асинхронно, като използваме методите за композиране като thenComposeAsync и thenAcceptAsync от класа CompletableFuture :

CompletableFuture 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 is a Java library based on object-oriented principles.

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:

 org.cactoos cactoos 0.43 

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 asyncFunction = new Async(input -> factorial(input)); Future 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 provides the @Async annotation for asynchronous programming through AspectJ AOP aspects.

First, let's add the latest jcabi-aspects Maven dependency:

 com.jcabi jcabi-aspects 0.22.6  

The jcabi-aspects library requires AspectJ runtime support. So, we'll add the aspectjrt Maven dependency:

 org.aspectj aspectjrt 1.9.5  

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:

 com.jcabi jcabi-maven-plugin 0.14.1    ajc      org.aspectj aspectjtools 1.9.1   org.aspectj aspectjweaver 1.9.1   

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

@Async @Loggable public Future factorialUsingAspect(int number) { Future 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. Conclusion

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.

Както обикновено, всички реализации на кода са достъпни в GitHub.