Как запустить параллельный стрим в собственном ForkJoinPool вместо общего в Java
Что проверяют
Знаете ли вы, что параллельный стрим по умолчанию работает в ForkJoinPool.commonPool(), и понимаете ли, почему трюк со своим пулом вообще срабатывает. Следом спрашивают, зачем это нужно и нет ли способа лучше.
Как отвечать
Сам приём: ForkJoinPool pool = new ForkJoinPool(8);, затем pool.submit(() -> list.parallelStream().map(this::process).toList()).get();.
Работает он потому, что стрим дробит работу через fork(), а fork(), вызванный из рабочего потока пула, кладёт подзадачу в очередь этого же пула. Терминальная операция стартует внутри задачи pool, значит, и всё дерево подзадач остаётся в нём. Скажите прямо, что это деталь реализации, а не документированный контракт.
Дальше мотивация. Общий пул один на всю JVM, его параллелизм по умолчанию равен числу ядер минус один и меняется свойством java.util.concurrent.ForkJoinPool.common.parallelism. Им же пользуется CompletableFuture.supplyAsync без явного executor. Блокирующий вызов в сеть или базу внутри параллельного стрима занимает потоки общего пула, и тормозить начинает весь сервис.
Сильный ответ заканчивается альтернативами. Для I/O параллельный стрим вообще неудачный инструмент: лучше ExecutorService или виртуальные потоки через Executors.newVirtualThreadPerTaskExecutor(). С Java 24 есть Gatherers.mapConcurrent(n, fn): ограничивает число одновременных задач, работает на виртуальных потоках и сохраняет порядок.
Где спотыкаются
Не закрывают пул. Каждый new ForkJoinPool держит свои потоки, и при создании пула на каждый запрос они копятся. Пул создают один раз или закрывают через try-with-resources: с Java 19 ExecutorService реализует AutoCloseable.
Ждут, что свой пул изменит и дробление задач. Размер листовых подзадач стрим считает от параллелизма общего пула, так что меняется только число исполнителей.
Путают invoke и submit. invoke принимает только ForkJoinTask, лямбду передают в submit, а дальше get() бросает проверяемое ExecutionException, join() непроверяемое.