CodeGym /Corsi /JAVA 25 SELF /I/O asincrono: AsynchronousFileChannel (NIO2)

I/O asincrono: AsynchronousFileChannel (NIO2)

JAVA 25 SELF
Livello 56, Lezione 0
Disponibile

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.

1
Compito
JAVA 25 SELF, livello 56, lezione 0
Bloccato
Preparazione per la ricezione asincrona dei dati 🚀
Preparazione per la ricezione asincrona dei dati 🚀
1
Compito
JAVA 25 SELF, livello 56, lezione 0
Bloccato
Scrittura asincrona di un evento importante nel registro ✍️
Scrittura asincrona di un evento importante nel registro ✍️
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION