CodeGym /Kursy /JAVA 25 SELF /Asynchroniczne I/O: AsynchronousFileChannel (NIO2)

Asynchroniczne I/O: AsynchronousFileChannel (NIO2)

JAVA 25 SELF
Poziom 56 , Lekcja 0
Dostępny

1. Wprowadzenie do asynchronicznego I/O

Zacznijmy od terminów. W klasycznym (synchronicznym) I/O, gdy wywołujesz metodę odczytu lub zapisu, twój wątek wykonania (np. główny wątek programu) zatrzymuje się i czeka na zakończenie operacji. To podobne do sytuacji, gdy dzwonisz do znajomego i dopóki nie odbierze, stoisz i czekasz, wpatrując się w telefon.

Asynchroniczne I/O (AIO) — to sytuacja, gdy zlecasz operację odczytu/zapisu systemowi, a sam kontynuujesz pracę. Gdy operacja się zakończy — „oddzwonią” do ciebie (np. zostanie wywołana twoja metoda callback lub wynik pojawi się przez Future).

Gdzie to jest potrzebne?

  • Aplikacje serwerowe: aby nie marnować wątków, gdy dysk „myśli”.
  • Masowe przetwarzanie dużych plików: żeby nie blokować głównego wątku.
  • Aplikacje z interfejsem (UI): by interfejs nie „zawieszał się” podczas odczytu/zapisu.

Wyobraź sobie, że zamówiłeś pizzę. W świecie synchronicznym stałbyś przy drzwiach i czekał na dostawcę. W asynchronicznym — robisz swoje, a gdy pizza przyjedzie, zadzwonią i powiedzą: „Pizza jest!”.

2. Przegląd AsynchronousFileChannel

W Javie asynchroniczne wejście/wyjście jest zaimplementowane w pakiecie java.nio.channels od 7 wersji. Główny bohater — klasa AsynchronousFileChannel.

Co potrafi?

  • Asynchronicznie czytać i zapisywać dane do pliku.
  • Pracować z buforami (ByteBuffer).
  • Używać różnych podejść do odbioru wyniku: przez Future lub CompletionHandler.
  • Pozwala jawnie wskazać pulę wątków (ExecutorService) do obsługi zdarzeń.

Najważniejsze metody

  • read(ByteBuffer dst, long position): zwraca Future<Integer>.
  • read(ByteBuffer dst, long position, A attachment, CompletionHandler<Integer, ? super A> handler).
  • write(ByteBuffer src, long position): zwraca 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) — otwiera kanał.

Sposoby użycia:

  • Przez Future: uruchamiasz operację i możesz później poczekać na jej zakończenie.
  • Przez CompletionHandler: przekazujesz „obsługę”, która zostanie wywołana, gdy operacja się zakończy (lub zakończy błędem).

Przykład otwarcia pliku do asynchronicznego odczytu/zapisu

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

Można także jawnie wskazać pulę wątków do obsługi zdarzeń:

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

Ciekawostka:
Jeśli nie wskażesz ExecutorService, Java utworzy własną wewnętrzną pulę wątków, która będzie obsługiwać zdarzenia I/O. Do prostych zadań to wystarczy, ale dla aplikacji serwerowych lepiej zarządzać pulą samodzielnie.

3. ExecutorService i jego rola

Gdy pracujesz z asynchronicznym kanałem, gdzieś za kulisami Java musi wykonać twoje callbacki albo zakończyć Future. Robi to nie magią, lecz przy pomocy specjalnych wątków roboczych — executor service.

Jeśli nie przekazujesz własnej puli wątków, Java po prostu tworzy wewnętrzną — zwykle jeden wątek na każdy procesor. Wygodne, ale nie zawsze bezpieczne. Jeśli jednak chcesz sam decydować, ile wątków działa, które zadania są ważniejsze i jak rozkłada się obciążenie, lepiej utworzyć własny ExecutorService i przekazać go do open.

W aplikacjach serwerowych to szczególnie ważne. Bez własnej puli wątków łatwo o nieoczekiwane skoki obciążenia — zamiast płynnej pracy serwer zacznie się dusić.

Przykład:

ExecutorService pool = Executors.newFixedThreadPool(8);

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

Wpływ wyboru puli:

  • Dużo wątków — większy poziom równoległości, ale też większe obciążenie systemu.
  • Mało wątków — mniej jednoczesnych operacji, ale mniejszy narzut.
  • Jeśli uruchamiasz tysiące operacji asynchronicznych, zadbaj o równowagę!

4. Praktyka: asynchroniczny odczyt pliku

Synchroniczny odczyt (dla porównania)

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

byte[] data = Files.readAllBytes(Path.of("input.txt"));
System.out.println("Przeczytano bajtów: " + data.length);

Problem w tym, że wątek po prostu czeka, aż cały plik zostanie odczytany. Jeśli plik jest duży albo dysk wolny, program też zaczyna „zamulać” — wszystko inne w tym czasie stoi.

Asynchroniczny odczyt z AsynchronousFileChannel i 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); // czytamy po 1 KB

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

            // Można robić coś równolegle!
            System.out.println("Rozpoczęto odczyt...");

            // ... a potem czekamy na wynik
            int bytesRead = result.get(); // blokuje wątek do zakończenia operacji

            System.out.println("Przeczytano bajtów: " + bytesRead);

            buffer.flip();
            // Konwersja bajtów na tekst (jeśli to tekst)
            byte[] data = new byte[bytesRead];
            buffer.get(data, 0, bytesRead);
            String text = new String(data);
            System.out.println("Zawartość: " + text);
        }
    }
}
  • channel.read(buffer, 0) — uruchamia asynchroniczny odczyt z pozycji 0.
  • Zwraca Future<Integer>, którego można użyć do oczekiwania na wynik.
  • Gdy operacja trwa, można wykonywać inne działania.
  • result.get() blokuje wątek, ale tylko jeśli wynik nie jest jeszcze gotowy.

Asynchroniczny odczyt z CompletionHandler

(Szczegóły omówimy w następnej lekcji, a na rozgrzewkę...)

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

                @Override
                public void failed(Throwable exc, ByteBuffer buf) {
                    System.err.println("Błąd odczytu: " + exc.getMessage());
                }
            });

            // Nie pozwalamy programowi zakończyć się od razu (inaczej callback nie zdąży)
            Thread.sleep(100); // W prawdziwych aplikacjach — lepiej synchronizacja przez latch, future itp.
        }
    }
}

5. Przydatne szczegóły

Porównanie: asynchroniczny vs synchroniczny odczyt

Cecha Synchroniczne I/O ( Files.readAllBytes ) Asynchroniczne I/O ( AsynchronousFileChannel )
Blokuje wątek Tak Nie (jeśli nie wywołasz get())
Skalowalność Niska Wysoka
Nadaje się do UI/serwerów Nie Tak
Złożoność kodu Prosta Nieco większa
Zarządzanie zasobami Proste Ważne: nie zapomnij zamknąć kanału!

Schemat działania asynchronicznego I/O

sequenceDiagram
    participant Main as Twój wątek
    participant OS as System operacyjny
    participant Disk as Dysk

    Main->>OS: Uruchamia asynchroniczny odczyt (read)
    OS->>Disk: Czyta dane
    Main->>Main: Wykonuje inne zadania
    OS-->>Main: Informuje o zakończeniu (Future/CompletionHandler)
    Main->>Main: Przetwarza wynik

6. Typowe błędy przy pracy z AsynchronousFileChannel

Błąd nr 1: zapomniano zamknąć kanał.
AsynchronousFileChannel — to zasób, który należy zamykać. Jeśli zapomnisz zamknąć kanał (channel.close() lub try-with-resources), możesz doprowadzić do wycieków deskryptorów i problemów z dostępem do plików. Używaj try-with-resources zawsze, gdy to możliwe.

Błąd nr 2: blokujące get() w głównym wątku.
Jeśli używasz Future i wywołujesz get() w wątku głównym (np. w aplikacji z UI), tracisz sens asynchronicznego I/O — wątek i tak będzie czekał. Użyj CompletionHandler albo osobnego wątku do oczekiwania na wynik.

Błąd nr 3: niepoprawna praca z ByteBuffer.
Po zapisaniu do bufora pamiętaj o flip(), by przygotować go do odczytu. Po odczycie — clear() lub compact(), jeśli zamierzasz użyć go ponownie.

Błąd nr 4: brak obsługi błędów.
Operacje asynchroniczne mogą zakończyć się błędem (np. plik nie znaleziony, brak dostępu). Jeśli nie obsłużysz wyjątków w CompletionHandler lub nie sprawdzisz Future pod kątem błędu, program „po cichu” nie wykona operacji.

Błąd nr 5: nieuwzględniona współbieżność.
Jeśli uruchamiasz kilka operacji na jednym kanale jednocześnie, upewnij się, że twój kod jest bezpieczny dla wątków i nie dochodzi do wyścigu o bufory lub pozycje w pliku.

1
Zadanie
JAVA 25 SELF, poziom 56, lekcja 0
Niedostępne
Przygotowanie do asynchronicznego odbioru danych 🚀
Przygotowanie do asynchronicznego odbioru danych 🚀
1
Zadanie
JAVA 25 SELF, poziom 56, lekcja 0
Niedostępne
Asynchroniczne zapisywanie ważnego zdarzenia do dziennika ✍️
Asynchroniczne zapisywanie ważnego zdarzenia do dziennika ✍️
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION