Перейти к основному содержимому

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

· 7 мин. чтения

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 .