1. Erreurs avec les buffers : ByteBuffer, position et limite
Les méthodes asynchrones de lecture et d’écriture travaillent avec un objet ByteBuffer. Contrairement aux tableaux ordinaires, un buffer possède un « curseur interne » — la position (position) et la limite (limit), qui déterminent quels octets seront lus ou écrits. Si vous gérez mal ces propriétés, vous risquez d’obtenir un résultat inattendu ou même de casser la logique.
À quoi cela ressemble-t-il dans le code ?
Exemple de mauvaise utilisation :
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer buf) {
// Oups ! Nous essayons de lire la chaîne directement à partir du buffer :
String str = new String(buf.array()); // C'est incorrect !
// ...
}
// ...
});
Quel est le problème ?
- Après la lecture, le buffer est en mode « écriture » : position pointe vers la fin des données lues et limit indique la taille du buffer. Si vous lisez immédiatement depuis le buffer, vous obtiendrez un tas de bruit (les 1024 octets, même si seulement 10 ont réellement été lus).
- L’appel buf.array() renvoie tout le tableau interne, et pas uniquement la partie lue. De plus, pour les buffers directs (allocated via ByteBuffer.allocateDirect), array() lèvera une UnsupportedOperationException.
Comment faire correctement ?
Avant de lire les données depuis le buffer, il faut appeler buffer.flip() — cela bascule le buffer en mode « lecture » :
public void completed(Integer result, ByteBuffer buf) {
buf.flip(); // Désormais position = 0, limit = nombre d’octets lus
String str = StandardCharsets.UTF_8.decode(buf).toString();
// ... traitement de la chaîne
}
Réutilisation du buffer
Si vous souhaitez réutiliser le buffer pour l’opération suivante, n’oubliez pas d’appeler buffer.clear() ou buffer.compact() après le traitement des données :
- clear() — réinitialise complètement les bornes : position=0, limit=capacity(), les anciennes données sont considérées comme du bruit.
- compact() — conserve les octets non lus au début du buffer et le prépare à l’écriture supplémentaire.
Nuance : si result vaut -1, la fin de fichier est atteinte — aucun traitement supplémentaire ne sera effectué.
2. Accès parallèle : conditions de course et incohérences
AsynchronousFileChannel permet de lancer plusieurs opérations en parallèle. Mais si vous ne contrôlez pas ce processus, vous pouvez facilement obtenir des données corrompues ou un plantage du programme.
Problème 1 : lecture simultanée dans un même buffer
// Deux lectures simultanées dans le même buffer
channel.read(buffer, 0, buffer, handler1);
channel.read(buffer, 1024, buffer, handler2);
Les deux lectures écrivent dans le même buffer ! Si elles se terminent presque en même temps, le contenu du buffer sera imprévisible.
Problème 2 : écriture simultanée dans un même fichier
Si deux threads écrivent simultanément dans la même partie d’un fichier, le résultat dépend de l’opération qui se termine en premier. C’est une condition de course (race condition) classique, pouvant conduire à des données corrompues.
Comment l’éviter ?
- Pour chaque opération asynchrone, utilisez un buffer distinct (ByteBuffer) — les threads n’entreront pas en collision en mémoire.
- N’initiez pas des écritures parallèles sur la même plage du fichier ; répartissez les offsets ou synchronisez l’accès.
- Si l’ordre strict est important, lancez une nouvelle opération uniquement après la fin de la précédente — par exemple depuis completed(...) de CompletionHandler.
3. Fuites de ressources : canal oublié non fermé
Un canal asynchrone est une ressource système. Si vous ne le fermez pas (channel.close()), le fichier restera « occupé » dans le système, des fuites de mémoire peuvent survenir et, sous Windows, il pourra même être verrouillé pour les autres programmes.
Erreur typique :
AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, ...);
// ... opération lancée
// oublié d’appeler channel.close() après la fin de toutes les opérations !
Comment faire correctement ?
Utilisez try-with-resources et assurez-vous d’attendre la fin de toutes les opérations avant de sortir du bloc :
CountDownLatch latch = new CountDownLatch(1);
try (AsynchronousFileChannel channel =
AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
ByteBuffer buf = ByteBuffer.allocate(4096);
channel.read(buf, 0, buf, new CompletionHandler<Integer, ByteBuffer>() {
@Override public void completed(Integer r, ByteBuffer b) {
// traitement...
latch.countDown();
}
@Override public void failed(Throwable ex, ByteBuffer b) {
ex.printStackTrace();
latch.countDown();
}
});
latch.await(); // Nous attendons la fin de l’opération asynchrone
}
// La fermeture se fera automatiquement
4. Gestion des exceptions : ignorer les erreurs dans CompletionHandler
Dans le code asynchrone, les erreurs ne « remontent » pas dans le thread principal — elles arrivent dans la méthode failed(...) de l’interface CompletionHandler. Si vous ne l’implémentez pas ou la laissez vide, les erreurs disparaîtront simplement et le programme aura un comportement étrange.
Exemple d’erreur « invisible » :
channel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer buf) {
// ... traitement du résultat
}
@Override
public void failed(Throwable exc, ByteBuffer buf) {
// Oups, vide ! L’erreur a été perdue
}
});
Comment faire correctement ?
@Override
public void failed(Throwable exc, ByteBuffer buf) {
System.err.println("Erreur lors de la lecture du fichier : " + exc.getMessage());
exc.printStackTrace();
// Éventuellement : fermer le canal, mettre à jour les métriques, avertir l’utilisateur, etc.
}
5. Perte de références à Future/CompletionHandler
Si vous lancez une opération asynchrone via un Future mais oubliez d’en conserver la référence, vous ne pourrez ni annuler l’opération ni attendre sa fin. De même, si vous utilisez un CompletionHandler sans synchroniser la fin de toutes les opérations, le programme peut se terminer trop tôt.
Exemple :
channel.read(buffer, 0, buffer, handler); // handler — anonyme, non stocké nulle part
// Le programme s’est terminé immédiatement sans attendre la fin de la lecture
Comment faire correctement ?
- Avec Future<Integer> : conservez la référence et utilisez future.get() ou future.cancel(true) si nécessaire.
- Avec CompletionHandler : appliquez des mécanismes de synchronisation (CountDownLatch, Semaphore) et attendez correctement la fin de toutes les opérations avant de fermer le canal/le programme.
6. Erreurs liées aux encodages
La lecture et l’écriture de fichiers texte exigent une gestion correcte des encodages. Si vous convertissez naïvement des octets en chaînes, vous pouvez obtenir des caractères illisibles ou perdre des données, en particulier lors de la lecture d’un fichier par morceaux.
Problème :
// Nous lisons le fichier par blocs de 1024 octets, puis nous le transformons en chaîne
String chunk = new String(buffer.array(), "UTF-8");
Si un caractère est découpé entre deux buffers (par exemple, un octet d’un caractère UTF-8 reste à la fin d’un buffer et les autres se trouvent au début du suivant), vous obtiendrez des caractères incorrects ou une erreur de décodage.
Comment faire correctement ? Utilisez CharsetDecoder et conservez les « restes » de caractères incomplets entre les lectures :
CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder()
.onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT);
ByteBuffer byteBuf = ByteBuffer.allocate(4096);
CharBuffer charBuf = CharBuffer.allocate(4096);
// À chaque completed(...):
byteBuf.flip();
CoderResult cr = decoder.decode(byteBuf, charBuf, false); // false — ce n’est pas la fin de l’entrée
if (cr.isError()) {
cr.throwException();
}
byteBuf.compact();
charBuf.flip();
String text = charBuf.toString();
charBuf.clear();
// Lorsque l’entrée ne sera plus disponible :
decoder.flush(charBuf);
7. Arrêt prématuré du programme
Les opérations asynchrones s’exécutent dans d’autres threads. Si le thread principal se termine avant toutes les opérations, le programme se fermera sans attendre le résultat.
Exemple :
// Nous lançons une lecture asynchrone
channel.read(buffer, 0, buffer, handler);
// Le thread principal s’est achevé immédiatement — le programme s’est fermé, l’opération n’a pas eu le temps de se terminer
Comment faire correctement ?
Utilisez CountDownLatch, Semaphore ou au moins Thread.sleep(...) (pour la démo) afin d’attendre la fin de toutes les opérations :
CountDownLatch latch = new CountDownLatch(1);
channel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override public void completed(Integer result, ByteBuffer buf) { /* ... */ latch.countDown(); }
@Override public void failed(Throwable exc, ByteBuffer buf) { /* ... */ latch.countDown(); }
});
latch.await(); // Nous attendons la fin de l’opération
8. Mauvaise intégration avec ExecutorService
AsynchronousFileChannel permet de fournir votre propre ExecutorService pour le traitement des événements. Si vous passez un pool avec un faible nombre de threads, voire un seul, toutes les opérations seront exécutées séquentiellement, et non en parallèle. Si le pool est trop grand, vous subirez des surcoûts inutiles de changement de contexte.
Exemple :
ExecutorService executor = Executors.newSingleThreadExecutor();
AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, options, executor);
// Toutes les opérations asynchrones seront en fait séquentielles !
Comment faire correctement ?
- Ajustez la taille du pool en fonction de la charge réelle et du nombre d’opérations simultanées.
- Pour la plupart des tâches, ForkJoinPool.commonPool() ou Executors.newCachedThreadPool() conviennent.
- Rappelez-vous que l’ExecutorService fourni pilote les callbacks (completed/failed), et non l’I/O disque lui‑même.
GO TO FULL VERSION