CodeGym /Cours /JAVA 25 SELF /I/O asynchrone : AsynchronousFileChannel (NIO2)

I/O asynchrone : AsynchronousFileChannel (NIO2)

JAVA 25 SELF
Niveau 56 , Leçon 0
Disponible

1. Introduction à l’I/O asynchrone

Commençons par clarifier les termes. Avec l’I/O classique (synchrone), lorsque vous appelez une méthode de lecture ou d’écriture, votre thread d’exécution (par exemple, le thread principal du programme) s’arrête et attend la fin de l’opération. C’est comme si vous appeliez un ami et, tant qu’il ne décroche pas, vous restiez planté à attendre, les yeux rivés sur le téléphone.

L’I/O asynchrone (AIO), c’est lorsque vous confiez l’opération de lecture/écriture au système et vous continuez à travailler. Quand l’opération se termine, on vous « rappelle » (par exemple, votre méthode de callback est invoquée ou un résultat vous est retourné via un Future).

Dans quels cas est-ce utile ?

  • Applications serveur : pour ne pas gaspiller des threads pendant que le disque « réfléchit ».
  • Traitement massif de gros fichiers : pour ne pas bloquer le thread principal.
  • Applications avec interface utilisateur : pour éviter que l’interface ne « gèle » pendant la lecture/l’écriture.

Imaginez que vous avez commandé une pizza. Dans un monde synchrone, vous resteriez à la porte à attendre le livreur. En asynchrone, vous vaquez à vos occupations et, lorsque la pizza arrive, on vous appelle pour vous dire : « La pizza est là ! »

2. Vue d’ensemble d’AsynchronousFileChannel

En Java, les E/S asynchrones sont implémentées dans le package java.nio.channels à partir de la version 7. Le protagoniste principal est la classe AsynchronousFileChannel.

Ce qu’il sait faire ?

  • Lire et écrire des données dans un fichier de manière asynchrone.
  • Travailler avec des tampons (ByteBuffer).
  • Utiliser différentes approches pour obtenir le résultat : via Future ou via CompletionHandler.
  • Permettre d’indiquer explicitement le pool de threads (ExecutorService) pour le traitement des événements.

Méthodes principales

  • read(ByteBuffer dst, long position) : renvoie Future<Integer>.
  • read(ByteBuffer dst, long position, A attachment, CompletionHandler<Integer, ? super A> handler).
  • write(ByteBuffer src, long position) : renvoie Future<Integer>.
  • write(ByteBuffer src, long position, A attachment, CompletionHandler<Integer, ? super A> handler).
  • static open(Path file, Set<OpenOption> options, ExecutorService executor, FileAttribute<?>... attrs) — ouvre le canal.

Modes de fonctionnement :

  • Via Future : vous lancez l’opération et vous pouvez ensuite attendre son achèvement.
  • Via CompletionHandler : vous fournissez un « gestionnaire », appelé lorsque l’opération se termine (ou échoue).

Exemple d’ouverture de fichier pour lecture/écriture asynchrones

import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.EnumSet;

AsynchronousFileChannel channel = AsynchronousFileChannel.open(
    Path.of("data.txt"),
    EnumSet.of(StandardOpenOption.READ, StandardOpenOption.WRITE)
);

On peut aussi préciser explicitement le pool de threads pour traiter les événements :

import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;

ExecutorService executor = Executors.newFixedThreadPool(4);

AsynchronousFileChannel channel = AsynchronousFileChannel.open(
    Path.of("data.txt"),
    EnumSet.of(StandardOpenOption.READ, StandardOpenOption.WRITE),
    executor
);

Fait intéressant :
Si vous n’indiquez pas d’ExecutorService, Java créera son propre pool de threads interne pour traiter les événements d’I/O. Pour des tâches simples, cela suffit, mais pour des applications serveur, il vaut mieux gérer le pool soi-même.

3. Pools de threads (ExecutorService) et leur rôle

Lorsque vous travaillez avec un canal asynchrone, Java doit, en coulisses, exécuter vos callbacks ou compléter le Future. Elle ne le fait pas par magie, mais à l’aide de threads de travail — un executor service.

Si vous ne passez pas votre propre pool de threads, Java en crée un interne — généralement un thread par processeur. Pratique, mais pas toujours sûr. Si vous voulez contrôler combien de threads tournent, quelles tâches sont prioritaires et comment la charge est répartie, mieux vaut créer votre propre ExecutorService et le transmettre à open.

C’est particulièrement important dans les applications serveur. Sans pool dédié, on peut facilement avoir des pics de charge inattendus — et au lieu d’un fonctionnement fluide, le serveur commence à s’essouffler.

Exemple :

ExecutorService pool = Executors.newFixedThreadPool(8);

AsynchronousFileChannel channel = AsynchronousFileChannel.open(
    Path.of("huge.log"),
    EnumSet.of(StandardOpenOption.READ),
    pool
);

Impact du choix du pool :

  • Beaucoup de threads — plus de parallélisme, mais aussi plus de charge sur le système.
  • Peu de threads — moins d’opérations simultanées, mais moins de surcharge.
  • Si vous lancez des milliers d’opérations asynchrones, pensez à l’équilibre !

4. Pratique : lecture de fichier asynchrone

Lecture synchrone (pour comparaison)

import java.nio.file.Files;
import java.nio.file.Path;

byte[] data = Files.readAllBytes(Path.of("input.txt"));
System.out.println("Octets lus: " + data.length);

Le problème ici, c’est que le thread attend simplement que tout le fichier soit lu. Si le fichier est volumineux ou si le disque est lent, le programme ralentit — tout le reste est alors mis en pause.

Lecture asynchrone avec AsynchronousFileChannel et Future

import java.nio.channels.AsynchronousFileChannel;
import java.nio.ByteBuffer;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.concurrent.Future;

public class AsyncReadExample {
    public static void main(String[] args) throws Exception {
        Path path = Path.of("input.txt");
        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
            ByteBuffer buffer = ByteBuffer.allocate(1024); // lecture par blocs de 1 Ko

            Future<Integer> result = channel.read(buffer, 0);

            // On peut faire autre chose en parallèle !
            System.out.println("Lecture lancée...");

            // ... puis on attend le résultat
            int bytesRead = result.get(); // bloque le thread jusqu’à la fin de l’opération

            System.out.println("Octets lus: " + bytesRead);

            buffer.flip();
            // Convertir les octets en chaîne (si c’est du texte)
            byte[] data = new byte[bytesRead];
            buffer.get(data, 0, bytesRead);
            String text = new String(data);
            System.out.println("Contenu: " + text);
        }
    }
}
  • channel.read(buffer, 0) — démarre une lecture asynchrone à partir de la position 0.
  • Renvoie un Future<Integer> que l’on peut utiliser pour attendre le résultat.
  • Tant que l’opération n’est pas terminée, vous pouvez exécuter d’autres actions.
  • result.get() bloque le thread, mais seulement si le résultat n’est pas encore prêt.

Lecture asynchrone avec CompletionHandler

(Nous verrons cela plus en détail dans le prochain cours, mais pour vous mettre en appétit…)

import java.nio.channels.AsynchronousFileChannel;
import java.nio.ByteBuffer;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.channels.CompletionHandler;

public class AsyncReadWithHandler {
    public static void main(String[] args) throws Exception {
        Path path = Path.of("input.txt");
        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
            ByteBuffer buffer = ByteBuffer.allocate(1024);

            channel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
                @Override
                public void completed(Integer bytesRead, ByteBuffer buf) {
                    buf.flip();
                    byte[] data = new byte[bytesRead];
                    buf.get(data, 0, bytesRead);
                    String text = new String(data);
                    System.out.println("Lu de façon asynchrone: " + text);
                }

                @Override
                public void failed(Throwable exc, ByteBuffer buf) {
                    System.err.println("Erreur de lecture: " + exc.getMessage());
                }
            });

            // Empêcher le programme de se terminer immédiatement (sinon le callback n’aura pas le temps)
            Thread.sleep(100); // Dans les applis réelles — mieux vaut synchroniser via latch, future, etc.
        }
    }
}

5. Points utiles

Comparaison : lecture asynchrone vs lecture synchrone

Caractéristique I/O synchrone ( Files.readAllBytes ) I/O asynchrone ( AsynchronousFileChannel )
Bloque le thread Oui Non (si vous n’appelez pas get())
Scalabilité Faible Élevée
Adapté aux interfaces utilisateur/serveurs Non Oui
Complexité du code Simple Un peu plus complexe
Gestion des ressources Simple Il est important de ne pas oublier de fermer le canal !

Schéma de fonctionnement de l’I/O asynchrone

sequenceDiagram
    participant Main as Votre thread
    participant OS as Système d’exploitation
    participant Disk as Disque

    Main->>OS: Lance une lecture asynchrone (read)
    OS->>Disk: Lit les données
    Main->>Main: Exécute d’autres tâches
    OS-->>Main: Signale la fin (Future/CompletionHandler)
    Main->>Main: Traite le résultat

6. Erreurs courantes avec AsynchronousFileChannel

Erreur n° 1 : oublier de fermer le canal.
AsynchronousFileChannel est une ressource qui doit être fermée. Si vous oubliez de fermer le canal (channel.close() ou try-with-resources), vous risquez des fuites de descripteurs et des problèmes d’accès aux fichiers. Utilisez try-with-resources dès que possible.

Erreur n° 2 : get() bloquant dans le thread principal.
Si vous utilisez un Future et appelez get() dans le thread principal (par exemple, dans une application à interface utilisateur), vous perdez l’intérêt de l’I/O asynchrone — le thread attend quand même. Utilisez CompletionHandler ou un thread séparé pour attendre le résultat.

Erreur n° 3 : mauvaise utilisation de ByteBuffer.
Après avoir écrit dans le tampon, n’oubliez pas d’appeler flip() pour le préparer à la lecture. Après la lecture — clear() ou compact() si vous comptez le réutiliser.

Erreur n° 4 : erreurs non traitées.
Les opérations asynchrones peuvent échouer (par exemple, fichier introuvable, accès refusé). Si vous n’interceptez pas les exceptions dans CompletionHandler ou ne vérifiez pas le Future en cas d’erreur, le programme peut « échouer en silence ».

Erreur n° 5 : parallélisme non maîtrisé.
Si vous lancez plusieurs opérations sur le même canal simultanément, assurez-vous que votre code est thread-safe et qu’il n’y a pas de compétition pour les tampons ou les positions du fichier.

1
Mission
JAVA 25 SELF, niveau 56, leçon 0
Bloqué
Préparation à la réception asynchrone de données 🚀
Préparation à la réception asynchrone de données 🚀
1
Mission
JAVA 25 SELF, niveau 56, leçon 0
Bloqué
Écriture asynchrone d'un événement important dans le journal ✍️
Écriture asynchrone d'un événement important dans le journal ✍️
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION