Как ускорить параллельное выполнение метода с большим числом вызовов в Java?
Что проверяют
Понимаете ли вы, что ускорение зависит от природы работы: считает метод на процессоре или ждёт сеть и базу. Следом спрашивают, какой размер пула вы выберете и почему параллельный стрим не всегда быстрее обычного цикла.
Как отвечать
Сначала выясните, что делает метод, и замерьте: JMH для самого метода, JFR или async-profiler под реальной нагрузкой. Дальше по ситуации:
- вычисления на CPU: потоков примерно столько, сколько ядер (
Runtime.getRuntime().availableProcessors()). Больше не даст выигрыша, только лишние переключения контекста. ПодходятForkJoinPool,parallelStream()иExecutors.newFixedThreadPool; - ожидание ввода-вывода: с Java 21 это виртуальные потоки,
Executors.newVirtualThreadPerTaskExecutor(). Потоков становится сколько угодно, поэтому ограничение ставят на ресурс:Semaphoreна число одновременных запросов к базе или внешнему API; - мелкие вызовы: если один вызов длится микросекунды, накладные расходы на постановку задачи съедают выигрыш. Вызовы группируют пачками, чтобы одна задача работала хотя бы миллисекунды, а внешний API по возможности вызывают пакетным методом;
- конкуренция за общее состояние:
synchronizedна горячем пути превращает параллельность в очередь. ПомогаютConcurrentHashMap,LongAdderвместоAtomicLongдля счётчиков под высокой нагрузкой и локальные результаты с объединением в конце; - повторяющиеся аргументы: если метод часто зовут с одними и теми же входами, мемоизация через
computeIfAbsentуберёт часть работы совсем.
Потолок ускорения задаёт закон Амдала: последовательная часть кода ограничивает выигрыш, сколько ядер ни добавляй.
Где спотыкаются
Ставят пул на сотни потоков для вычислительной задачи. Это медленнее, чем пул по числу ядер.
Запускают блокирующие вызовы в parallelStream() или в CompletableFuture.supplyAsync() без своего исполнителя. Оба используют общий ForkJoinPool, и блокировка в нём тормозит всех, кто на него опирается.
Меряют время через System.currentTimeMillis() одним запуском без прогрева. JIT ещё не скомпилировал горячий код, и цифры ничего не значат.