1. Conoscere l'I/O asincrono
Mettiamo subito in chiaro i termini. Nel classico I/O (sincrono), quando chiami un metodo di lettura o scrittura, il tuo thread di esecuzione (per esempio, il thread principale del programma) si ferma e aspetta finché l'operazione non termina. È come se chiamassi un amico e, finché non risponde, restassi lì ad aspettare fissando il telefono.
L'I/O asincrono (AIO) è quando affidi l'operazione di lettura/scrittura al sistema e continui a lavorare. Quando l'operazione termina, ti “richiamano” (per esempio, viene invocato il tuo metodo di callback oppure il risultato è disponibile tramite Future).
Dove serve?
- Applicazioni server: per non sprecare thread mentre il disco “pensa”.
- Elaborazione massiva di file di grandi dimensioni: per non bloccare il thread principale.
- Applicazioni con interfaccia grafica (UI): per evitare che l'interfaccia “si blocchi” durante la lettura/scrittura.
Immagina di aver ordinato una pizza. In un mondo sincrono rimarresti alla porta ad aspettare il fattorino. In quello asincrono continui a fare le tue cose e, quando la pizza arriva, ti chiamano e dicono: “La pizza è qui!”
2. Panoramica di AsynchronousFileChannel
In Java l'I/O asincrono è implementato nel package java.nio.channels a partire dalla 7ª versione. Il protagonista è la classe AsynchronousFileChannel.
Cosa sa fare?
- Leggere e scrivere dati su file in modo asincrono.
- Lavorare con buffer (ByteBuffer).
- Usare approcci diversi per ottenere il risultato: tramite Future o CompletionHandler.
- Permette di specificare esplicitamente un pool di thread (ExecutorService) per la gestione degli eventi.
Metodi principali
- read(ByteBuffer dst, long position): restituisce Future<Integer>.
- read(ByteBuffer dst, long position, A attachment, CompletionHandler<Integer, ? super A> handler).
- write(ByteBuffer src, long position): restituisce 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) — apre il canale.
Modalità di utilizzo:
- Tramite Future: avvii l'operazione e puoi poi attendere il suo completamento.
- Tramite CompletionHandler: passi un “gestore” che verrà chiamato al termine dell'operazione (o in caso di errore).
Esempio di apertura file per lettura/scrittura asincrona
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
AsynchronousFileChannel channel = AsynchronousFileChannel.open(
Path.of("data.txt"),
StandardOpenOption.READ, StandardOpenOption.WRITE
);
È anche possibile specificare esplicitamente un pool di thread per gestire gli eventi:
import java.util.EnumSet;
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
);
Fatto interessante:
Se non specifichi ExecutorService, Java creerà un proprio pool di thread interno che gestirà gli eventi di I/O. Per compiti semplici è sufficiente, ma per applicazioni server è meglio gestire il pool in modo esplicito.
3. Thread executor (ExecutorService) e il loro ruolo
Quando lavori con un canale asincrono, da qualche parte dietro le quinte Java deve eseguire i tuoi callback o completare il Future. Non è magia: lo fa con thread di lavoro speciali — un executor service.
Se non passi un tuo pool di thread, Java ne crea uno interno — di solito un thread per ciascun processore. Comodo, ma non sempre sicuro. Se invece vuoi controllare tu quanti thread girano, quali attività sono più importanti e come distribuire il carico, è meglio creare un tuo ExecutorService e passarlo a open.
Nelle applicazioni server questo è particolarmente importante. Senza un pool dedicato puoi facilmente incorrere in picchi di carico imprevisti — e invece di un funzionamento fluido il server inizierà ad ansimare.
Esempio:
ExecutorService pool = Executors.newFixedThreadPool(8);
AsynchronousFileChannel channel = AsynchronousFileChannel.open(
Path.of("huge.log"),
EnumSet.of(StandardOpenOption.READ),
pool
);
Impatto della scelta del pool:
- Molti thread — più parallelismo, ma anche maggior carico sul sistema.
- Pochi thread — meno operazioni contemporanee, ma minore overhead.
- Se avvii migliaia di operazioni asincrone, pensa all'equilibrio!
4. Pratica: lettura asincrona di un file
Lettura sincrona (per confronto)
import java.nio.file.Files;
import java.nio.file.Path;
byte[] data = Files.readAllBytes(Path.of("input.txt"));
System.out.println("Byte letti: " + data.length);
Il problema qui è che il thread aspetta finché l'intero file non viene letto. Se il file è grande o il disco è lento, anche il programma comincia a “rallentare” — tutto il resto in quel momento resta in pausa.
Lettura asincrona con AsynchronousFileChannel e 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); // leggiamo 1 KB alla volta
Future<Integer> result = channel.read(buffer, 0);
// Nel frattempo si può fare altro!
System.out.println("Lettura avviata...");
// ... e poi si attende il risultato
int bytesRead = result.get(); // blocca il thread fino al termine dell'operazione
System.out.println("Byte letti: " + bytesRead);
buffer.flip();
// Convertiamo i byte in stringa (se è testo)
byte[] data = new byte[bytesRead];
buffer.get(data, 0, bytesRead);
String text = new String(data);
System.out.println("Contenuto: " + text);
}
}
}
- channel.read(buffer, 0) — avvia la lettura asincrona dalla posizione 0.
- Restituisce un Future<Integer>, che puoi usare per attendere il risultato.
- Finché l'operazione non è terminata, puoi svolgere altre attività.
- result.get() blocca il thread, ma solo se il risultato non è ancora pronto.
Lettura asincrona con CompletionHandler
(Ne parleremo in dettaglio nella prossima lezione, ma per stuzzicare...)
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("Letto in modo asincrono: " + text);
}
@Override
public void failed(Throwable exc, ByteBuffer buf) {
System.err.println("Errore di lettura: " + exc.getMessage());
}
});
// Non lasciamo che il programma termini subito (altrimenti il callback non fa in tempo)
Thread.sleep(100); // Nelle applicazioni reali — meglio sincronizzare con latch, future, ecc.
}
}
}
5. Dettagli utili
Confronto: lettura asincrona vs sincrona
| Caratteristica | I/O sincrono ( Files.readAllBytes ) | I/O asincrono ( AsynchronousFileChannel ) |
|---|---|---|
| Blocca il thread | Sì | No (se non si chiama get()) |
| Scalabilità | Bassa | Alta |
| Adatto per UI/server | No | Sì |
| Complessità del codice | Semplice | Un po' più complesso |
| Gestione delle risorse | Semplice | Importante non dimenticare di chiudere il canale! |
Schema di funzionamento dell'I/O asincrono
sequenceDiagram
participant Main as Il tuo thread
participant OS as Sistema operativo
participant Disk as Disco
Main->>OS: Avvia la lettura asincrona (read)
OS->>Disk: Legge i dati
Main->>Main: Esegue altre attività
OS-->>Main: Notifica il completamento (Future/CompletionHandler)
Main->>Main: Elabora il risultato
6. Errori tipici nell'uso di AsynchronousFileChannel
Errore n. 1: hai dimenticato di chiudere il canale.
AsynchronousFileChannel è una risorsa che va chiusa. Se dimentichi di chiudere il canale (channel.close() o try-with-resources), puoi incorrere in perdite di descrittori e problemi di accesso ai file. Usa sempre try-with-resources quando possibile.
Errore n. 2: get() bloccante nel thread principale.
Se usi Future e chiami get() nel thread principale (per esempio, in un'app UI), annulli il vantaggio dell'I/O asincrono — il thread aspetterà comunque. Usa CompletionHandler o un thread separato per attendere il risultato.
Errore n. 3: utilizzo scorretto di ByteBuffer.
Dopo aver scritto nel buffer, non dimenticare di chiamare flip() per prepararlo alla lettura. Dopo la lettura — clear() o compact(), se intendi riutilizzarlo.
Errore n. 4: hai dimenticato di gestire gli errori.
Le operazioni asincrone possono terminare con errore (per esempio, file non trovato, accesso negato). Se non gestisci le eccezioni in CompletionHandler o non verifichi gli errori nel Future, il programma potrebbe “non eseguire” l'operazione senza avvisi.
Errore n. 5: parallelismo non considerato.
Se avvii più operazioni contemporaneamente sullo stesso canale, assicurati che il tuo codice sia thread-safe e che non si verifichino race condition su buffer o posizioni del file.
GO TO FULL VERSION