← Назад к вопросам

Почему для решения 10000 задач нужен ExecutorService?

2.0 Middle🔥 151 комментариев
#Многопоточность

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

ExecutorService для решения тысяч задач

Краткий ответ

ExecutorService необходим для решения 10000 задач, потому что создание 10000 отдельных потоков приводит к истощению памяти и деградации производительности. ExecutorService переиспользует ограниченное количество потоков через пул, экономя ресурсы и повышая эффективность.

Проблема: создание потока на задачу

Наивный подход (НЕПРАВИЛЬНО)

// Попытка решить 10000 задач
for (int i = 0; i < 10000; i++) {
    Thread thread = new Thread(() -> {
        // Решаем задачу
        solveTask();
    });
    thread.start();  // Создаём новый поток!
}

// Проблемы:
// 1. Создание потока дорого (100+ микросекунд)
// 2. Каждый поток займёт 1MB памяти на stack
// 3. 10000 потоков = 10GB RAM (на x64)
// 4. OS не может эффективно управлять 10000 потоками
// 5. Context switching overhead будет огромный

Стек и память для потока

Операционная система (ОС):

ОS может запустить примерно:
- на 32-bit: до ~2000 потоков (2GB / 1MB per thread)
- на 64-bit: до ~10000+ потоков (зависит от RAM)

Если попробуешь создать больше:
Exception in thread "main" java.lang.OutOfMemoryError: 
  unable to create new native thread

ExecutorService: пул потоков

Правильный подход с ExecutorService

// Создаём пул из 10 потоков (например)
ExecutorService executor = Executors.newFixedThreadPool(10);

// Отправляем 10000 задач
for (int i = 0; i < 10000; i++) {
    executor.submit(() -> {
        // Решаем задачу
        solveTask();
    });
}

// Результат:
// - Всего потоков: 10 (не 10000!)
// - Каждый поток решает задачи последовательно
// - Очередь (queue) хранит оставшиеся 9990 задач
// - Память: 10MB (10 потоков * 1MB) вместо 10GB

Как работает ExecutorService

модель выполнения:

Очередь задач:
[Task 1][Task 2][Task 3]...[Task 10000]

Пул потоков (10 потоков):
┌──────────┬──────────┬──────────┐
│ Thread 1 │ Thread 2 │ Thread 3 │ ...
│ решает   │ решает   │ решает   │
│ Task 1   │ Task 2   │ Task 3   │
└──────────┴──────────┴──────────┘

Когда Thread 1 завершит Task 1:
- он берет Task 11 из очереди
- продолжает работу

Таким образом все 10000 задач решаются 10 потоками

Практический пример

import java.util.concurrent.*;

public class TaskSolver {
    public static void main(String[] args) throws InterruptedException {
        // Количество потоков в пуле
        int numThreads = 10;
        ExecutorService executor = Executors.newFixedThreadPool(numThreads);
        
        // Количество задач
        int numTasks = 10000;
        
        // Отправляем все задачи
        for (int i = 0; i < numTasks; i++) {
            final int taskId = i;
            executor.submit(() -> {
                try {
                    // Имитируем долгую задачу (1 секунда)
                    Thread.sleep(1000);
                    System.out.println("Task " + taskId + " completed");
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }
        
        // Ждём, пока все задачи завершатся
        executor.shutdown();
        boolean finished = executor.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
        
        if (finished) {
            System.out.println("All tasks completed");
        }
    }
}

// Время выполнения:
// С 1 потоком: 10000 * 1 сек = 10000 сек (2.7 часов)
// С 10 потоками: ~1000 * 1 сек = 1000 сек (16.6 минут)

Типы ExecutorService

1. Fixed Thread Pool

// Фиксированное количество потоков
ExecutorService executor = Executors.newFixedThreadPool(10);
// Хорошо для CPU-intensive задач
// Обычно используют количество потоков = количество CPU ядер

int numCores = Runtime.getRuntime().availableProcessors();
ExecutorService executor = Executors.newFixedThreadPool(numCores);

2. Cached Thread Pool

// Динамическое количество потоков
ExecutorService executor = Executors.newCachedThreadPool();
// Создаёт новый поток когда нужен
// Переиспользует неактивные потоки (60 сек timeout)
// Хорошо для I/O-bound задач с непредсказуемой нагрузкой

// Используется когда:
// - Задачи приходят случайно
// - Длительность задач варьируется

3. Single Thread Executor

// Один поток для всех задач
ExecutorService executor = Executors.newSingleThreadExecutor();
// Гарантирует последовательное выполнение
// Хорошо для очереди обработки в порядке FIFO

// Используется для:
// - Database migration
// - Sequential logging
// - Ordered event processing

4. Scheduled Thread Pool

// Для задач по расписанию
ScheduledExecutorService executor = 
    Executors.newScheduledThreadPool(5);

// Выполнить один раз с задержкой
executor.schedule(() -> {
    System.out.println("Delayed task");
}, 5, TimeUnit.SECONDS);

// Выполнять периодически
executor.scheduleAtFixedRate(() -> {
    System.out.println("Recurring task");
}, 0, 1, TimeUnit.SECONDS);

Advanced: Custom ThreadPoolExecutor

public class TaskProcessor {
    public static void main(String[] args) {
        // Создаём кастомный пул
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
            10,           // corePoolSize (всегда активные потоки)
            50,           // maximumPoolSize (макс потоки при пиковой нагрузке)
            60,           // keepAliveTime
            TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(5000),  // очередь задач
            new ThreadFactory() {
                private int count = 0;
                @Override
                public Thread newThread(Runnable r) {
                    Thread t = new Thread(r);
                    t.setName("TaskWorker-" + (++count));
                    t.setDaemon(false);
                    return t;
                }
            },
            new ThreadPoolExecutor.CallerRunsPolicy()  // overflow policy
        );
        
        // Отправляем 10000 задач
        for (int i = 0; i < 10000; i++) {
            final int taskId = i;
            try {
                executor.submit(() -> {
                    System.out.println("Task " + taskId + " running on " + 
                        Thread.currentThread().getName());
                });
            } catch (RejectedExecutionException e) {
                System.out.println("Queue is full, task rejected");
            }
        }
        
        executor.shutdown();
    }
}

Future для отслеживания результатов

ExecutorService executor = Executors.newFixedThreadPool(10);

List<Future<Integer>> futures = new ArrayList<>();

// Отправляем задачи и сохраняем Future
for (int i = 0; i < 10000; i++) {
    final int taskId = i;
    Future<Integer> future = executor.submit(() -> {
        // Решаем задачу, возвращаем результат
        return solveTask(taskId);
    });
    futures.add(future);
}

// Собираем результаты (ждем завершения)
int totalResult = 0;
for (Future<Integer> future : futures) {
    try {
        totalResult += future.get();  // блокирует, пока задача не завершится
    } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
    }
}

System.out.println("Total result: " + totalResult);

CountDownLatch для синхронизации

public class ParallelTaskProcessor {
    public static void main(String[] args) throws InterruptedException {
        int numTasks = 10000;
        CountDownLatch latch = new CountDownLatch(numTasks);
        ExecutorService executor = Executors.newFixedThreadPool(10);
        
        for (int i = 0; i < numTasks; i++) {
            final int taskId = i;
            executor.submit(() -> {
                try {
                    solveTask(taskId);
                } finally {
                    latch.countDown();  // уменьшаем счетчик
                }
            });
        }
        
        // Ждем, пока все задачи завершатся
        latch.await();
        System.out.println("All tasks completed!");
        executor.shutdown();
    }
}

Сравнение: с/без ExecutorService

ХарактеристикаБез ExecutorServiceС ExecutorService
Память10GB (10000 * 1MB)10MB (10 * 1MB)
Создание потока10000 раз10 раз
Overhead10000 контекстных переключений10 контекстных переключений
CPU использованиеНеэффективноОптимально
ScalabilityДо ~2000 потоковДо 100000+ задач

Рекомендации

  1. Для CPU-bound задач: newFixedThreadPool(numCores)
  2. Для I/O-bound задач: newCachedThreadPool()
  3. Для долгих операций: newFixedThreadPool(меньше потоков)
  4. Для расписания: newScheduledThreadPool(n)
  5. Для очереди: newSingleThreadExecutor()

Вывод

ExecutorService необходим для 10000 задач, потому что:

  1. Производительность: пулирует потоки вместо создания новых
  2. Память: экономит RAM (MB вместо GB)
  3. Масштабируемость: может обработать миллионы задач
  4. Управление: простой контроль над параллелизмом
  5. Надежность: обработка overflow и graceful shutdown

Это стандартная практика для любой системы, которая обрабатывает большое количество параллельных задач.

Почему для решения 10000 задач нужен ExecutorService? | PrepBro