1. Errori con i buffer: ByteBuffer, posizione e limite
I metodi asincroni di lettura e scrittura lavorano con un oggetto ByteBuffer. A differenza dei normali array, il buffer ha un « cursore interno » — posizione (position) e limite (limit), che determinano quali byte verranno letti o scritti. Se si gestiscono male queste proprietà, si può ottenere un risultato diverso da quello atteso o addirittura compromettere la logica.
Come appare nel codice?
Esempio di uso errato:
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer buf) {
// Ops! Stiamo cercando di leggere subito una stringa dal buffer:
String str = new String(buf.array()); // Questo è sbagliato!
// ...
}
// ...
});
Cosa non va?
- Dopo la lettura il buffer è in modalità « scrittura »: position punta alla fine dei dati letti e limit alla dimensione del buffer. Se leggi subito dal buffer, otterrai un mucchio di spazzatura (tutti i 1024 byte, anche se in realtà ne sono stati letti 10).
- La chiamata buf.array() restituisce l'intero array sottostante, non solo la parte letta. Inoltre, per i buffer diretti (allocati tramite ByteBuffer.allocateDirect) array() lancerà UnsupportedOperationException.
Come fare correttamente?
Prima di leggere i dati dal buffer bisogna chiamare buffer.flip() — questo metterà il buffer in modalità « lettura »:
public void completed(Integer result, ByteBuffer buf) {
buf.flip(); // Ora position = 0, limit = numero di byte letti
String str = StandardCharsets.UTF_8.decode(buf).toString();
// ... elaborazione della stringa
}
Riutilizzo del buffer
Se vuoi riutilizzare il buffer per l'operazione successiva, non dimenticare di chiamare buffer.clear() o buffer.compact() dopo aver elaborato i dati:
- clear() — « azzera » completamente i limiti: position=0, limit=capacity(), i dati vecchi sono considerati spazzatura.
- compact() — conserva i byte non letti all'inizio del buffer e lo prepara alla scrittura successiva.
Sfumatura: se result è uguale a -1, è stata raggiunta la fine del file — non ci sarà ulteriore elaborazione.
2. Accesso parallelo: race condition e inconsistenza
AsynchronousFileChannel consente di avviare più operazioni in parallelo. Ma se non controlli questo processo, è facile ottenere dati « corrotti » o un crash dell'applicazione.
Problema 1: lettura simultanea nello stesso buffer
// Due letture simultanee nello stesso buffer
channel.read(buffer, 0, buffer, handler1);
channel.read(buffer, 1024, buffer, handler2);
Entrambe le letture scrivono nello stesso buffer! Se entrambe terminano quasi contemporaneamente, il contenuto del buffer diventerà imprevedibile.
Problema 2: scrittura simultanea nello stesso file
Se due thread scrivono contemporaneamente nella stessa porzione di file, il risultato dipende da quale operazione termina prima. È una classica race condition, che può portare a dati danneggiati.
Come evitarlo?
- Per ogni operazione asincrona usa un buffer separato (ByteBuffer) — i thread non si calpesteranno la memoria a vicenda.
- Non avviare scritture parallele sullo stesso intervallo del file; separa gli offset o sincronizza l'accesso.
- Se è importante un ordine rigoroso, avvia una nuova operazione solo dopo il completamento della precedente — ad esempio da completed(...) di CompletionHandler.
3. Perdite di risorse: canale dimenticato aperto
Un canale asincrono è una risorsa di sistema. Se non lo chiudi (channel.close()), il file rimarrà « occupato » nel sistema, possono verificarsi perdite di memoria e su Windows — anche il file potrebbe essere bloccato per altre applicazioni.
Errore tipico:
AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, ...);
// ... avviata l'operazione
// dimenticato di chiamare channel.close() dopo il completamento di tutte le operazioni!
Come fare correttamente?
Usa try-with-resources e assicurati di attendere il completamento di tutte le operazioni prima di uscire dal blocco:
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) {
// elaborazione...
latch.countDown();
}
@Override public void failed(Throwable ex, ByteBuffer b) {
ex.printStackTrace();
latch.countDown();
}
});
latch.await(); // Attendiamo il completamento dell'operazione asincrona
}
// La chiusura avverrà automaticamente
4. Gestione delle eccezioni: errori ignorati in CompletionHandler
Nel codice asincrono gli errori non « scoppiano » nel thread principale — arrivano al metodo failed(...) dell'interfaccia CompletionHandler. Se non lo implementi o lo lasci vuoto, gli errori semplicemente scompariranno e il programma si comporterà in modo anomalo.
Esempio di errore « invisibile »:
channel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer buf) {
// ... elaborazione del risultato
}
@Override
public void failed(Throwable exc, ByteBuffer buf) {
// Ops, vuoto! Errore perso
}
});
Come fare correttamente?
@Override
public void failed(Throwable exc, ByteBuffer buf) {
System.err.println("Errore durante la lettura del file: " + exc.getMessage());
exc.printStackTrace();
// Eventualmente: chiudere il canale, aggiornare le metriche, notificare l'utente, ecc.
}
5. Perdita di riferimenti a Future/CompletionHandler
Se avvii un'operazione asincrona tramite Future ma dimentichi di conservarne il riferimento, non potrai annullare l'operazione o attendere il suo completamento. Analogamente, se usi CompletionHandler ma non sincronizzi il completamento di tutte le operazioni, il programma potrebbe terminare in anticipo.
Esempio:
channel.read(buffer, 0, buffer, handler); // handler — anonimo, non viene conservato da nessuna parte
// Il programma termina subito, senza attendere la fine della lettura
Come fare correttamente?
- Con Future<Integer>: conserva il riferimento e usa future.get() o future.cancel(true) se necessario.
- Con CompletionHandler: usa meccanismi di sincronizzazione (CountDownLatch, Semaphore) e attendi correttamente il completamento di tutte le operazioni prima di chiudere il canale/il programma.
6. Errori con le codifiche
La lettura e la scrittura di file di testo richiedono una gestione corretta delle codifiche. Se trasformi i byte in stringhe « alla cieca », puoi ottenere caratteri illeggibili o perdere parte dei dati, soprattutto leggendo il file a blocchi.
Problema:
// Leggiamo il file in blocchi da 1024 byte, poi convertiamo in stringa
String chunk = new String(buffer.array(), "UTF-8");
Se una sequenza di caratteri viene spezzata tra due buffer (ad esempio, un byte di un carattere UTF-8 resta alla fine di un buffer e gli altri — all'inizio del successivo), otterrai caratteri non validi o un errore di decodifica.
Come fare correttamente? Usa CharsetDecoder e conserva i « residui » dei caratteri incompleti tra le letture:
CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder()
.onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT);
ByteBuffer byteBuf = ByteBuffer.allocate(4096);
CharBuffer charBuf = CharBuffer.allocate(4096);
// A ogni completed(...):
byteBuf.flip();
CoderResult cr = decoder.decode(byteBuf, charBuf, false); // false — non è la fine dell'input
if (cr.isError()) {
cr.throwException();
}
byteBuf.compact();
charBuf.flip();
String text = charBuf.toString();
charBuf.clear();
// Quando non ci sarà più input:
decoder.flush(charBuf);
7. Terminazione prematura del programma
Le operazioni asincrone vengono eseguite in altri thread. Se il thread principale termina prima che tutte le operazioni siano concluse, il programma si chiuderà senza attendere il risultato.
Esempio:
// Avviamo la lettura asincrona
channel.read(buffer, 0, buffer, handler);
// Il thread principale termina subito — il programma si chiude, l'operazione non fa in tempo a completarsi
Come fare correttamente?
Usa CountDownLatch, Semaphore o almeno Thread.sleep(...) (per le demo), per attendere il completamento di tutte le operazioni:
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(); // Attendiamo il completamento dell'operazione
8. Integrazione non riuscita con ExecutorService
AsynchronousFileChannel consente di specificare un proprio ExecutorService per gestire gli eventi. Se passi un pool con pochi thread o addirittura uno solo, tutte le operazioni verranno eseguite in sequenza, non in parallelo. Se il pool è troppo grande — avrai overhead inutile di cambio di contesto.
Esempio:
ExecutorService executor = Executors.newSingleThreadExecutor();
AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, options, executor);
// Tutte le operazioni asincrone saranno di fatto sincrone!
Come fare correttamente?
- Scegli la dimensione del pool in base al carico reale e al numero di operazioni simultanee.
- Per la maggior parte dei casi vanno bene ForkJoinPool.commonPool() o Executors.newCachedThreadPool().
- Ricorda che l'ExecutorService passato gestisce le chiamate dei callback (completed/failed), non l'I/O su disco in sé.
GO TO FULL VERSION