1. Relembrando a Stream API
Você já conhece a Stream API — é uma forma conveniente de trabalhar com coleções que permite escrever código compacto e claro para processamento de dados: filtragem, ordenação, contagem etc.
Aqui vai um exemplo clássico:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
.filter(n -> n % 2 == 0)
.mapToInt(n -> n)
.sum();
System.out.println(sum); // 6 (2 + 4)
Neste exemplo, a coleção é transformada em um stream (stream()), dele são selecionados apenas os números pares, em seguida eles são convertidos para int, e o resultado é somado com a chamada a sum().
A Stream API torna o código mais curto e expressivo: em vez de descrever passo a passo como tudo acontece, você apenas diz o que quer obter. Além disso, quando necessário, você pode alternar facilmente para processamento paralelo — com apenas uma linha.
2. Streams paralelos: sintaxe e princípio de funcionamento
Como tornar um stream paralelo?
É simples: em vez de stream() use parallelStream(). Ou chame o método .parallel() no stream existente.
List<Integer> numbers = ...;
int sum = numbers.parallelStream()
.filter(n -> n % 2 == 0)
.mapToInt(n -> n)
.sum();
Ou assim:
numbers.stream()
.parallel() // torna o stream paralelo
.filter(...)
.map(...)
.sum();
O que acontece por baixo dos panos?
- A coleção é automaticamente dividida em partes.
- Cada parte é processada em uma thread separada (usa-se ForkJoinPool — um pool de threads especializado).
- Os resultados são combinados em um valor final.
Ou seja, se você tem um processador multinúcleo, o processamento realmente ocorre em paralelo — por exemplo, a filtragem e a soma podem ser executadas simultaneamente em vários núcleos.
Onde isso é especialmente útil?
- Processamento de coleções grandes (dezenas de milhares de elementos ou mais).
- Cálculos complexos para cada elemento.
- Quando não é necessário preservar uma ordem estrita de processamento.
Exemplo: comparando stream sequencial e paralelo
Vamos observar um exemplo simples com processamento de um array grande.
import java.util.*;
import java.util.stream.*;
public class ParallelStreamDemo {
public static void main(String[] args) {
List<Integer> numbers = IntStream.rangeClosed(1, 10_000_000)
.boxed()
.collect(Collectors.toList());
// Stream sequencial
long time1 = System.currentTimeMillis();
long count1 = numbers.stream()
.filter(n -> n % 2 == 0)
.count();
long time2 = System.currentTimeMillis();
System.out.println("Sequencial: " + (time2 - time1) + " ms, pares: " + count1);
// Stream paralelo
long time3 = System.currentTimeMillis();
long count2 = numbers.parallelStream()
.filter(n -> n % 2 == 0)
.count();
long time4 = System.currentTimeMillis();
System.out.println("Paralelo: " + (time4 - time3) + " ms, pares: " + count2);
}
}
Experimente este código no seu computador — muito provavelmente o stream paralelo processará a coleção mais rápido (especialmente se você tiver um processador multinúcleo). Mas nem sempre! Os detalhes vêm a seguir.
3. Como funciona: ForkJoinPool e divisão automática
Streams paralelos usam por baixo dos panos o ForkJoinPool.commonPool(), que gerencia automaticamente a quantidade de threads (geralmente — de acordo com o número de núcleos de CPU disponíveis).
Esquematicamente:
+-----------------------------+
| Sua colecao |
+-----------------------------+
| 1 | 2 | 3 | ... | 10 mi |
+----+----+----+-----+--------+
| | | |
v v v v
[Thread1][Thread2]...[ThreadN]
| | | |
+----+----+-----------+
|
[Combinacao do resultado]
Cada thread processa sua parte e depois os resultados são combinados.
4. Limitações e armadilhas
Streams paralelos não são um botão mágico “acelerar tudo”. Às vezes eles até deixam a execução mais lenta!
Quando não compensa paralelizar:
- A coleção é pequena (até ~1000 elementos).
- A operação em cada elemento é muito rápida (por exemplo, apenas n * 2).
- Você precisa de ordem estrita de processamento (por exemplo, para escrita sequencial em arquivo).
Por quê? Criar e sincronizar threads também consome tempo. Se a tarefa for “miúda”, a sobrecarga pode superar o ganho do paralelismo.
Efeitos colaterais — o inimigo do paralelismo
Se suas operações dentro do stream modificam variáveis externas, tenha cuidado!
Exemplo ruim:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int[] sum = {0};
numbers.parallelStream().forEach(n -> sum[0] += n);
System.out.println(sum[0]); // ??? (você espera 15, mas pode obter qualquer coisa)
Por quê? Porque múltiplas threads modificam a mesma variável ao mesmo tempo — surge uma race condition (condição de corrida). O valor final pode ficar incorreto.
A forma correta — usar métodos do stream que retornam o resultado:
int sum = numbers.parallelStream().mapToInt(n -> n).sum();
Nem todas as coleções paralelizam igualmente bem
Algumas coleções (por exemplo, um ArrayList comum) são bem divididas em partes. Já o LinkedList ou um stream com número infinito de elementos (por exemplo, Stream.generate(...)) — nem tanto.
5. Prática: comparação de desempenho
Exemplo: busca do número máximo
import java.util.*;
import java.util.stream.*;
public class ParallelMaxDemo {
public static void main(String[] args) {
List<Integer> numbers = IntStream.rangeClosed(1, 30_000_000)
.boxed()
.collect(Collectors.toList());
// Sequencial
long t1 = System.currentTimeMillis();
int max1 = numbers.stream().max(Integer::compareTo).get();
long t2 = System.currentTimeMillis();
System.out.println("Sequencial: " + (t2 - t1) + " ms, max = " + max1);
// Paralelo
long t3 = System.currentTimeMillis();
int max2 = numbers.parallelStream().max(Integer::compareTo).get();
long t4 = System.currentTimeMillis();
System.out.println("Paralelo: " + (t4 - t3) + " ms, max = " + max2);
}
}
O que veremos? Em processadores modernos multinúcleo o stream paralelo costuma ser mais rápido. Mas se você trocar 30_000_000 por 1000, a diferença some — e às vezes o paralelo é até mais lento!
6. Exemplos de uso: filtragem, agregação, ordenação
Filtragem e contagem
List<String> names = Arrays.asList("Ana", "Bruno", "Luis", "Guilherme", "Dora", "Igor", "Zeca");
long count = names.parallelStream()
.filter(name -> name.length() == 4)
.count();
System.out.println("Nomes com 4 letras: " + count);
Agrupamento
List<String> words = Arrays.asList("gato", "peixe", "gato", "cao", "peixe", "gato");
Map<String, Long> freq = words.parallelStream()
.collect(Collectors.groupingBy(
w -> w,
Collectors.counting()
));
System.out.println(freq); // {cao=1, peixe=2, gato=3}
Ordenação (mas aqui o paralelismo nem sempre traz ganho!)
List<Integer> bigList = IntStream.rangeClosed(1, 5_000_000)
.boxed()
.collect(Collectors.toList());
long t1 = System.currentTimeMillis();
List<Integer> sorted = bigList.parallelStream()
.sorted()
.collect(Collectors.toList());
long t2 = System.currentTimeMillis();
System.out.println("Ordenacao paralela: " + (t2 - t1) + " ms");
7. Nuances importantes e recomendações
Quando vale a pena usar parallelStream()
- A coleção é grande (dezenas de milhares de elementos ou mais).
- A operação por elemento é “pesada” (cálculos complexos, trabalho com arquivos/rede).
- Não há dependência da ordem dos elementos.
- Não há efeitos colaterais (variáveis externas não são modificadas).
Quando NÃO vale a pena usar parallelStream()
- A coleção é pequena.
- A operação é rápida.
- É necessário preservar a ordem estrita.
- Há acesso a variáveis compartilhadas (considere coleções thread-safe ou outras abordagens).
Como saber quantas threads são usadas?
Por padrão — de acordo com a quantidade de núcleos do processador: Runtime.getRuntime().availableProcessors(). É possível alterar esse comportamento via propriedade do sistema:
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "8");
Faça isso somente se você entende as consequências — caso contrário, você pode “entupir” a CPU e causar lentidão.
8. Erros comuns ao trabalhar com streams paralelos
Erro nº 1: Efeitos colaterais dentro de forEach
Muita gente pensa: “Agora vou preencher uma lista em paralelo!”
List<Integer> result = new ArrayList<>();
IntStream.range(0, 1_000)
.parallel()
.forEach(result::add); // PERIGOSO!
System.out.println(result.size()); // O resultado é aleatorio!
Por que isso é ruim? ArrayList não é thread-safe e, ao adicionar simultaneamente de várias threads, o resultado é imprevisível: pode haver perdas, duplicações, exceções.
Solução: Use métodos de coleta do stream (collect), que já garantem segurança, ou coleções especiais.
List<Integer> result = IntStream.range(0, 1_000)
.parallel()
.boxed()
.collect(Collectors.toList());
Erro nº 2: Esperar aceleração em tarefas pequenas
Paralelismo não é “de graça”! Se a coleção é pequena, o stream paralelo pode ser mais lento devido à sobrecarga de escalonamento e sincronização.
Erro nº 3: Violação de ordem
Se a ordem dos elementos é importante (por exemplo, ao escrever em arquivo), não use streams paralelos — a ordem não é garantida (ou vai ficar mais lento).
Erro nº 4: Uso de coleções “desfavoráveis”
Algumas coleções (por exemplo, LinkedList, estruturas não padronizadas) se dividem mal em partes — a eficácia do paralelismo cai.
Erro nº 5: Ignorar a segurança de thread ao coletar resultados
Se você coleta resultados manualmente (por exemplo, adicionando a uma lista), use coleções thread-safe (CopyOnWriteArrayList, ConcurrentLinkedQueue) ou métodos de coleta do stream.
GO TO FULL VERSION