Получить доступ

Почему опасно выполнять блокирующие операции в стадиях CompletableFuture без явного Executor?

SeniorМногопоточность
Коротко

Async-стадии без Executor выполняются в ForkJoinPool.commonPool — он общий на всю JVM и размером примерно число ядер − 1. Блокирующие вызовы (HTTP, БД, get чужого future) занимают его потоки, и встаёт всё, что им пользуется: другие цепочки, parallel stream, библиотеки.

  • Для блокирующих шагов — свой пул: supplyAsync(task, ioExecutor)
  • Симптом: приложение «тормозит» в коде, который не менялся
  • Виртуальные потоки — хороший executor для блокирующих стадий

А ещё В контейнере с 1–2 CPU commonPool совсем крошечный

Так подсказку видит пользователь sotto на живом собеседовании — за пару секунд, пока вы думаете вслух.

Подробно

Почему commonPool не подходит для ввода-вывода. Он задуман для коротких вычислительных задач без блокировок; его параллелизм равен availableProcessors() - 1. На машине с 4 ядрами — 3 потока. Три сетевых вызова по секунде — и все async-стадии приложения ждут.

Особенность контейнеров. JVM видит лимит CPU контейнера: при 1 CPU commonPool может состоять из одного потока, а async-задачи вообще выполняться в новом потоке на каждую.

Правильно:

ExecutorService io = Executors.newVirtualThreadPerTaskExecutor();
supplyAsync(() -> http.get(url), io)
    .thenApplyAsync(this::parse, cpuPool);

Блокирующие стадии — в отдельный пул (ограниченный, именованный, или виртуальные потоки), вычислительные можно оставить в commonPool.

То же касается parallelStream() с сетевыми вызовами внутри.

Что хотят услышать

  • Какой пул используется по умолчанию и его размер.
  • Почему блокировки в нём вредят всему приложению.
  • Решение: свой executor.

Частые ошибки

  • Делать блокирующий I/O в commonPool.
  • Думать, что commonPool безграничен.

Проверьте себя

Один верный вариант. Подумайте, потом откройте ответ.

Показать варианты и ответ
  • ✓Без Executor стадии идут в ForkJoinPool.commonPool — он маленький (по числу ядер) и общий на весь процесс; блокировки истощают его. Для блокирующих шагов передают отдельный пул через *Async-варианты
  • ✗commonPool однопоточный, любая операция его блокирует
  • ✗CompletableFuture без Executor вообще не использует потоки
  • ✗Блокировки в commonPool бросают IllegalStateException

Что спросят дальше

  • Чем отличается thenApply от thenApplyAsync?
  • Как обрабатывать исключения в цепочке (exceptionally/handle)?

Разбор темы

Связанные вопросы

Закрытая бета

Подсказки на собеседовании — в реальном времени

sotto слышит вопрос интервьюера и показывает короткий ответ и план, как в блоке «Коротко». Оставьте контакт — пришлём доступ и промокод.

  • Промокод на 2 часа — сверх пробных 30 минут
  • Программа для Windows — скачать сразу, войти — через Яндекс ID или VK ID
  • Прямая связь с разработчиком: что улучшить — сделаем