Наверняка эта задачка входит в число хрестоматийных и встречается на собеседованиях по #Java, но поскольку я по ним не хожу, пришлось убить часок на разбирательство с ней. Поделюсь с вами; вдруг кто-то тоже встретит.
🎓 ДаноВанильный пул потоков на голом JDK (аналоги из класса
Executors не подходят):
var threadPool = new ThreadPoolExecutor(
corePoolSize, // 3
maxPoolSize, // 30
keepAliveTime, // 60
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(maxQueueLength), // 100
Executors.defaultThreadFactory(),
new DiscardPolicy()); // could also be a logging implementation
И некий метод, который сначала просто напихивает задачки в этот пул:
List<Future> futures = new ArrayList<>();
futures.add(threadPool.submit(task));
, потом идёт по своим делам, а когда заканчивает с ними, проверяет готовность задачек и, если надо, дожидается завершения каждой, чтобы продолжить работу дальше:
for (Future future : futures) {
try {
future.get();
}
catch (Exception e) {
log.error("Failed to get task result from future", e);
}
}🔍 НайтиКакого _🙊_ в некоторых случаях метод
намертво зависает на вызове
future.get()?
При этом в дампе потоков нет никаких следов задач, которые бы зависли/зациклились/заблокированы.
🔑 РешениеВариант 1. Добавить таймаут в метод
future.get(). Да, это решит проблему, но не даст понять, почему она появилась.
Вариант 2. Извернуться как-нибудь так, чтобы прийти к вызову вида
CompletableFuture.allOf(c1, c2, c3).join(). Наверняка так можно (не проверял), но выглядит избыточно сложно, должно же работать и так.
Вариант 3. Обернуть вызов
future.get() вот в такое условие:
if (future.isDone()) {
future.get();
}Казалось бы, смысла в нём нет, ведь если задача не завершена (
isDone() == false), то мы просто дождёмся её завершения при вызове
get(). Но нет. Когда мы имеем дело с
ThreadPoolExecutor, то при вызове
submit() он в качестве имплементации
Future возвращает экземпляр
FutureTask, у которого метод
isDone() выглядит так:
public boolean isDone() {
return state != NEW;
}, то есть "сделанной" считается любая
не новая задача (в том числе когда она ещё в работе).
А метод
FutureTask.get() устроен так:
public V get() throws InterruptedException, ExecutionException {
int s = state;
if (s <= COMPLETING)
s = awaitDone(false, 0L);
return report(s);
}, то есть ожидание наступит для любого состояния, предшествующего завершению, в том числе для вышеупомянутого
NEW (на самом деле только для
NEW и
COMPLETING).
Но если бы задача, на которой завис наш поток, была в состоянии
COMPLETING, мы наверняка увидели бы соответствующий ей поток в дампе, а раз его нет, значит, она в
NEW.
А как она могла оказаться
NEW, если все задачи вроде как были переданы пулу на исполнение? Видимо, он её не взял. А почему не взял? Правильно — из-за переполнения очереди, ведь она ограничена (
new LinkedBlockingQueue<>(maxQueueLength)). А почему тогда это не привело к ошибкам и прерыванию всего процесса? Верно, из-за с виду безобидной
new DiscardPolicy(), которая просто дропает не влезшие задачки (и логирует их, как было в нашем случае).
✅ Вся цепочка кратко:
— не влезающие в очередь задачи молча отбрасываются политикой пула, оставляя им статус
NEW;
— видя этот статус, метод
future.get() впадает в бесконечное ожидание;
— исключить ожидание можно добавлением проверки на
NEW в виде вызова
future.isDone().
⚠️ Если очередь будет большой, а задачи — долгоиграющими, можно нарваться на случай, когда задача уже в очереди и имеет шанс быть выполненной, но цикл с вызовом
get() её не подождёт. В этом случае решение должно быть другим.
Мораль: пишите однопоточные приложения🤪