Очереди задач в Java: превращаем хаос в порядок

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

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

Начать курс

Когда ваш код начинает заикаться: введение в очереди и фоновые задачи на Java

Представьте ситуацию: вы запускаете веб-сервер на Java. Приходит запрос от пользователя — нужно отправить письмо, сгенерировать PDF-отчёт и обновить статистику. Если выполнять всё синхронно, пользователь будет ждать 30 секунд. Его кофе остынет, а он уйдёт к конкурентам. Знакомо?

В мире фоновых задач Java это решается элегантно: мы бросаем задачу в очередь и возвращаем пользователю «200 ОК». А потом, в фоне, наш воркер спокойно обрабатывает письма, PDF и статистику. Никакого хаоса, только порядок.

Сегодня мы разберём, как реализовать очереди задач в Java — от простейшего однопоточного варианта до промышленных решений с RabbitMQ и Kafka. Вы узнаете, как перестать тушить пожары и начать проектировать надёжные системы.

Зачем нужны очереди? Три причины, которые вас удивят

  1. Асинхронность — пользователь не ждёт. Скорость ответа сервера падает с 30 секунд до 5 миллисекунд.
  2. Балансировка нагрузки — пиковые запросы не кладут сервер. Задачи складируются в очередь и обрабатываются по мере освобождения ресурсов.
  3. Отказоустойчивость — если упал один воркер, задача остаётся в очереди. Другой воркер её подхватит. Данные не теряются.

В Java для этого есть встроенные инструменты (ExecutorService, BlockingQueue) и внешние брокеры (RabbitMQ, Kafka). Выбор зависит от масштаба. Начнём с малого.

Простая очередь в Java: ExecutorService и BlockingQueue

Если вам не нужна распределённая система, а просто хочется вынести тяжёлую работу в фон — используйте ThreadPoolExecutor. Это встроенный пул потоков, который берёт задачи из очереди.

Пример: пул потоков для обработки заказов

import java.util.concurrent.*;

public class OrderProcessor {
    private final ExecutorService executor;

    public OrderProcessor(int threadCount) {
        // Создаём пул с фиксированным числом потоков
        this.executor = Executors.newFixedThreadPool(threadCount);
    }

    public void processOrder(Order order) {
        executor.submit(() -> {
            // Тяжёлая работа: генерация чека, отправка email, обновление склада
            System.out.println("Обработка заказа: " + order.getId());
            try {
                Thread.sleep(2000); // симуляция работы
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            System.out.println("Заказ " + order.getId() + " обработан");
        });
    }

    public void shutdown() {
        executor.shutdown();
    }

    public static void main(String[] args) {
        OrderProcessor processor = new OrderProcessor(4);
        for (int i = 0; i < 10; i++) {
            processor.processOrder(new Order(i));
        }
        processor.shutdown();
    }
}

class Order {
    private final int id;
    public Order(int id) { this.id = id; }
    public int getId() { return id; }
}

Что тут происходит? Мы создали пул на 4 потока. 10 заказов будут обрабатываться по 4 штуки параллельно. Очередь задач в Java управляется внутри ThreadPoolExecutor — это LinkedBlockingQueue по умолчанию. Когда все потоки заняты, задачи ждут своей очереди.

Продвинутая настройка: приоритеты и отказоустойчивость

А что если задачи срочные? Используйте PriorityBlockingQueue:

BlockingQueue<Runnable> priorityQueue = new PriorityBlockingQueue<>(
    10,
    Comparator.comparingInt(task -> ((PriorityTask) task).getPriority())
);

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    2, 4, 60, TimeUnit.SECONDS, priorityQueue
);

Теперь задачи с высоким приоритетом будут обрабатываться раньше. Полезно для систем, где срочные транзакции важнее фоновой аналитики.

Когда одного JVM мало: распределённые очереди с RabbitMQ

Представьте, что у вас 10 микросервисов, разбросанных по разным серверам. Встроенная очередь Java не поможет — она живёт внутри одного процесса. Тут нужен брокер сообщений. RabbitMQ для Java — классика.

Архитектура: Producer → Queue → Consumer

  • Producer — сервис, который отправляет задачу (например, «отправить письмо»).
  • Queue — хранилище задач в RabbitMQ.
  • Consumer — воркер, который забирает задачу и выполняет.

Пример: отправка уведомлений через RabbitMQ

Добавим зависимость в pom.xml:

<dependency>
    <groupId>com.rabbitmq</groupId>
    <artifactId>amqp-client</artifactId>
    <version>5.20.0</version>
</dependency>

Код Producer'а:

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

public class NotificationProducer {
    private final static String QUEUE_NAME = "notification_queue";

    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {
            channel.queueDeclare(QUEUE_NAME, false, false, false, null);
            String message = "Привет! Ваш заказ подтверждён.";
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
            System.out.println("Отправлено: '" + message + "'");
        }
    }
}

Код Consumer'а (воркера):

import com.rabbitmq.client.*;

public class NotificationWorker {
    private final static String QUEUE_NAME = "notification_queue";

    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        System.out.println("Ожидание задач...");

        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println("Получено: '" + message + "'");
            // Эмуляция отправки письма
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            System.out.println("Письмо отправлено!");
        };
        channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> {});
    }
}

Запустите Consumer, потом Producer — и увидите, как задача уходит в очередь и обрабатывается. Можно запустить 5 воркеров — они будут параллельно разбирать задачи. Обработка фоновых задач Java становится масштабируемой.

Kafka: когда очередь становится рекой

Если у вас миллионы событий в день (логи, метрики, транзакции), RabbitMQ может не справиться. Тут приходит

Блоги

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