Java. Менторинг за чашкой кода

@channel_java_cup_code
ExecutorService: как запускать задачи и не устроить DDoS самому себе В прошлом посте мы разобрали потокобезопасные коллекции. Но возникает следующий вопрос: кто вообще должен работать с этими коллекциями? Можно, конечно, сделать так: for (int i = 0; i < 1000; i++) { new Thread(() -> processTask()).start(); } Работать будет. Пока задач немного. А теперь представим, что таких задач не 1000, а 100 000. Создавать отдельный Thread на каждую задачу — плохая идея: потоки потребляют память, а большое количество потоков приводит к переключениям контекста и конкуренции за CPU. Что делает ExecutorService? Мы отделяем задачу от потока, который её выполнит. ExecutorService executor = Executors.newFixedThreadPool(10); for (int i = 0; i < 1000; i++) { int taskId = i; executor.submit(() -> processTask(taskId)); } Здесь мы создали всего 10 рабочих потоков, а задач отправили 1000. Остальные задачи будут ждать своей очереди. Это и есть главное преимущество пула: количество одновременно работающих потоков контролируется. А что делать с результатом? Если задача должна что-то вернуть — используем Callable: Future<String> future = executor.submit(() -> { return loadUserName(42); }); String name = future.get(); Future позволяет: - получить результат; - дождаться завершения; - проверить отмену; - отменить задачу. Но есть важный момент: future.get(); блокирует текущий поток, пока задача не завершится. Поэтому бездумно ставить get() везде тоже не стоит. А если задача упала? При использовании submit() исключение не падает прямо в вызывающий поток. Оно сохраняется внутри Future: Future<?> future = executor.submit(() -> { throw new RuntimeException("Что-то пошло не так"); }); try { future.get(); } catch (ExecutionException e) { System.err.println("Задача завершилась с ошибкой"); } Это одна из причин, почему ошибки в фоновых задачах иногда «исчезают» из логов. И самое важное — shutdown Пул нужно корректно закрывать: executor.shutdown(); try { if (!executor.awaitTermination(30, TimeUnit.SECONDS)) { executor.shutdownNow(); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); executor.shutdownNow(); } shutdown() — перестаём принимать новые задачи, но даём уже отправленным завершиться. shutdownNow() — пытаемся прервать выполняющиеся задачи через interrupt() и возвращаем задачи, которые ещё не начали выполняться. Но interrupt() — не убийство потока. Если ваш код игнорирует прерывание, shutdownNow() не гарантирует мгновенную остановку. Какой размер пула выбрать? Для CPU-bound задач: количество потоков ≈ количество CPU-ядер Для IO-bound задач поток большую часть времени ждёт сеть, БД или диск, поэтому потоков обычно может быть больше. но формула вида cores * 2 — это не закон. Размер пула зависит от: - времени CPU; - времени ожидания; - нагрузки; - количества одновременных задач; - внешних ограничений БД/API. И вот здесь начинается самое интересное. Пул потоков — это не просто способ не писать new Thread() - Это механизм управления конкуренцией. Если поставить слишком маленький пул — задачи будут стоять в очереди. Если слишком большой — можно положить CPU, БД или внешний API. Поэтому в production важно контролировать не только количество потоков, но и очередь задач, время выполнения и политику отказа. А Executors.newFixedThreadPool() — хороший способ познакомиться с ExecutorService, но в production часто нужен более явный контроль через ThreadPoolExecutor.
Если у вас установлено приложение,
вы можете сразу перейти в канал