Конкурентность и многопоточность в Java: продвинутые техники

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

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

Начать курс

Конкурентность и многопоточность в Java: продвинутый уровень

Многопоточность в Java — одна из самых мощных и одновременно сложных тем. На начальном уровне мы используем synchronized и Thread, но для построения производительных и масштабируемых систем требуется более глубокое понимание. В этой статье мы разберем продвинутые механизмы конкурентности: пулы потоков, ForkJoinPool, асинхронное программирование с CompletableFuture, тонкую настройку блокировок и работу с памятью. Материал ориентирован на разработчиков, уже знакомых с основами многопоточности.

1. Продвинутая работа с пулами потоков: ExecutorService и настройка

Использование ExecutorService — стандарт промышленной разработки. Однако стандартный Executors.newFixedThreadPool(10) подходит не всегда. Рассмотрим тонкую настройку ThreadPoolExecutor.

import java.util.concurrent.*;

public class CustomThreadPool {
    public static void main(String[] args) {
        int corePoolSize = 4;
        int maxPoolSize = 8;
        long keepAliveTime = 30;
        TimeUnit unit = TimeUnit.SECONDS;
        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(100);

        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                corePoolSize,
                maxPoolSize,
                keepAliveTime,
                unit,
                workQueue,
                new ThreadPoolExecutor.CallerRunsPolicy() // политика насыщения
        );

        // Мониторинг состояния пула
        executor.submit(() -> {
            System.out.println("Активные потоки: " + executor.getActiveCount());
            System.out.println("Завершенные задачи: " + executor.getCompletedTaskCount());
        });

        executor.shutdown();
    }
}

Ключевые параметры:

  • corePoolSize — минимальное количество потоков, которые держатся даже без задач.
  • maxPoolSize — максимальное количество потоков.
  • keepAliveTime — время жизни избыточных потоков.
  • workQueue — очередь задач. ArrayBlockingQueue с ограниченным размером предотвращает переполнение памяти.
  • Политика насыщения — что делать, если очередь заполнена и все потоки заняты. CallerRunsPolicy замедляет вызывающий поток.

Для длительных I/O задач лучше увеличивать corePoolSize, для CPU-bound задач — ориентироваться на Runtime.getRuntime().availableProcessors().

2. ForkJoinPool и рекурсивные задачи

ForkJoinPool — это специализированный пул для задач, которые можно разделить на подзадачи (разделяй и властвуй). Он эффективно использует работу-воровство (work stealing).

Пример: вычисление суммы элементов массива рекурсивно.

import java.util.concurrent.RecursiveTask;
import java.util.concurrent.ForkJoinPool;

public class SumTask extends RecursiveTask<Long> {
    private static final int THRESHOLD = 1000;
    private final int[] array;
    private final int start;
    private final int end;

    public SumTask(int[] array, int start, int end) {
        this.array = array;
        this.start = start;
        this.end = end;
    }

    @Override
    protected Long compute() {
        if (end - start <= THRESHOLD) {
            long sum = 0;
            for (int i = start; i < end; i++) {
                sum += array[i];
            }
            return sum;
        }
        int mid = (start + end) / 2;
        SumTask leftTask = new SumTask(array, start, mid);
        SumTask rightTask = new SumTask(array, mid, end);

        leftTask.fork(); // асинхронный запуск
        long rightResult = rightTask.compute(); // вычисляем правую часть
        long leftResult = leftTask.join(); // ждем левую

        return leftResult + rightResult;
    }

    public static void main(String[] args) {
        int[] data = new int[10_000_000];
        for (int i = 0; i < data.length; i++) {
            data[i] = i;
        }

        ForkJoinPool pool = new ForkJoinPool();
        long result = pool.invoke(new SumTask(data, 0, data.length));
        System.out.println("Сумма: " + result);
    }
}

Важные моменты:

  • Пороговое значение (THRESHOLD) подбирается экспериментально для уменьшения накладных расходов.
  • fork() планирует задачу в общий пул, join() блокирует поток до получения результата.
  • Для задач, не возвращающих результат, используйте RecursiveAction.

ForkJoinPool идеален для обработки больших массивов, параллельной сортировки или обхода деревьев.

3. CompletableFuture: асинхронность и композиция

CompletableFuture позволяет строить цепочки асинхронных операций без блокировок. Это мощный инструмент для реактивного программирования.

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(() -> {
            sleep(1000);
            return "Привет";
        });

        // Композиция: thenApply преобразует результат
        CompletableFuture<String> greetingFuture = future
                .thenApply(greeting -> greeting + ", мир!")
                .thenApply(String::toUpperCase);

        // Обработка ошибок
        CompletableFuture<String> safeFuture = greetingFuture
                .exceptionally(ex -> "Ошибка: " + ex.getMessage());

        // Комбинирование двух независимых Future
        CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Java");
        CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "Конкурентность");

        CompletableFuture<String> combined = future1
                .thenCombine(future2, (a, b) -> a + " + " + b);

        System.out.println(combined.get()); // Java + Конкурентность
        System.out.println(safeFuture.get()); // ПРИВЕТ, МИР!
    }

    private static void sleep(long ms) {
        try { Thread.sleep(ms); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
    }
}

Популярные методы: