Введение в асинхронное программирование на Java
Современные приложения требуют высокой производительности и отзывчивости. Пользователи не готовы ждать, пока приложение обработает запрос к базе данных или выполнит сетевой вызов. Здесь на помощь приходит асинхронное программирование в Java — подход, позволяющий не блокировать основной поток выполнения при ожидании завершения длительных операций.
В отличие от традиционной многопоточности, где вы явно управляете потоками, асинхронное программирование предлагает более высокоуровневые абстракции. Вместо того чтобы создавать новые потоки вручную, вы используете CompletableFuture, ExecutorService и другие инструменты, предоставляемые Java Concurrency API.
В этой статье мы разберём ключевые концепции асинхронного программирования на Java с практическими примерами. Вы узнаете, как обрабатывать асинхронные задачи, объединять их в цепочки и эффективно работать с многопоточностью.
Основные инструменты асинхронного программирования в Java
1. CompletableFuture — основа асинхронности
CompletableFuture — это класс, появившийся в Java 8, который предоставляет мощный API для асинхронного программирования. Он позволяет создавать асинхронные задачи, объединять их, обрабатывать ошибки и преобразовывать результаты.
Рассмотрим простой пример создания и выполнения асинхронной задачи:
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
public class AsyncExample {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// Создаём асинхронную задачу
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(1000); // Имитация длительной операции
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return "Результат асинхронной операции";
});
// Добавляем обработчик завершения
future.thenAccept(result -> System.out.println("Получен результат: " + result));
// Основной поток продолжает работу
System.out.println("Основной поток продолжает выполнение");
// Ждём завершения (только для демонстрации)
future.get();
}
}
Обратите внимание, что supplyAsync() по умолчанию использует ForkJoinPool.commonPool(). Это пул потоков, оптимизированный для параллельных задач.
2. ExecutorService — управление пулом потоков
Для более тонкого контроля над выполнением асинхронных задач используйте ExecutorService. Это позволяет задать количество потоков, их поведение и стратегию обработки задач.
import java.util.concurrent.*;
public class ExecutorServiceExample {
public static void main(String[] args) {
// Создаём пул из 4 потоков
ExecutorService executor = Executors.newFixedThreadPool(4);
// Создаём CompletableFuture с кастомным executor'ом
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return 42;
}, executor);
// Обработка результата
future.thenAccept(result -> {
System.out.println("Результат: " + result);
executor.shutdown(); // Важно! Закрываем пул
});
System.out.println("Основной поток не блокируется");
}
}
Важно: Всегда закрывайте ExecutorService с помощью shutdown() или shutdownNow(), чтобы избежать утечки ресурсов.
3. Объединение асинхронных задач
Одна из сильных сторон CompletableFuture — возможность объединять несколько асинхронных операций в цепочки и комбинировать их результаты.
import java.util.concurrent.CompletableFuture;
public class CombineFuturesExample {
public static void main(String[] args) {
// Первая асинхронная задача
CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> 10);
// Вторая асинхронная задача
CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> 20);
// Комбинируем результаты
CompletableFuture<Integer> combined = future1
.thenCombine(future2, (a, b) -> a + b);
// Выполняем действие после завершения обеих задач
combined.thenAccept(sum -> System.out.println("Сумма: " + sum));
// Последовательное выполнение
future1.thenApply(result -> result * 2)
.thenAccept(System.out::println);
}
}
Практические примеры асинхронного программирования
Асинхронные HTTP-запросы
Рассмотрим реальный пример — выполнение нескольких HTTP-запросов асинхронно. Для этого будем использовать HttpClient, доступный начиная с Java 11.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
public class AsyncHttpExample {
public static void main(String[] args) {
HttpClient client = HttpClient.newHttpClient();
List<String> urls = List.of(
"https://api.github.com",
"https://httpbin.org/get",
"https://jsonplaceholder.typicode.com/posts/1"
);
// Создаём список асинхронных запросов
List<CompletableFuture<String>> futures = urls.stream()
.map(url -> {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.build();
return client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body);
})
.collect(Collectors.toList());
// Ждём завершения всех запросов
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.thenAccept(v -> {
futures.forEach(future -> {
try {
System.out.println(future.get().substring(0, 100) + "...");
} catch (Exception e) {
e.printStackTrace();
}
});
})
.join(); // Блокируем основной поток для демонстрации
}
}
В этом примере все три запроса выполняются параллельно, а затем мы собираем их результаты. allOf() возвращает новый CompletableFuture, который завершается после завершения всех переданных задач.
Обработка ошибок в асинхронном коде
Асинхронные операции могут завершаться ошибками. CompletableFuture