Почему 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 — что спрашивают на самом деле
В этом посте были ссылки, но мы их удалили по правилам Сетки