CodeGym /Corsi /JAVA 25 SELF /Attraversamenti paralleli del file system: Files.walk + p...

Attraversamenti paralleli del file system: Files.walk + parallel() e ForkJoin

JAVA 25 SELF
Livello 59 , Lezione 2
Disponibile

1. Problema: come elaborare in modo efficiente molti file in una directory

Nelle applicazioni moderne capita spesso il compito: elaborare un gran numero di file in una cartella e nelle sue sottodirectory. Per esempio:

  • Calcolare il numero totale di righe in tutti i file ".java" del progetto.
  • Trovare tutti i file modificati nell'ultimo mese.
  • Copiare o eliminare file in base a un determinato criterio.

Se i file sono pochi, basta un ciclo normale. Ma con migliaia e decine di migliaia, soprattutto quando su ogni file viene eseguita un'operazione «pesante» (lettura, parsing, analisi), il tempo cresce sensibilmente.

Domanda: come velocizzare l'elaborazione di un gran numero di file?
Risposta: usare il parallelismo — elaborare i file contemporaneamente in più thread.

2. Strumenti per attraversare il file system

Files.walk()

In Java 8+ è apparso un modo comodo per attraversare l'albero delle directory — il metodo Files.walk() dal package java.nio.file. Restituisce uno Stream<Path> — tutti i file e le cartelle, a partire dalla directory specificata.

Esempio:

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) — restituisce uno stream di tutti i file e cartelle, incluse le sottodirectory.
  • È possibile specificare la profondità massima di attraversamento: Files.walk(start, 3).

Files.find()

Se è necessario filtrare subito per un criterio (ad esempio solo i file ".java"), usa 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() accetta un filtro (BiPredicate<Path, BasicFileAttributes>) che riceve il percorso e gli attributi del file.

3. Elaborazione parallela: parallel() e ForkJoinPool

Stream paralleli: .parallel()

Ogni Stream ha il metodo parallel(). Se lo si invoca, l'elaborazione degli elementi avverrà su più thread.

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

Ogni file verrà elaborato in parallelo (quando possibile), il che è particolarmente efficace per operazioni «pesanti»: lettura, parsing, calcoli.

Come funziona internamente? ForkJoinPool

Gli stream paralleli utilizzano un pool di thread comune — ForkJoinPool.commonPool(). È un pool «intelligente» che distribuisce i task tra i thread.

  • Per impostazione predefinita il numero di thread è uguale al numero di processori disponibili: Runtime.getRuntime().availableProcessors().
  • Il modello parallelo «fork/join» è adatto a task indipendenti — come l'elaborazione di singoli file.

Quando usare .parallel()?

  • Quando l'elaborazione di ciascun file è indipendente dagli altri.
  • Quando l'operazione è «pesante» (impegna la CPU o attende a lungo l'I/O).
  • Quando i file sono molti (centinaia, migliaia).

Non conviene usare gli stream paralleli:

  • Se i file sono pochi (i costi del parallelismo possono superare i benefici).
  • Se è richiesto un ordine rigoroso o ci sono dipendenze tra gli elementi.

4. Alternative e ottimizzazione del parallelismo

Quando è meglio usare ExecutorService?

Gli stream paralleli sono ottimi per casi semplici. Ma se serve:

  • Controllare il numero esatto di thread (per i task IO-bound conviene avere più thread che core).
  • Gestire code, cancellazioni, retry, gestione degli errori.
  • Costruire pipeline di task più complesse.

Allora usa 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();

Ottimizzazione di ForkJoinPool

Per impostazione predefinita il pool comune usa un numero di thread pari al numero di processori. È possibile modificarlo tramite una proprietà di sistema (prima del primo utilizzo degli stream paralleli):

System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "16");
  • Dopo questa impostazione tutti gli stream paralleli useranno fino a 16 thread.

CPU-bound vs IO-bound task

  • CPU-bound: caricano intensamente la CPU (calcoli, parsing, compressione). Numero di thread ≈ numero di core.
  • IO-bound: molto tempo di attesa su disco/rete. Spesso conviene avere più thread che core.

Gli stream paralleli non sono sempre ottimali per i task IO-bound — spesso conviene un ExecutorService dedicato con un pool aumentato.

5. Esempio: ricerca ed elaborazione parallela dei file

Calcoliamo il numero totale di righe in tutti i file ".java" del progetto, utilizzando l'attraversamento parallelo.

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() // elaborazione parallela!
            .filter(p -> p.toString().endsWith(".java"))
            .mapToLong(LineCounter::countLines)
            .sum();

        System.out.println("Totale righe di codice: " + totalLines);
    }

    // Metodo per contare le righe in un file
    private static long countLines(Path path) {
        try (Stream<String> lines = Files.lines(path)) {
            return lines.count();
        } catch (IOException e) {
            System.err.println("Errore di lettura del file: " + path);
            return 0;
        }
    }
}

Cosa succede:

  • Files.walk(start) — attraversamento di tutti i percorsi.
  • parallel() — abilitiamo l'elaborazione parallela.
  • filter(...) — manteniamo solo i file ".java".
  • mapToLong(...) — contiamo le righe in ogni file.
  • sum() — sommiamo il risultato.

Vantaggi: si sfruttano più thread e il codice rimane conciso.

6. Dettagli importanti ed errori comuni

  • Non tutti i task traggono beneficio dal parallelismo. Per insiemi piccoli di file o operazioni veloci, l'overhead può rallentare il programma.
  • Chiudete le risorse. Quando lavorate con i file usate try-with-resources — così i descrittori non andranno in «leak». Per esempio, Files.lines(path) nel try(...).
  • Parallelismo annidato. Avviare stream paralleli all'interno di altri task paralleli (nested parallelism) raramente è efficace e può portare a degrado delle prestazioni.
  • Effetti collaterali. Evitate di scrivere in strutture/file condivisi senza sincronizzazione. Preferite operazioni «pure» sugli elementi.

7. Schema: come funziona l'attraversamento parallelo dei file

flowchart TD
    A["Files.walk(start)"] --> B["Stream<Path>"]
    B --> C{".parallel()?"}
    C -- No --> D[forEach normale]
    C -- Sì --> E["forEach parallelo (ForkJoinPool)"]
    E --> F[Elaborazione dei file su più thread]

8. Errori tipici durante l'elaborazione parallela dei file

Errore n. 1: Uso degli stream paralleli per task piccoli — l'overhead è maggiore del guadagno.

Errore n. 2: Aspettarsi che gli stream paralleli accelerino i task IO-bound come quelli CPU-bound. Per l'I/O spesso serve un ExecutorService con un pool più grande.

Errore n. 3: Eccezioni non gestite nelle lambda — senza gestione di IOException lo stream può interrompersi e il risultato può essere incompleto.

Errore n. 4: Race condition durante la scrittura in variabili o file condivisi — sincronizzate l'accesso o evitate effetti collaterali.

Errore n. 5: Dimenticare di chiudere le risorse — usate try-with-resources per tutte le operazioni sui file.

Errore n. 6: Tentare di modificare ForkJoinPool.commonPool() dopo il primo utilizzo — l'impostazione tramite System.setProperty(...) va fatta in anticipo.

Errore n. 7: Uso di stream paralleli all'interno di altri stream paralleli — spesso porta a degrado delle prestazioni.

Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION