CodeGym /Cours /JAVA 25 SELF /Lecture et écriture de fichiers en multithreading

Lecture et écriture de fichiers en multithreading

JAVA 25 SELF
Niveau 59, Leçon 0
Disponible

1. Quand le multithreading aide

Le multithreading est utile lorsqu’il y a beaucoup de travail que l’on peut faire en même temps. Par exemple, s’il faut traiter des dizaines de fichiers — les copier, les recalculer ou les analyser — il est plus simple de confier des parties différentes à des threads distincts que de tout faire séquentiellement. C’est comme si, au lieu d’un seul ami qui trie votre archive de photos, vous en appeliez cinq d’un coup: le travail avancera plus vite et sera plus agréable.

Il est particulièrement utile pour le traitement par lots de fichiers, le téléchargement ou la copie de gros fichiers par morceaux, ou encore lorsque, après lecture des données, il faut calculer en parallèle quelque chose pour différentes parties.

Mais le multithreading n’aide pas toujours. Si vous n’avez qu’un petit fichier, lancer une dizaine de threads n’a aucun sens. Si le disque ou le réseau est déjà saturé, de nouveaux threads ne feront que ralentir le processus. Et si plusieurs threads écrivent simultanément dans le même fichier sans synchronisation, vous pouvez obtenir un véritable chaos avec des données corrompues.

En bref, le multithreading est un outil. Comme un marteau: on peut enfoncer un clou, mais aussi se taper sur les doigts. L’essentiel est de savoir quand et comment l’utiliser.

2. Outils Java pour les E/S multithread

Vous savez déjà qu’en Java il existe plusieurs façons de lancer des tâches en parallèle:

  • Thread classique — création manuelle des threads.
  • Pool de threads via ExecutorService — une approche moderne, flexible et pratique.
  • CompletableFuture et flux parallèles (Stream API) — pour des cas avancés (nous les étudierons plus en détail dans les prochaines leçons).

Commençons par le plus simple: traiter plusieurs fichiers dans des threads différents.

Exemple 1: Thread classique

public class FileCopyTask extends Thread {
    private final Path source;
    private final Path target;

    public FileCopyTask(Path source, Path target) {
        this.source = source;
        this.target = target;
    }

    @Override
    public void run() {
        try {
            Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
            System.out.println("Fichier copié: " + source);
        } catch (IOException e) {
            System.err.println("Erreur de copie " + source + ": " + e.getMessage());
        }
    }
}
// Lancement de plusieurs copies dans des threads séparés
List<Path> filesToCopy = List.of(
    Path.of("log1.txt"), Path.of("log2.txt"), Path.of("log3.txt")
);
for (Path file : filesToCopy) {
    new FileCopyTask(file, Path.of("backup_" + file.getFileName())).start();
}

Avantages: Simple et clair.

Inconvénients: Gérer un grand nombre de threads à la main est peu pratique, et il n’y a pas de contrôle sur le nombre de threads actifs simultanément.

Exemple 2: ExecutorService — pool de threads

ExecutorService vous permet de déléguer des tâches à un pool de threads, qui décidera lui-même combien de threads utiliser en parallèle.

import java.util.concurrent.*;

public class MultiFileCopier {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executor = Executors.newFixedThreadPool(4); // au maximum 4 threads

        List<Path> filesToCopy = List.of(
            Path.of("log1.txt"), Path.of("log2.txt"), Path.of("log3.txt")
        );
        for (Path file : filesToCopy) {
            executor.submit(() -> {
                try {
                    Files.copy(file, Path.of("backup_" + file.getFileName()), StandardCopyOption.REPLACE_EXISTING);
                    System.out.println("Copié: " + file);
                } catch (IOException e) {
                    System.err.println("Erreur: " + file + " " + e.getMessage());
                }
            });
        }

        executor.shutdown(); // on n’accepte plus de tâches
        executor.awaitTermination(1, TimeUnit.MINUTES); // on attend la fin de toutes les tâches
    }
}

Avantages:

  • Facile à faire évoluer (on peut définir le nombre de threads nécessaire).
  • Pratique pour contrôler la fin des tâches (méthodes shutdown(), awaitTermination(...)).
  • Convient pour traiter des centaines voire des milliers de fichiers.

3. Problèmes et limites des E/S multithread

Concurrence pour les ressources

Si vous essayez de lire ou d’écrire simultanément le même fichier à partir de plusieurs threads sans synchronisation, le résultat sera imprévisible. C’est comme si deux personnes écrivaient en même temps sur la même page d’un livre: ce sera un brouillon illisible. Pour la coordination, utilisez par exemple synchronized, des verrous explicites ou un thread d’écriture dédié.

Limites du système de fichiers et de l’OS

  • Tous les systèmes de fichiers ne gèrent pas bien l’écriture simultanée dans un même fichier.
  • Le système d’exploitation peut limiter le nombre de fichiers ouverts en même temps.
  • Un disque dur (surtout un HDD) fonctionne mal avec un grand nombre d’accès aléatoires.

Synchronisation lors de l’écriture dans une ressource partagée

Si plusieurs threads écrivent dans un même fichier (par exemple, un journal), utilisez impérativement la synchronisation (par exemple via synchronized, des verrous ou des threads d’écriture dédiés).

Inefficacité avec de petits fichiers

Pour de petits fichiers, la surcharge liée à la création des threads et aux changements de contexte peut être supérieure au gain du parallélisme.

4. Exemples pratiques

Copie de fichiers en parallèle

Supposons que nous ayons un dossier avec beaucoup de logs à copier vers un répertoire d’archives.

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

public class ParallelFileCopier {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executor = Executors.newFixedThreadPool(4);

        List<Path> filesToCopy = List.of(
            Path.of("log1.txt"), Path.of("log2.txt"), Path.of("log3.txt")
            // ... ajoutez autant de fichiers que vous voulez
        );

        for (Path file : filesToCopy) {
            executor.submit(() -> {
                try {
                    Path target = Path.of("archive", file.getFileName().toString());
                    Files.copy(file, target, StandardCopyOption.REPLACE_EXISTING);
                    System.out.println("Copié: " + file);
                } catch (IOException e) {
                    System.err.println("Erreur: " + file + " " + e.getMessage());
                }
            });
        }

        executor.shutdown();
        executor.awaitTermination(10, TimeUnit.MINUTES);
    }
}

Commentaire:

  • Nous utilisons un pool de 4 threads — c’est généralement suffisant pour charger le disque sans surcharger le système.
  • Pour 1000 fichiers, on peut augmenter le pool à 8, mais il ne faut pas le rendre trop grand.

Traitement parallèle des lignes d’un fichier avec le Stream API

Depuis Java 8, on peut utiliser des flux parallèles pour traiter le contenu d’un fichier:

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

public class ParallelLineProcessing {
    public static void main(String[] args) throws IOException {
        Path path = Path.of("biglog.txt");

        // Files.lines renvoie Stream<String> — un flux de lignes du fichier
        Files.lines(path)
            .parallel() // on le transforme en flux parallèle
            .filter(line -> line.contains("ERROR"))
            .forEach(line -> System.out.println("Erreur: " + line));
    }
}

Important:

  • Les flux parallèles accélèrent le traitement s’il est gourmand en calcul (CPU-bound), pas le processus de lecture lui-même (IO-bound).
  • Si le traitement d’une ligne est simple (par exemple, juste un System.out.println), il se peut qu’il n’y ait aucun gain.

Lecture/écriture de différentes parties d’un même gros fichier

Java permet de lire ou d’écrire différentes sections d’un même fichier simultanément à l’aide de FileChannel et des méthodes de positionnement. C’est un niveau avancé, mais le principe est simple: chaque thread travaille sur sa propre portion du fichier.

import java.nio.channels.FileChannel;
import java.nio.file.*;
import java.io.*;
import java.nio.ByteBuffer;

public class FileChunkReader implements Runnable {
    private final Path path;
    private final long position;
    private final long size;

    public FileChunkReader(Path path, long position, long size) {
        this.path = path;
        this.position = position;
        this.size = size;
    }

    @Override
    public void run() {
        try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
            ByteBuffer buffer = ByteBuffer.allocate((int) size);
            channel.read(buffer, position);
            System.out.println("Morceau lu à partir de la position " + position + " de taille " + size);
            // Ici, on peut traiter le buffer
        } catch (IOException e) {
            System.err.println("Erreur de lecture du morceau: " + e.getMessage());
        }
    }
}
// Exemple d'exécution: on lit le fichier par blocs de 1 Mo avec 4 threads
Path file = Path.of("bigdata.bin");
long fileSize = Files.size(file);
long chunkSize = 1024 * 1024; // 1 Mo
int chunks = (int) Math.ceil((double) fileSize / chunkSize);

ExecutorService executor = Executors.newFixedThreadPool(4);

for (int i = 0; i < chunks; i++) {
    long position = i * chunkSize;
    long size = Math.min(chunkSize, fileSize - position);
    executor.submit(new FileChunkReader(file, position, size));
}

executor.shutdown();
executor.awaitTermination(10, TimeUnit.MINUTES);

Commentaire:

  • Chaque thread lit sa propre portion du fichier sans gêner les autres.
  • Cette approche est utilisée, par exemple, par les torrents et les gestionnaires de téléchargement.

Synchronisation lors de l’écriture dans un fichier commun

Si plusieurs threads écrivent dans un même fichier (par exemple, un journal), il faut synchroniser l’accès pour éviter d’obtenir un « charabia » de lignes:

import java.io.*;

public class SafeLogger {
    private final Writer writer;

    public SafeLogger(String filename) throws IOException {
        this.writer = new BufferedWriter(new FileWriter(filename, true));
    }

    public synchronized void log(String message) throws IOException {
        writer.write(message);
        writer.write(System.lineSeparator());
        writer.flush();
    }

    public void close() throws IOException {
        writer.close();
    }
}

Commentaire:

  • La méthode log est marquée synchronized afin qu’un seul thread écrive dans le fichier à un instant donné.
  • Ça fonctionne, mais avec beaucoup de threads cela peut devenir un goulot d’étranglement — mieux vaut écrire dans des fichiers différents puis les fusionner.

5. Quand il ne faut pas utiliser le multithreading

Le multithreading est séduisant: plus de threads, donc tout devrait aller plus vite! Mais en pratique, ce n’est pas toujours le cas. Si vous traitez quelques petits fichiers, il est plus simple et plus fiable de le faire séquentiellement. Le temps passé à lancer les threads et à les coordonner ne sera tout simplement pas rentabilisé.

Parfois, le problème ne vient pas du disque, mais du réseau — alors ajouter des threads n’accélérera rien, car le bottleneck se trouve ailleurs. Un autre piège est l’écriture parallèle dans un même fichier. Si vous avez peu d’expérience avec la synchronisation, mieux vaut éviter: le risque d’obtenir des données corrompues est élevé.

Enfin, si votre disque ou votre système de fichiers n’apprécie pas d’être sollicité par des dizaines de threads, le multithreading ne sauvera rien et ne fera qu’empirer la situation.

En bref, si vous pensez que « plus il y a de threads, mieux c’est », c’est le plus souvent faux. Parfois, un seul thread tranquille fait le travail plus proprement, plus vite et plus sûrement que dix threads pressés.

6. Brève introduction à FileChannel pour des tâches avancées

FileChannel du package java.nio.channels est un outil bas niveau pour travailler avec des fichiers, permettant de lire et d’écrire des données à des positions arbitraires. Cela permet, par exemple, de réaliser un téléchargement parallèle ou le traitement de gros fichiers par morceaux.

Exemple:

try (FileChannel channel = FileChannel.open(Path.of("bigfile.bin"), StandardOpenOption.READ)) {
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    long position = 0;
    int bytesRead = channel.read(buffer, position); // lire 1024 octets à partir de la position 0
    // traitement du buffer
}

Important:

  • FileChannel n’est pas synchronisé — si plusieurs threads utilisent le même canal, la synchronisation doit être implémentée manuellement.
  • Pour du travail en parallèle, il est plus simple d’ouvrir un canal distinct par thread.

7. Erreurs typiques avec les E/S multithread

Erreur №1: Accès non synchronisé à un même fichier en écriture.
Résultat: données corrompues, caractères étranges, et parfois un fichier illisible. Synchronisez toujours l’accès ou écrivez dans des fichiers séparés.

Erreur №2: Trop de threads.
Si vous ouvrez 1000 threads pour copier 1000 fichiers, votre ordinateur peut « râler » (OutOfMemoryError, ralentissements, plantages). Utilisez un pool de threads (ExecutorService) et limitez leur nombre.

Erreur №3: Des threads/fichiers ne sont pas fermés.
Chaque thread/fichier ouvert est une ressource de l’OS. Si vous ne les fermez pas, vous pouvez obtenir l’erreur « Too many open files ». Utilisez try-with-resources ou n’oubliez pas d’appeler close().

Erreur №4: Fin prématurée du programme.
Si vous ne laissez pas tous les threads terminer (par exemple, si vous n’appelez pas executor.awaitTermination(...)), le programme peut s’arrêter avant que tous les fichiers ne soient copiés.

Erreur №5: Écriture parallèle dans la même zone d’un fichier sans gestion des positions.
Si plusieurs threads écrivent dans la même zone d’un fichier, les données se mélangeront. Pour l’écriture par positions, utilisez des canaux et une séparation stricte des plages.

1
Mission
JAVA 25 SELF, niveau 59, leçon 0
Bloqué
Lecture parallèle de parties d'un grand fichier
Lecture parallèle de parties d'un grand fichier
1
Mission
JAVA 25 SELF, niveau 59, leçon 0
Bloqué
Écriture synchronisée dans un fichier journal partagé depuis plusieurs threads
Écriture synchronisée dans un fichier journal partagé depuis plusieurs threads
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION