Асинхронное программирование в Java: полное руководство на практике

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение в асинхронное программирование на 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

Блоги

Рекомендаций по книгам