CodeGym /Cursos /JAVA 25 SELF /Melhores práticas de programação paralela

Melhores práticas de programação paralela

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

1. Nem tudo que pode ser paralelizado precisa ser paralelizado

Em Java há muitas maneiras de paralelizar tarefas. Mas “paralelismo = sempre mais rápido” é como pensar que, se adicionar mais sal à sopa, ela ficará mais saborosa: até certo ponto — sim; depois disso — é melhor não tentar.

ExecutorService é uma ótima opção quando você tem tarefas explícitas que precisam ser iniciadas e controladas: processamento de requisições, carregamento assíncrono de dados, cálculos independentes. Você decide quantas threads haverá no pool e gerencia o ciclo de vida das tarefas.

parallelStream — uma forma rápida de paralelizar o processamento de coleções quando as operações são independentes e sem efeitos colaterais. Vale a pena para coleções “pesadas” (dezenas de milhares de elementos ou mais).

ForkJoinPool — a escolha para tarefas que se dividem bem em subtarefas (divide & conquer): ordenação, busca, agregação de grandes arrays. É usado por dentro de parallelStream, mas você também pode controlá-lo diretamente.

Não use paralelismo “por via das dúvidas”. Se a tarefa for pequena, a sobrecarga de agendamento, troca de contexto e sincronização pode consumir todo o ganho.

Exemplo: quando o paralelismo não é necessário

List<Integer> smallList = List.of(1, 2, 3, 4, 5);
int sum = smallList.parallelStream()
    .mapToInt(x -> x)
    .sum(); // Paralelizar por 5 números é exagero!

2. Thread-safety: evite dados compartilhados mutáveis

No mundo paralelo, a principal ameaça são as condições de corrida (race conditions). Se várias threads alteram a mesma variável, o resultado pode ser inesperado.

  • Evite variáveis compartilhadas mutáveis. Até uma expressão como counter++ não é atômica.
  • Use coleções thread-safe e operações atômicas. Por exemplo, ConcurrentHashMap, CopyOnWriteArrayList, AtomicInteger, AtomicLong.
  • Sem efeitos colaterais em streams paralelos. Não modifique estruturas externas a partir de parallelStream.

Exemplo de código ruim

List<Integer> numbers = Arrays.asList(1,2,3,4,5);
List<Integer> result = new ArrayList<>();
numbers.parallelStream().forEach(n -> result.add(n * 2)); // PERIGOSO!

Aqui, result.add() não é thread-safe. Resultado: elementos perdidos ou exceções.

Como fazer corretamente?

List<Integer> result = numbers.parallelStream()
    .map(n -> n * 2)
    .collect(Collectors.toList());

3. Desempenho: mais threads nem sempre é melhor

Tarefas pequenas não valem a paralelização. Se o trabalho leva milissegundos, a execução paralela muitas vezes só vai desacelerar por causa da sobrecarga.

Meça o desempenho. Para medições rápidas, use System.nanoTime():

long start = System.nanoTime();
// ... seu código ...
long end = System.nanoTime();
System.out.println("Tempo de execução: " + (end - start) + " ns");

Para microbenchmarks sérios, use JMH (Java Microbenchmark Harness).

Exemplo: comparação entre stream sequencial e paralelo

List<Integer> bigList = IntStream.range(0, 1_000_000)
    .boxed().collect(Collectors.toList());

long t1 = System.nanoTime();
long sum1 = bigList.stream().mapToLong(x -> x).sum();
long t2 = System.nanoTime();
long sum2 = bigList.parallelStream().mapToLong(x -> x).sum();
long t3 = System.nanoTime();

System.out.println("Sequencial: " + (t2 - t1) / 1_000_000 + " ms");
System.out.println("Paralelo:     " + (t3 - t2) / 1_000_000 + " ms");

Tente no seu computador — o ganho é perceptível em coleções realmente grandes e em operações pesadas.

4. Tratamento de erros: não ignore exceções em threads

Future e tratamento de exceções

Se você iniciou a tarefa via ExecutorService.submit(), as exceções não “vazam” automaticamente — é necessário tratá-las com Future.get():

Future<Integer> future = executor.submit(() -> {
    if (Math.random() > 0.5) throw new RuntimeException("Ops!");
    return 42;
});
try {
    Integer result = future.get(); // pode lançar ExecutionException
} catch (ExecutionException e) {
    System.err.println("Erro na tarefa: " + e.getCause());
}

ForkJoin e tratamento de erros

No ForkJoinPool, as exceções são “empacotadas” na tarefa. Ao chamar join()/get(), elas emergem:

ForkJoinPool pool = new ForkJoinPool();
RecursiveTask<Integer> task = new MyTask();
try {
    int result = pool.invoke(task);
} catch (Exception e) {
    System.err.println("Erro no ForkJoin: " + e);
}

Não se esqueça de tratar InterruptedException

Muitos métodos (por exemplo, Future.get(), Thread.sleep()) podem lançar InterruptedException. Não “engula” a exceção — reaja corretamente: restaure o status de interrupção ou finalize a tarefa.

5. Depuração e testes de código paralelo

Logging e debug

Bugs paralelos são traiçoeiros e costumam se manifestar de forma instável. Registre com indicação da thread: Thread.currentThread().getName(). Isso ajuda a entender quem e quando executa o código.

Em casos complexos, use um depurador com suporte a multithreading (por exemplo, IntelliJ IDEA). Thread.sleep() temporários às vezes ajudam a “capturar” uma corrida de dados rara.

Testando cenários multithread

Separe testes específicos para operações paralelas e use utilitários de espera de condições, por exemplo Awaitility. Execute esses testes repetidamente: alguns problemas só aparecem na 100ª ou 1000ª execução.

6. Dicas e nuances úteis

Legibilidade e manutenção: escreva código paralelo compreensível

  • Documente. Comente trechos complexos e a escolha das ferramentas.
  • Use abstrações de alto nível. Prefira ExecutorService, parallelStream, ForkJoinPool em vez de gerenciamento manual de threads.
  • Evite “mágica”. Não complique a sincronização se for possível fazer de forma mais simples.

Tabela: quando usar cada ferramenta

Cenário Ferramenta recomendada
Muitas tarefas independentes
ExecutorService
Processamento de uma coleção grande parallelStream ou ForkJoin
Tarefa de “dividir para conquistar”
ForkJoinPool + RecursiveTask
Tarefa assíncrona simples
CompletableFuture
Muitas tarefas pequenas Stream sequencial
Tarefas com efeitos colaterais Apenas coleções thread-safe!

“Mandamentos” do programador paralelo

  • Não tenha variáveis compartilhadas mutáveis — a menos que tenha certeza de que são thread-safe.
  • Não paralelize por paralelizar: avalie o ganho potencial.
  • Não se esqueça de encerrar os pools: shutdown()/shutdownNow().
  • Não use parallelStream para operações com efeitos colaterais.
  • Não se esqueça de tratar exceções de Future e ForkJoinTask.
  • Não “engula” InterruptedException — finalize a tarefa corretamente.

7. Erros comuns em programação paralela

Erro nº 1: paralelização de tarefas pequenas. Muitas vezes iniciantes paralelizam tudo, mesmo quando o trabalho leva microssegundos. Resultado — mais lento por causa da sobrecarga.

Erro nº 2: efeitos colaterais em streams. Em parallelStream não se deve modificar variáveis ou coleções externas — você terá condições de corrida e bugs imprevisíveis.

Erro nº 3: ignorar exceções. Se você não tratar erros de Future.get() ou ForkJoinTask, não saberá por que a tarefa não foi concluída.

Erro nº 4: esquecer shutdown() no ExecutorService. Sem finalização explícita, o aplicativo pode “travar” ao encerrar.

Erro nº 5: uso de coleções não thread-safe. Escrever de várias threads em um ArrayList comum é um caminho certo para erros.

Erro nº 6: “engolir” InterruptedException. Se a thread foi interrompida — respeite isso e finalize o trabalho corretamente.

Erro nº 7: lógica de sincronização complexa demais. Blocos synchronized em excesso levam a deadlock/livelock. Prefira abstrações de alto nível.

1
Pesquisa/teste
Paralelismo e ForkJoin, nível 54, lição 4
Indisponível
Paralelismo e ForkJoin
Paralelismo e ForkJoin
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION