CodeGym /Cours /JAVA 25 SELF /Fichiers volumineux : modèles de chunking

Fichiers volumineux : modèles de chunking

JAVA 25 SELF
Niveau 41 , Leçon 2
Disponible

1. Introduction

Dans le monde moderne, les données croissent plus vite que des champignons après la pluie. Il arrive de devoir manipuler des fichiers de plusieurs dizaines voire centaines de gigaoctets — journaux, dumps de bases de données ou immenses archives. Tenter de lire un tel fichier en entier en mémoire se termine généralement mal : le programme soit « dévore » toute la RAM, soit se met à fonctionner douloureusement lentement.

Les raisons sont évidentes. La mémoire vive n’est pas infinie, et si le fichier dépasse sa capacité, vous risquez d’attraper une OutOfMemoryError. Même si la mémoire suffit, la lecture et le traitement séquentiels d’un fichier gigantesque dans un seul thread peuvent durer des heures. À cela s’ajoute la limite du disque lui‑même : sa vitesse de lecture est fixe, mais en utilisant plusieurs threads, surtout sur SSD, on peut accélérer sensiblement le processus.

La conclusion est donc simple : les gros fichiers doivent être traités par morceaux, les fameux chunks, et si possible de manière parallèle. Cette approche permet de manipuler des gigaoctets de données sans souffrance inutile.

2. Solution : modèle de chunking

Chunking — c’est un modèle où un gros fichier est découpé en petits morceaux gérables (chunks) pouvant être traités indépendamment les uns des autres.

Analogie :
Au lieu d’essayer de manger une pastèque entière d’un coup, vous la coupez en tranches et vous les mangez une par une. C’est plus simple et plus rapide !

Comment ça marche ?

  1. Déterminer la taille du fichier.
    • Avec File.length() ou Files.size(Path), on obtient le nombre d’octets du fichier.
  2. Calculer la taille du chunk (chunk size).
    • On choisit généralement 10–20 Mo (ou plus/moins — selon la tâche et le matériel).
    • Il est pratique de stocker la taille du chunk dans la variable chunkSize et de la choisir multiple de la taille de bloc du disque pour des performances maximales.
  3. Créer la liste des tâches.
    • Chaque tâche correspond au traitement d’un chunk : lecture, parsing, chiffrement, compression, etc.
    • On peut lancer les tâches en parallèle à l’aide d’un pool de threads.

Visualisation :

+-------------------+
|      Fichier      |
+-------------------+
|  [chunk 1]        |
|  [chunk 2]        |
|  [chunk 3]        |
|  ...              |
|  [chunk N]        |
+-------------------+

3. Mise en œuvre du traitement parallèle

Utiliser ExecutorService ou ForkJoinPool

Pour traiter les chunks en parallèle, utilisez les mécanismes de multithreading standard de Java :

  • ExecutorService — un pool de threads de taille fixe (Executors.newFixedThreadPool(n)).
  • ForkJoinPool — pour les tâches récursives et l’approche « diviser pour régner ».

Exemple :

ExecutorService pool = Executors.newFixedThreadPool(4); // 4 threads

for (int i = 0; i < chunkCount; i++) {
    final int chunkIndex = i;
    pool.submit(() -> {
        processChunk(file, chunkIndex, chunkSize);
    });
}

pool.shutdown();
pool.awaitTermination(1, TimeUnit.HOURS);

Chaque tâche lit son propre chunk du fichier et le traite indépendamment.

4. Mécanismes clés : RandomAccessFile et FileChannel

RandomAccessFile

RandomAccessFile permet de « se déplacer » dans le fichier et de lire à la position voulue.

try (RandomAccessFile raf = new RandomAccessFile(file, "r")) {
    raf.seek(chunkStart); // Se déplacer au début du chunk
    byte[] buffer = new byte[chunkSize];
    int bytesRead = raf.read(buffer);
    // Traiter le buffer
}
  • seek(long pos) — déplace le « curseur » à la position voulue.
  • On peut lire uniquement la plage d’octets nécessaire.

FileChannel

FileChannel — une méthode plus moderne et plus rapide (surtout pour les gros fichiers).

try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
    ByteBuffer buffer = ByteBuffer.allocate(chunkSize);
    channel.position(chunkStart);
    int bytesRead = channel.read(buffer);
    // Traiter le buffer
}
  • position(long newPosition) — définit la position de lecture.
  • On peut lire uniquement la plage souhaitée sans toucher au reste du fichier.

5. Comparaison du chunking avec transferTo/transferFrom

transferTo/transferFrom

Les méthodes FileChannel.transferTo() et transferFrom() permettent d’utiliser ce que l’on appelle le zero-copy. L’idée est simple : les données peuvent être copiées ou déplacées directement entre fichiers et flux en évitant les buffers de la JVM. Cela rend les opérations très rapides. Seule contrainte — on ne peut pas modifier les données « à la volée », on peut seulement les copier, mais pour de nombreux cas ce procédé accélère nettement le traitement de gros volumes d’information.

Exemple :

try (FileChannel src = FileChannel.open(srcPath, READ);
     FileChannel dst = FileChannel.open(dstPath, WRITE)) {
    src.transferTo(0, src.size(), dst);
}

Chunking

Ainsi, le chunking est un moyen de travailler avec de gros fichiers par parties, en chunks. Il ne sert pas seulement à copier des données, mais aussi à les traiter : on peut parser, chiffrer, compresser ou rechercher des informations au fil de l’eau. Chaque chunk peut être traité indépendamment, et au besoin même en parallèle, ce qui accélère considérablement le travail.

L’idée est simple : si la tâche se réduit à une simple copie, mieux vaut utiliser transferTo ou transferFrom, où les données circulent directement, rapidement et sans copies inutiles. Mais si vous devez faire quelque chose avec le contenu — chercher, modifier, analyser — le chunking devient un outil indispensable.

6. Limites et pièges

Surcharge liée aux threads

  • Créer trop de threads peut entraîner une baisse de performances (commutations de contexte, compétition pour les ressources).
  • On choisit généralement un nombre de threads égal au nombre de cœurs du processeur ou légèrement supérieur.

Limitations du disque

  • Même avec 100 threads, le disque ne pourra pas lire plus vite que sa vitesse maximale.
  • Sur SSD, la lecture parallèle peut apporter un gain ; sur HDD — quasiment aucun.

Besoin de synchronisation

  • Si le traitement des chunks est indépendant — tout est simple.
  • Si vous devez rassembler un résultat global (par exemple, sommer tous les nombres d’un fichier), il faudra synchroniser l’accès aux variables partagées (p. ex. utiliser AtomicLong ou collecter les résultats dans une liste séparée).

Frontières des chunks

  • Si le fichier est texte, prudence : ne coupez pas une ligne ou un caractère au milieu.
  • Pour les fichiers binaires (archives, images) — on peut généralement couper comme on veut.
  • Pour les fichiers texte, on prévoit souvent un « chevauchement » entre chunks ou on cherche le saut de ligne le plus proche.

7. Exemple : calcul parallèle de la somme des nombres dans un gros fichier

Objectif :
On dispose d’un fichier contenant des millions de nombres (un par ligne). Il faut calculer rapidement leur somme.

Plan étape par étape :

  1. Déterminer la taille du fichier.
  2. Choisir la taille du chunk (par exemple, 10 Mo).
  3. Pour chaque chunk :
    • Trouver le saut de ligne le plus proche (pour ne pas couper un nombre en deux).
    • Lire le chunk, parser les nombres, calculer la somme.
  4. Agréger les sommes de tous les chunks.

Squelette de code :

ExecutorService pool = Executors.newFixedThreadPool(4);
List<Future<Long>> results = new ArrayList<>();

for (int i = 0; i < chunkCount; i++) {
    final int chunkIndex = i;
    results.add(pool.submit(() -> {
        // Ouvrir RandomAccessFile, rechercher les frontières du chunk
        // Lire, parser les nombres, calculer la somme
        long chunkSum = 0L;
        return chunkSum;
    }));
}

long total = 0;
for (Future<Long> f : results) {
    total += f.get();
}
pool.shutdown();
System.out.println("Somme: " + total);

8. Bilan et bonnes pratiques

  • Chunking — un modèle universel pour traiter de gros fichiers : on découpe en chunks, on traite indépendamment, on agrège le résultat.
  • Utilisez RandomAccessFile ou FileChannel pour lire à la position souhaitée.
  • Pour le traitement parallèle — ExecutorService ou ForkJoinPool.
  • Pour une copie sans traitement — utilisez transferTo/transferFrom (zero-copy).
  • Surveillez la taille des chunks, le nombre de threads et les limitations du disque.
  • Pour les fichiers texte — gérez soigneusement les frontières de lignes.
  • Pour les fichiers binaires, on peut couper comme on veut, sauf spécificités de format.

9. Erreurs courantes avec le chunking

Erreur n°1 : Fichier trop grand. Vous essayez de lire tout le fichier en mémoire — vous obtenez une OutOfMemoryError.

Erreur n°2 : Trop de threads. Vous créez trop de threads — le système commence à « rames » à cause des commutations de contexte.

Erreur n°3 : Lignes coupées. Vous n’avez pas tenu compte des frontières de lignes dans les fichiers texte — vous obtenez des lignes « déchirées » et des erreurs de parsing.

Erreur n°4 : Mauvaise utilisation des méthodes. Vous essayez d’utiliser transferTo/transferFrom pour traiter des données — cela ne marche pas, ces méthodes ne servent qu’à copier.

Erreur n°5 : Oubli de la synchronisation. Vous ne synchronisez pas la collecte des résultats — vous obtenez une somme incorrecte ou d’autres bugs.

Erreur n°6 : Fuite de ressources. Vous ne fermez pas fichiers/canaux — vous créez des fuites de ressources.

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