1. Problème : comment traiter efficacement de nombreux fichiers dans un répertoire
Dans les applications modernes, on rencontre souvent la tâche suivante : traiter un grand nombre de fichiers dans un dossier et ses sous-répertoires. Par exemple :
- Compter le nombre total de lignes dans tous les fichiers ".java" d’un projet.
- Trouver tous les fichiers modifiés durant le dernier mois.
- Copier ou supprimer des fichiers selon un critère donné.
S’il y a peu de fichiers, une simple boucle suffit. Mais avec des milliers ou des dizaines de milliers, surtout lorsque chaque fichier subit une opération « lourde » (lecture, parsing, analyse), le temps augmente considérablement.
Question : comment accélérer le traitement d’un grand nombre de fichiers ?
Réponse : utiliser le parallélisme — traiter les fichiers simultanément dans plusieurs threads.
2. Outils pour parcourir le système de fichiers
Files.walk()
Depuis Java 8+, une méthode pratique pour parcourir un arbre de répertoires est apparue — la méthode Files.walk() du package java.nio.file. Elle renvoie un stream Stream<Path> — tous les fichiers et dossiers à partir du répertoire indiqué.
Exemple :
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) — renvoie un stream de tous les fichiers et dossiers, y compris les sous-répertoires.
- On peut indiquer une profondeur maximale de parcours : Files.walk(start, 3).
Files.find()
Si vous devez filtrer immédiatement selon un critère (par exemple, uniquement les fichiers ".java"), utilisez 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() prend un filtre (BiPredicate<Path, BasicFileAttributes>) qui reçoit le chemin et les attributs du fichier.
3. Traitement parallèle : parallel() et ForkJoinPool
Streams parallèles : .parallel()
Tout Stream possède la méthode parallel(). Si vous l’appelez, le traitement des éléments s’effectuera dans plusieurs threads.
Files.walk(start)
.parallel()
.forEach(path -> processFile(path));
Chaque fichier sera traité en parallèle (si possible), ce qui est particulièrement efficace pour des opérations « lourdes » : lecture, parsing, calculs.
Comment cela fonctionne-t-il en interne ? ForkJoinPool
Les streams parallèles utilisent un pool de threads commun — ForkJoinPool.commonPool(). C’est un pool « intelligent » qui répartit les tâches entre les threads.
- Par défaut, le nombre de threads = le nombre de processeurs disponibles : Runtime.getRuntime().availableProcessors().
- Le modèle parallèle « fork/join » convient bien aux tâches indépendantes — comme le traitement de fichiers individuels.
Quand utiliser .parallel() ?
- Lorsque le traitement de chaque fichier est indépendant des autres.
- Lorsque l’opération est « lourde » (elle sollicite le CPU ou attend longtemps l’IO).
- Lorsqu’il y a beaucoup de fichiers (centaines, milliers).
À éviter d’utiliser des streams parallèles :
- S’il y a peu de fichiers (les surcoûts de parallélisation peuvent dépasser les gains).
- Si un ordre strict est requis ou s’il existe des dépendances entre éléments.
4. Alternatives et réglage du parallélisme
Quand vaut-il mieux utiliser ExecutorService ?
Les streams parallèles sont adaptés aux cas simples. Mais si vous devez :
- Contrôler le nombre exact de threads (pour les tâches IO-bound, il est souvent avantageux d’avoir plus de threads que de cœurs).
- Gérer les files d’attente, l’annulation, les retries, le traitement des erreurs.
- Construire des pipelines de tâches plus complexes.
Alors utilisez 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();
Réglage de ForkJoinPool
Par défaut, le pool commun utilise un nombre de threads égal au nombre de processeurs. Vous pouvez le modifier via une propriété système (avant la première utilisation des streams parallèles) :
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "16");
- Après cet appel, tous les streams parallèles utiliseront jusqu’à 16 threads.
Tâches CPU-bound vs IO-bound
- CPU-bound : sollicitent activement le processeur (maths, parsing, compression). Nombre de threads ≈ nombre de cœurs.
- IO-bound : beaucoup d’attentes disque/réseau. Il est souvent avantageux d’avoir plus de threads que de cœurs.
Les streams parallèles ne sont pas toujours optimaux pour les tâches IO-bound — on obtient souvent de meilleurs résultats avec un ExecutorService dédié et un pool plus grand.
5. Exemple : recherche et traitement parallèles de fichiers
Comptons le nombre total de lignes dans tous les fichiers ".java" du projet en utilisant un parcours parallèle.
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() // traitement parallèle !
.filter(p -> p.toString().endsWith(".java"))
.mapToLong(LineCounter::countLines)
.sum();
System.out.println("Nombre total de lignes de code : " + totalLines);
}
// Méthode pour compter les lignes dans un fichier
private static long countLines(Path path) {
try (Stream<String> lines = Files.lines(path)) {
return lines.count();
} catch (IOException e) {
System.err.println("Erreur de lecture du fichier : " + path);
return 0;
}
}
}
Ce qui se passe :
- Files.walk(start) — parcours de tous les chemins.
- parallel() — activation du traitement parallèle.
- filter(...) — on ne conserve que les fichiers ".java".
- mapToLong(...) — comptage des lignes dans chaque fichier.
- sum() — somme du résultat.
Avantages : plusieurs threads sont utilisés tout en gardant un code concis.
6. Points importants et erreurs typiques
- Toutes les tâches ne sont pas accélérées par le parallélisme. Pour de petits ensembles de fichiers ou des opérations rapides, les surcoûts peuvent ralentir le programme.
- Fermez les ressources. Lors de la manipulation de fichiers, utilisez try-with-resources — ainsi, les descripteurs ne « fuiront » pas. Par exemple, Files.lines(path) dans try(...).
- Parallélisme imbriqué. Lancer des streams parallèles à l’intérieur d’autres tâches parallèles (nested parallelism) est rarement efficace et peut dégrader les performances.
- Effets de bord. Évitez d’écrire dans des structures/fichiers partagés sans synchronisation. Préférez des opérations « pures » sur les éléments.
7. Schéma : comment fonctionne le parcours parallèle de fichiers
flowchart TD
A["Files.walk(start)"] --> B["Stream<Path>"]
B --> C{".parallel()?"}
C -- Non --> D[forEach classique]
C -- Oui --> E["forEach parallèle (ForkJoinPool)"]
E --> F[Traitement des fichiers dans plusieurs threads]
8. Erreurs typiques lors du traitement parallèle de fichiers
Erreur n° 1 : Utiliser des streams parallèles pour de petites tâches — les surcoûts dépassent les gains.
Erreur n° 2 : S’attendre à ce que les streams parallèles accélèrent les tâches IO-bound autant que les CPU-bound. Pour l’IO, un ExecutorService avec un plus grand pool est souvent nécessaire.
Erreur n° 3 : Exceptions non gérées dans les lambdas — sans gestion de IOException, le stream peut s’interrompre et le résultat être incomplet.
Erreur n° 4 : Conditions de course lors des écritures dans des variables ou fichiers partagés — synchronisez l’accès ou évitez les effets de bord.
Erreur n° 5 : Oublier de fermer les ressources — utilisez try-with-resources pour toutes les opérations sur fichiers.
Erreur n° 6 : Tenter de modifier ForkJoinPool.commonPool() après sa première utilisation — la configuration via System.setProperty(...) doit être faite à l’avance.
Erreur n° 7 : Utiliser des streams parallèles à l’intérieur d’autres streams parallèles — conduit souvent à une dégradation des performances.
GO TO FULL VERSION