CodeGym /Kurse /JAVA 25 SELF /Asynchrones IO: AsynchronousFileChannel (NIO2)

Asynchrones IO: AsynchronousFileChannel (NIO2)

JAVA 25 SELF
Level 56, Lektion 0
Verfügbar

1. Einführung in asynchrones IO

Klären wir gleich die Begriffe. Beim klassischen (synchronen) IO wird, wenn Sie eine Lese- oder Schreibmethode aufrufen, Ihr Ausführungsthread (zum Beispiel der Hauptthread des Programms) angehalten und wartet, bis die Operation abgeschlossen ist. Das ist so, als würden Sie einen Freund anrufen und, bis er abnimmt, einfach dastehen und warten, aufs Telefon starrend.

Asynchrones IO (AIO) bedeutet, dass Sie die Lese-/Schreiboperation dem System übergeben und selbst weiterarbeiten. Wenn die Operation fertig ist – werden Sie „zurückgerufen“ (etwa indem Ihre Callback-Methode aufgerufen wird oder das Ergebnis über ein Future geliefert wird).

Wofür braucht man das?

  • Serveranwendungen: um Threads nicht zu vergeuden, während die Festplatte „nachdenkt“.
  • Massive Verarbeitung großer Dateien: um den Hauptthread nicht zu blockieren.
  • UI-Anwendungen: damit die Oberfläche während Lesen/Schreiben nicht „einfriert“.

Stellen Sie sich vor, Sie haben eine Pizza bestellt. In der synchronen Welt würden Sie an der Tür stehen und auf den Zusteller warten. In der asynchronen – erledigen Sie Ihre Dinge, und wenn die Pizza ankommt, ruft man Sie an und sagt: „Die Pizza ist da!“

2. Überblick über AsynchronousFileChannel

In Java ist asynchrone Ein-/Ausgabe seit Version 7 im Paket java.nio.channels implementiert. Der Hauptdarsteller ist die Klasse AsynchronousFileChannel.

Was kann er?

  • Asynchron Daten in eine Datei lesen und schreiben.
  • Mit Buffern arbeiten (ByteBuffer).
  • Verschiedene Ansätze zur Ergebnisabfrage nutzen: über Future oder über CompletionHandler.
  • Einen Thread-Pool (ExecutorService) für die Ereignisverarbeitung explizit angeben.

Wichtige Methoden

  • read(ByteBuffer dst, long position): gibt Future<Integer> zurück.
  • read(ByteBuffer dst, long position, A attachment, CompletionHandler<Integer, ? super A> handler).
  • write(ByteBuffer src, long position): gibt Future<Integer> zurück.
  • write(ByteBuffer src, long position, A attachment, CompletionHandler<Integer, ? super A> handler).
  • static open(Path file, Set<OpenOption> options, ExecutorService executor, FileAttribute<?>... attrs) – öffnet den Channel.

Arbeitsweisen:

  • Über Future: Sie starten die Operation und können später auf deren Abschluss warten.
  • Über CompletionHandler: Sie übergeben einen „Handler“, der aufgerufen wird, wenn die Operation fertig ist (oder mit einem Fehler endet).

Beispiel: Datei für asynchrones Lesen/Schreiben öffnen

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)
);

Man kann auch explizit einen Thread-Pool für die Ereignisverarbeitung angeben:

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
);

Interessante Tatsache:
Wenn kein ExecutorService angegeben wird, erstellt Java einen eigenen internen Thread-Pool, der IO-Ereignisse bedient. Für einfache Aufgaben reicht das aus, aber für Serveranwendungen ist es besser, den Pool selbst zu steuern.

3. Executor-Threads (ExecutorService) und ihre Rolle

Wenn Sie mit einem asynchronen Channel arbeiten, muss Java im Hintergrund irgendwo Ihre Callbacks ausführen oder ein Future abschließen. Das geschieht nicht durch Magie, sondern mit Hilfe spezieller Worker-Threads – des ExecutorService.

Wenn Sie keinen eigenen Thread-Pool übergeben, erstellt Java einfach einen internen – üblicherweise einen Thread pro Prozessor. Bequem, aber nicht immer optimal. Wenn Sie hingegen selbst steuern möchten, wie viele Threads laufen, welche Aufgaben wichtiger sind und wie die Last verteilt wird, erstellen Sie besser einen eigenen ExecutorService und übergeben ihn an open.

In Serveranwendungen ist das besonders wichtig. Ohne eigenen Thread-Pool kann es leicht zu unerwarteten Lastspitzen kommen – und statt eines sanften Betriebs beginnt der Server zu „schnaufen“.

Beispiel:

ExecutorService pool = Executors.newFixedThreadPool(8);

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

Auswirkung der Pool-Wahl:

  • Viele Threads – mehr Parallelität, aber auch höhere Systemlast.
  • Wenige Threads – weniger gleichzeitige Operationen, aber geringerer Overhead.
  • Wenn Sie Tausende asynchrone Operationen starten, denken Sie an die Balance!

4. Praxis: Asynchrones Lesen einer Datei

Synchrones Lesen (zum Vergleich)

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

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

Das Problem hier ist, dass der Thread einfach wartet, bis die gesamte Datei gelesen ist. Wenn die Datei groß ist oder das Laufwerk langsam, wird das Programm ebenfalls ausgebremst – alles andere pausiert in dieser Zeit.

Asynchrones Lesen mit AsynchronousFileChannel und 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); // wir lesen 1 KB

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

            // In der Zwischenzeit kann man etwas anderes tun!
            System.out.println("Lesen gestartet...");

            // ... und dann auf das Ergebnis warten
            int bytesRead = result.get(); // blockiert den Thread bis zum Abschluss der Operation

            System.out.println("Gelesene Bytes: " + bytesRead);

            buffer.flip();
            // Wir wandeln die Bytes in einen String um (falls es Text ist)
            byte[] data = new byte[bytesRead];
            buffer.get(data, 0, bytesRead);
            String text = new String(data);
            System.out.println("Inhalt: " + text);
        }
    }
}
  • channel.read(buffer, 0) – startet das asynchrone Lesen ab Position 0.
  • Gibt ein Future<Integer> zurück, das man zum Warten auf das Ergebnis verwenden kann.
  • Solange die Operation nicht abgeschlossen ist, können andere Aufgaben ausgeführt werden.
  • result.get() blockiert den Thread, aber nur, wenn das Ergebnis noch nicht bereit ist.

Asynchrones Lesen mit CompletionHandler

(Ausführlicher in der nächsten Vorlesung, hier als Appetithappen ...)

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("Asynchron gelesen: " + text);
                }

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

            // Wir lassen das Programm nicht sofort beenden (sonst hat der Callback keine Zeit)
            Thread.sleep(100); // In realen Anwendungen – besser Synchronisation über Latch, Future usw.
        }
    }
}

5. Nützliche Feinheiten

Vergleich: Asynchrones vs. synchrones Lesen

Merkmal Synchrones IO ( Files.readAllBytes ) Asynchrones IO ( AsynchronousFileChannel )
Blockiert den Thread Ja Nein (wenn get() nicht aufgerufen wird)
Skalierbarkeit Niedrig Hoch
Geeignet für UI/Server Nein Ja
Codekomplexität Einfach Etwas komplexer
Ressourcenverwaltung Einfach Wichtig: Kanal schließen nicht vergessen!

Ablauf des asynchronen IO

sequenceDiagram
    participant Main as Ihr Thread
    participant OS as Betriebssystem
    participant Disk as Festplatte

    Main->>OS: Startet asynchrones Lesen (read)
    OS->>Disk: Liest Daten
    Main->>Main: Führt andere Aufgaben aus
    OS-->>Main: Meldet Abschluss (Future/CompletionHandler)
    Main->>Main: Verarbeitet das Ergebnis

6. Häufige Fehler beim Arbeiten mit AsynchronousFileChannel

Fehler Nr. 1: Kanal nicht geschlossen.
AsynchronousFileChannel ist eine Ressource, die geschlossen werden muss. Wenn Sie vergessen, den Kanal zu schließen (channel.close() oder try-with-resources), kann es zu Handle-Leaks und Problemen beim Dateizugriff kommen. Verwenden Sie try-with-resources immer, wenn möglich.

Fehler Nr. 2: Blockierendes get() im Hauptthread.
Wenn Sie ein Future verwenden und get() im Hauptthread (z. B. in einer UI-Anwendung) aufrufen, verlieren Sie den Sinn des asynchronen IO – der Thread wartet dennoch. Verwenden Sie einen CompletionHandler oder einen separaten Thread zum Warten auf das Ergebnis.

Fehler Nr. 3: Falscher Umgang mit ByteBuffer.
Nach dem Schreiben in den Buffer nicht vergessen, flip() aufzurufen, um ihn fürs Lesen vorzubereiten. Nach dem Lesen – clear() oder compact(), wenn Sie ihn erneut verwenden.

Fehler Nr. 4: Fehlerbehandlung vergessen.
Asynchrone Operationen können mit einem Fehler enden (z. B. Datei nicht gefunden, keine Berechtigung). Wenn Sie Ausnahmen im CompletionHandler nicht behandeln oder das Future nicht auf einen Fehler prüfen, führt das Programm die Operation „stillschweigend“ nicht aus.

Fehler Nr. 5: Nicht berücksichtigte Parallelität.
Wenn Sie mehrere Operationen auf demselben Channel gleichzeitig starten, stellen Sie sicher, dass Ihr Code thread-sicher ist und es keine Race Conditions bei Buffern oder Dateipositionen gibt.

1
Aufgabe
JAVA 25 SELF, Level 56, Lektion 0
Gesperrt
Vorbereitung auf asynchronen Dateneingang 🚀
Vorbereitung auf asynchronen Dateneingang 🚀
1
Aufgabe
JAVA 25 SELF, Level 56, Lektion 0
Gesperrt
Asynchrone Protokollierung eines wichtigen Ereignisses im Log ✍️
Asynchrone Protokollierung eines wichtigen Ereignisses im Log ✍️
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION