.parallelStream()
em todos os lugares? Nem sempre é uma boa ideia.
Você já pensou:
“E se eu colocar
.parallelStream()
em todas as camadas do meu código? Vai ficar tudo mais rápido, né?”
Pois é. Eu pensei isso. Spoiler: não ficou. 😅
Recentemente otimizando o processamento e transformações de milhões de objetos em memória (haja CPU) eu me peguei com alguns níveis aninhados de parallelStreams. Quando eu olhei minhas métricas, a CPU estava derretendo. A minha hipótese é que as tasks competem pelo mesmo thread pool, o common fork join pool nesse caso.
Se você assim como eu ficou curioso pra entender o porquê disso, continue lendo. Neste artigo, vamos olhar os resultados de alguns benchmarks que mostram o que acontece quando você exagera no uso de .parallelStream()
.
Também mostro qual abordagem funciona melhor e por quê.
Um problema complexo: muitas camadas, muito paralelismo
Imagine um código com múltiplas camadas:
- 10 grupos externos (ex: regiões)
- 100 grupos médios (ex: armazéns)
- 100 itens finais (ex: produtos)
Cada item executa um cálculo pesado em CPU, e pode alocar memória no processo.
A primeira ideia foi paralelizar tudo:
outer.parallelStream().forEach(o ->
middle.parallelStream().forEach(m ->
inner.parallelStream().forEach(this::processar)
)
);
Parece ideal. Mas quando rodei benchmarks sérios...
🚨 O desempenho caiu. E a variabilidade aumentou.
⚙️ Como o experimento foi feito
Pra medir corretamente, eu usei o JMH (Java Microbenchmark Harness) — a ferramenta padrão da comunidade Java pra benchmarks de alta confiança.
Simulei uma hierarquia de dados:
-
outerSize
: regiões -
middleSize
: armazéns -
innerSize
: produtos
Cada combinação gera uma tarefa com carga de CPU + alocação de memória.
📊 O que estamos comparando
Implementei três variações do mesmo processamento:
Técnica | Descrição |
---|---|
nestedParallelStreams() |
Paraleliza todas as camadas (exagerado) |
flattenedParallelStream() |
Só a camada externa é paralela |
singleParallelStream() |
Cria uma lista plana e paraleliza uma vez |
🧪 Como simulamos a carga
Cada tarefa executa:
- Operações com
Math.sqrt()
(CPU) - Concatenação de strings
- Criação de listas intermediárias (pra adicionar uma pressãozinha na heap)
record ComplexObject(String name, int value, byte[] payload) {
ComplexObject(String name, int value) {
this(name, value, new byte[1024]); // Simula peso na memória
}
}
🕳️ Por que usamos Blackhole
?
Essa técnica foi nova pra mim. O JMH fornece o objeto Blackhole
pra evitar que o compilador JIT otimize fora o código de benchmark.
Sem o Blackhole
, o compilador pode notar que você não usa o resultado de uma função e simplesmente eliminar a execução dela — o que estraga o nosso experimento.
blackhole.consume(results); // Garante que os resultados sejam "usados"
▶️ Como rodar o benchmark
Você pode rodar com o próprio main()
incluído na classe:
./gradlew run
Ou rodar diretamente com:
./gradlew jmh
Os parâmetros (outerSize
, middleSize
, etc.) são controlados por @Param
e podem ser ajustados com argumentos de linha de comando ou diretamente no código.
5 horas de exeução depois...
🔍 O que os resultados mostraram?
✅ 1. Paralelismo único é mais eficiente
Configuração: (100, 50, 5)
→ 25.000 tarefas
Técnica | Tempo médio (ms) |
---|---|
singleParallelStream |
3.233 ms |
flattenedParallelStream |
3.546 ms |
nestedParallelStreams |
3.972 ms |
💡 A paralelização profunda não ajudou. Só causou mais overhead.
⚠️ 2. Em escala, o nested vira caos
Configuração: (500, 100, 10)
→ 500.000 tarefas
Técnica | Tempo médio (ms) | Desvio |
---|---|---|
nestedParallelStreams |
69.486 ms | ±106.638 ms 😱 |
flattenedParallelStream |
78.037 ms | ±44.430 ms |
singleParallelStream |
75.201 ms | ±63.081 ms |
💣 O nested parecia “mais rápido” em uma das execuções, mas o desvio padrão gigantesco mostra que o sistema ficou instável — provavelmente por conta do GC ou contenção de threads.
✅ Conclusão: paralelize com cuidado
O que aprendemos:
- 🔹 Paralelize uma vez, na camada mais externa
- 🔹 Evite
.parallelStream()
aninhado - 🔹 Benchmarks revelam o que "parece rápido", mas não é
- 🔹 Mais
.parallelStream()
≠ mais performance
✌️ Bônus: minha lição pessoal
“Achei que estava otimizando. Estava só confundindo o escalonador.”
A minha hipótese estava correta. E se você ficou curioso, dá pra fazer uns experimentos menores que o resultado é o mesmo. Eu rodei um código bem mais simples e ainda assim é notável que o nestedParallelStreams
adiciona overheard em toda a operação.
🔗 Código completo
O código está disponível aqui.
Se lembre que você precisa adicionar a dependência do jmh
.
Clone, rode, brinque com os parâmetros — e veja por si mesmo!
Top comments (4)
Baita artigo! Também não sabia que o nested parallel causava essas issues.. interessante. Tem algum cenário em que você enxerga que isso possa ser interessante?
Você quer dizer em qual cenário vale a penar usar parallel? Ou em qual cenário tem que ter cuidado com nestedParallels?
Em qual cenário vale a pena usar.
Sempre que vc tem tarefas que podem ser quebradas em microtarefas independentes. Por exemplo, se vc processa countries x usuarios. Você consegue processar cada país em isolado? Se sim, vale à pena. Também é bom se atentar se seu processamento é CPU ou IO. Se for IO, não vale. Se for CPU, aí sim.