Почему ForkJoinPool.commonPool() внезапно виден в стектрейсе продовой ошибки

Команда пишет CompletableFuture.supplyAsync(() -> heavyCall()) без второго аргумента и не задумывается, где это выполнится. По умолчанию — не в отдельном пуле сервиса, а в ForkJoinPool.commonPool().

Этот пул общий на всю JVM. В него же уходят параллельные стримы — list.parallelStream() из совершенно другого модуля того же приложения. Если где-то в коде параллельный стрим держит воркер надолго (блокирующий вызов, I/O), CompletableFuture из другого места начинает ждать свободный воркер.

Размер commonPool считается по числу процессоров минус один. На проде это часто 3-7 потоков на всё приложение — на все асинхронные цепочки сразу.

CompletableFuture .supplyAsync(() -> callPaymentGateway()) .thenApply(this::parseResponse) .thenAccept(this::saveResult);

Если внутри supplyAsync блокирующий HTTP-вызов, а весь сервис так делает асинхронные цепочки — все они делят один и тот же маленький пул.

На собеседовании обычно спрашивают следом: как передать свой executor и почему это меняет поведение thenApply. Ответ — второй аргумент у supplyAsync и thenApplyAsync с явным ExecutorService, тогда цепочка не зависит от commonPool и от того, что делают параллельные стримы в другом месте кода.

Если в проекте есть хоть один parallelStream и хоть один CompletableFuture без явного executor — они уже делят пул, даже если код писали разные люди в разное время.

senior·base — что спрашивают на самом деле


В этом посте были ссылки, но мы их удалили по правилам Сетки