CodeGym /Cursos /JAVA 25 SELF /Percursos paralelos no sistema de arquivos: Files.walk + ...

Percursos paralelos no sistema de arquivos: Files.walk + parallel() e ForkJoin

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

1. Problema: como processar muitos arquivos em um diretório de forma eficiente

Em aplicativos modernos, é comum surgir a tarefa: processar uma grande quantidade de arquivos em uma pasta e seus subdiretórios. Por exemplo:

  • Calcular o número total de linhas em todos os arquivos ".java" do projeto.
  • Encontrar todos os arquivos modificados no último mês.
  • Copiar ou excluir arquivos por um determinado critério.

Se houver poucos arquivos, um laço simples basta. Mas com milhares e dezenas de milhares, especialmente quando cada arquivo envolve uma operação “pesada” (leitura, parsing, análise), o tempo cresce significativamente.

Pergunta: como acelerar o processamento de uma grande quantidade de arquivos?
Resposta: usar paralelismo — processar os arquivos simultaneamente em várias threads.

2. Ferramentas para percorrer o sistema de arquivos

Files.walk()

No Java 8+ surgiu uma maneira prática de percorrer a árvore de diretórios — o método Files.walk() do pacote java.nio.file. Ele retorna um stream Stream<Path> — todos os arquivos e pastas, a partir do diretório especificado.

Exemplo:

import java.nio.file.*;
import java.util.stream.Stream;

Path start = Paths.get("src");
try (Stream<Path> stream = Files.walk(start)) {
    stream.forEach(System.out::println);
}
  • Files.walk(start) — retorna um stream de todos os arquivos e pastas, incluindo subdiretórios.
  • Você pode especificar a profundidade máxima do percurso: Files.walk(start, 3).

Files.find()

Se você precisa filtrar imediatamente por um critério (por exemplo, apenas arquivos ".java"), use Files.find():

import java.nio.file.*;
import java.util.stream.Stream;

Path start = Paths.get("src");

try (Stream<Path> stream = Files.find(
        start,
        Integer.MAX_VALUE,
        (path, attr) -> path.toString().endsWith(".java"))) {
    stream.forEach(System.out::println);
}
  • Files.find() aceita um filtro (BiPredicate<Path, BasicFileAttributes>) que recebe o caminho e os atributos do arquivo.

3. Processamento paralelo: parallel() e ForkJoinPool

Streams paralelos: .parallel()

Todo Stream possui o método parallel(). Ao chamá-lo, o processamento dos elementos ocorrerá em várias threads.

Files.walk(start)
    .parallel()
    .forEach(path -> processFile(path));

Cada arquivo será processado em paralelo (quando possível), o que é especialmente eficaz em operações “pesadas”: leitura, parsing e cálculos.

Como isso funciona por dentro? ForkJoinPool

Streams paralelos usam o pool comum de threads — ForkJoinPool.commonPool(). É um pool “inteligente” que distribui tarefas entre as threads.

  • Por padrão, o número de threads é igual ao de processadores disponíveis: Runtime.getRuntime().availableProcessors().
  • O modelo paralelo “fork/join” é adequado para tarefas independentes — como o processamento de arquivos individuais.

Quando usar .parallel()?

  • Quando o processamento de cada arquivo é independente dos demais.
  • Quando a operação é “pesada” (carrega a CPU ou passa muito tempo aguardando I/O).
  • Quando há muitos arquivos (centenas, milhares).

Evite usar streams paralelos:

  • Se houver poucos arquivos (a sobrecarga de paralelização pode superar o ganho).
  • Se for necessário ordem estrita ou houver dependências entre os elementos.

4. Alternativas e ajuste do paralelismo

Quando é melhor usar ExecutorService?

Streams paralelos são bons para casos simples. Mas se você precisa:

  • Controlar o número exato de threads (para tarefas IO-bound pode ser vantajoso ter mais threads do que núcleos).
  • Gerenciar filas, cancelamentos, tentativas (retries) e tratamento de erros.
  • Construir pipelines de tarefas mais complexos.

Então use ExecutorService:

import java.nio.file.*;
import java.util.concurrent.*;

ExecutorService executor = Executors.newFixedThreadPool(8);
Files.walk(start)
    .filter(Files::isRegularFile)
    .forEach(path -> executor.submit(() -> processFile(path)));
executor.shutdown();

Ajuste do ForkJoinPool

Por padrão, o pool comum usa um número de threads igual ao número de processadores. É possível alterar isso via propriedade do sistema (antes do primeiro uso de streams paralelos):

System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "16");
  • Após essa configuração, todos os streams paralelos usarão até 16 threads.

CPU-bound vs IO-bound tarefas

  • CPU-bound: carregam ativamente o processador (matemática, parsing, compressão). Número de threads ≈ número de núcleos.
  • IO-bound: passam muito tempo esperando disco/rede. Frequentemente é vantajoso ter mais threads do que núcleos.

Streams paralelos nem sempre são ideais para tarefas IO-bound — muitas vezes um ExecutorService próprio, com pool ampliado, apresenta melhor desempenho.

5. Exemplo: busca e processamento paralelos de arquivos

Vamos calcular o número total de linhas em todos os arquivos ".java" do projeto usando percurso paralelo.

import java.nio.file.*;
import java.util.stream.*;
import java.io.IOException;

public class LineCounter {
    public static void main(String[] args) throws IOException {
        Path start = Paths.get("src");

        long totalLines = Files.walk(start)
            .parallel() // processamento paralelo!
            .filter(p -> p.toString().endsWith(".java"))
            .mapToLong(LineCounter::countLines)
            .sum();

        System.out.println("Total de linhas de código: " + totalLines);
    }

    // Método para contar as linhas em um arquivo
    private static long countLines(Path path) {
        try (Stream<String> lines = Files.lines(path)) {
            return lines.count();
        } catch (IOException e) {
            System.err.println("Erro ao ler o arquivo: " + path);
            return 0;
        }
    }
}

O que está acontecendo:

  • Files.walk(start) — percorre todos os caminhos.
  • parallel() — ativamos o processamento paralelo.
  • filter(...) — mantemos apenas os arquivos ".java".
  • mapToLong(...) — contamos as linhas em cada arquivo.
  • sum() — somamos o resultado.

Vantagens: várias threads são utilizadas e, ainda assim, o código permanece conciso.

6. Nuances importantes e erros comuns

  • Nem toda tarefa fica mais rápida com paralelismo. Para conjuntos pequenos de arquivos ou operações rápidas, a sobrecarga pode tornar o programa mais lento.
  • Feche os recursos. Ao trabalhar com arquivos, use try-with-resources — assim os descritores não “vazam”. Por exemplo, Files.lines(path) em try(...).
  • Paralelismo aninhado. Executar streams paralelos dentro de outras tarefas paralelas (nested parallelism) raramente é eficaz e pode levar à degradação de desempenho.
  • Efeitos colaterais. Evite escrever em estruturas/arquivos compartilhados sem sincronização. Prefira operações “puras” sobre os elementos.

7. Esquema: como funciona o percurso paralelo de arquivos

flowchart TD
    A["Files.walk(start)"] --> B["Stream<Path>"]
    B --> C{".parallel()?"}
    C -- Não --> D[forEach sequencial]
    C -- Sim --> E["forEach paralelo (ForkJoinPool)"]
    E --> F[Processamento de arquivos em várias threads]

8. Erros típicos no processamento paralelo de arquivos

Erro nº 1: Usar streams paralelos para tarefas pequenas — a sobrecarga é maior que o benefício.

Erro nº 2: Esperar que streams paralelos acelerem IO-bound tanto quanto CPU-bound. Para I/O, geralmente é necessário um ExecutorService com pool maior.

Erro nº 3: Exceções não tratadas em lambdas — sem tratar IOException, o stream pode ser interrompido e o resultado acabar incompleto.

Erro nº 4: Condições de corrida ao escrever em variáveis ou arquivos compartilhados — sincronize o acesso ou evite efeitos colaterais.

Erro nº 5: Esquecer de fechar recursos — use try-with-resources para todas as operações com arquivos.

Erro nº 6: Tentar alterar ForkJoinPool.commonPool() após o primeiro uso — a configuração via System.setProperty(...) deve ser feita com antecedência.

Erro nº 7: Usar streams paralelos dentro de outros streams paralelos — frequentemente leva à degradação de desempenho.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION