Comments 12
С появлением виртуальных потоков я бы добавил, что для IO-bound задач в принципе больше нет необходимости в пуле потоков, будет достаточно Executors.newVitrualThreadPerTaskExecutor()
Еще бы увидеть код с этой ситуацией.
// Плохая конфигурация (причина deadlock)
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(1); // Всего ОДИН поток!
executor.initialize();
// Главный поток
CompletableFuture<?> task1 = CompletableFuture.supplyAsync(() -> {
// Внутри задачи 1...
CompletableFuture<?> subTask1 = CompletableFuture.supplyAsync(..., executor);
CompletableFuture<?> subTask2 = CompletableFuture.supplyAsync(..., executor);
// БЛОКИРОВКА! Используем allOf().join() для ожидания всех подзадач
return CompletableFuture.allOf(subTask1, subTask2).join();
}, executor);
CompletableFuture<?> task2 = CompletableFuture.supplyAsync(..., executor);
CompletableFuture.allOf(task1, task2).join(); // Вечное ожидание...Странно, что решением выбран костыль с докинуть памя потоков, вместо отказа от блокировок.
Можно использовать эластичный пулpublic ExecutorService myExecutorService() {
BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1000); ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1000, 1000, 60L, TimeUnit.SECONDS, queue, new ThreadPoolExecutor.AbortPolicy()); threadPoolExecutor.allowCoreThreadTimeOut(true);
return threadPoolExecutor;
}
«эластичный» пул с большим числом потоков и очередью не устраняет корневую причину — блокировку рабочего потока из‑за join внутри того же пула, где должны стартовать дочерние задачи, поэтому риск starvation/дедлока остаётся и лишь «маскируется» масштабированием. Решение — не блокировать воркеры: компоновать этапы через thenCompose/thenCombine/allOf и делать единственный join на границе, либо выносить дочерние задачи в отдельный исполнитель или виртуальные потоки для изоляции от блокировок.
Кейс из production: Deadlock в асинхронном коде на Java