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
.
Мы можем использовать метод submit
службы ExecutorService
для асинхронного выполнения задачи и возврата экземпляра 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 предоставляет класс 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.
Например, давайте посмотрим, как использовать 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. Асинхронный советник
Electronic Arts перенесла функцию async-await из .NET в экосистему Java через библиотеку ea-async
.
Эта библиотека позволяет писать асинхронный (неблокирующий) код последовательно. Следовательно, это упрощает асинхронное программирование и обеспечивает естественное масштабирование.
Во- первых, мы добавим последнюю зависимость ea-async
Maven в pom.xml
:
<dependency>
<groupId>com.ea.async</groupId>
<artifactId>ea-async</artifactId>
<version>1.2.3</version>
</dependency>
Затем мы преобразуем ранее обсуждавшийся код CompletableFuture
, используя метод await
, предоставляемый классом Async
EA :
static {
Async.init();
}
public long factorialUsingEAAsync(int number) {
CompletableFuture<Long> completableFuture = CompletableFuture.supplyAsync(() -> factorial(number));
long result = Async.await(completableFuture);
}
Здесь мы вызываем метод Async.init
в статическом
блоке, чтобы инициализировать инструментарий среды выполнения Async .
Асинхронное
инструментирование преобразует код во время выполнения и переписывает вызов метода await
, чтобы он вел себя аналогично использованию цепочки CompletableFuture
.
Поэтому вызов метода await
аналогичен вызову Future.join.
Мы можем использовать параметр – javaagent
JVM для инструментовки времени компиляции. Это альтернатива методу Async.init
:
java -javaagent:ea-async-1.2.3.jar -cp <claspath> <MainClass>
Теперь давайте рассмотрим еще один пример последовательного написания асинхронного кода.
Во-первых, мы выполним несколько цепочек операций асинхронно, используя методы композиции, такие как thenComposeAsync
и thenAcceptAsync
класса CompletableFuture
:
CompletableFuture<Void> completableFuture = hello()
.thenComposeAsync(hello -> mergeWorld(hello))
.thenAcceptAsync(helloWorld -> print(helloWorld))
.exceptionally(throwable -> {
System.out.println(throwable.getCause());
return null;
});
completableFuture.get();
Затем мы можем преобразовать код, используя Async.await()
EA :
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 — это библиотека 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-Аспекты
Jcabi-Aspects предоставляет аннотацию @Async
для асинхронного программирования через аспекты АОП AspectJ .
Во-первых, давайте добавим последнюю зависимость jcabi-aspects
Maven:
<dependency>
<groupId>com.jcabi</groupId>
<artifactId>jcabi-aspects</artifactId>
<version>0.22.6</version>
</dependency>
Библиотеке jcabi-aspects
требуется поддержка времени выполнения AspectJ, поэтому мы добавим зависимость Maven аспекта jrt
:
<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>
Теперь мы готовы использовать аспекты АОП для асинхронного программирования:
@Async
@Loggable
public Future<Long> factorialUsingAspect(int number) {
Future<Long> factorialFuture = CompletableFuture.completedFuture(factorial(number));
return factorialFuture;
}
Когда мы скомпилируем код, библиотека вставит рекомендацию АОП вместо аннотации @Async
через плетение AspectJ для асинхронного выполнения метода factorialUsingAspect
.
Скомпилируем класс с помощью команды 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)
Мы можем проверить правильность плетения нашего класса, проверив журналы в файле jcabi-ajc.log
, сгенерированном плагином Maven:
Join point 'method-execution(java.util.concurrent.Future
com.foreach.async.JavaAsync.factorialUsingJcabiAspect(int))'
in Type 'com.foreach.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.foreach.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, с готовыми решениями.
Мы также обсудили поддержку асинхронного выполнения задач с использованием классов ListenableFuture
и Futures в Guava.
Наконец, мы коснулись библиотеки jcabi-AspectJ, которая предоставляет функции АОП с помощью аннотации @Async
для асинхронных вызовов методов.
Как обычно, все реализации кода доступны на GitHub .