CodeGym /Cursos /JAVA 25 SELF /Streams paralelos: sintaxe e aplicação

Streams paralelos: sintaxe e aplicação

JAVA 25 SELF
Nível 54 , Lição 2
Disponível

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.

1
Tarefa
JAVA 25 SELF, nível 54, lição 2
Bloqueado
Medição do potencial energético dos artefatos ✨
Medição do potencial energético dos artefatos ✨
1
Tarefa
JAVA 25 SELF, nível 54, lição 2
Bloqueado
Organização do Desfile dos Animais 🐾
Organização do Desfile dos Animais 🐾
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION