CodeGym /Kursy /JAVA 25 SELF /Duże pliki: wzorce chunkingu

Duże pliki: wzorce chunkingu

JAVA 25 SELF
Poziom 41 , Lekcja 2
Dostępny

1. Wprowadzenie

We współczesnym świecie dane rosną szybciej niż grzyby po deszczu. Czasem trzeba pracować z plikami o rozmiarze dziesiątek lub nawet setek gigabajtów – mogą to być logi, zrzuty baz danych czy ogromne archiwa. Próba wczytania takiego pliku w całości do pamięci zwykle kończy się źle: program albo „zjada” całą pamięć operacyjną, albo zaczyna działać boleśnie wolno.

Powody są oczywiste. Pamięć operacyjna nie jest nieskończona i jeśli plik przekracza jej rozmiar, ryzykujesz OutOfMemoryError. Nawet jeśli pamięci wystarczy, sekwencyjne czytanie i przetwarzanie ogromnego pliku w jednym wątku może ciągnąć się godzinami. Do tego dochodzi ograniczenie samego dysku: ma on stałą prędkość odczytu, ale jeśli uruchomisz kilka wątków, zwłaszcza na SSD, możesz zauważalnie przyspieszyć proces.

Dlatego wniosek jest prosty: duże pliki należy przetwarzać częściami, tak zwanymi chunkami, i w miarę możliwości robić to równolegle. Takie podejście pozwala pracować z gigabajtami danych bez zbędnych męczarni.

2. Rozwiązanie: wzorzec chunkingu

Chunking to wzorzec, w którym duży plik dzieli się na mniejsze, łatwe do zarządzania kawałki (chunks), które można przetwarzać niezależnie od siebie.

Analogią:
Zamiast zjeść naraz cały arbuz, kroisz go na kawałki i jesz po jednym. Jest prościej i szybciej!

Jak to działa?

  1. Określenie rozmiaru pliku.
    • Za pomocą File.length() lub Files.size(Path) sprawdzamy, ile bajtów ma plik.
  2. Wyliczenie rozmiaru kawałka (chunk size).
    • Zwykle wybiera się 10–20 MB (lub więcej/mniej – zależy od zadania i sprzętu).
    • Rozmiar kawałka warto przechowywać w zmiennej chunkSize i dobierać jako wielokrotność rozmiaru bloku dysku dla maksymalnej wydajności.
  3. Utworzenie listy zadań.
    • Każde zadanie to przetworzenie jednego kawałka: odczyt, parsowanie, szyfrowanie, kompresja itd.
    • Zadania można uruchamiać równolegle, używając puli wątków.

Wizualizacja:

+-------------------+
|       Plik        |
+-------------------+
|  [chunk 1]        |
|  [chunk 2]        |
|  [chunk 3]        |
|  ...              |
|  [chunk N]        |
+-------------------+

3. Implementacja przetwarzania równoległego

Użycie ExecutorService lub ForkJoinPool

Aby przetwarzać kawałki równolegle, używaj standardowych narzędzi wielowątkowości Javy:

  • ExecutorService – pula wątków o stałym rozmiarze (Executors.newFixedThreadPool(n)).
  • ForkJoinPool – do zadań rekurencyjnych i podejścia „dziel i rządź”.

Przykład:

ExecutorService pool = Executors.newFixedThreadPool(4); // 4 wątki

for (int i = 0; i < chunkCount; i++) {
    final int chunkIndex = i;
    pool.submit(() -> {
        processChunk(file, chunkIndex, chunkSize);
    });
}

pool.shutdown();
pool.awaitTermination(1, TimeUnit.HOURS);

Każde zadanie czyta swój kawałek pliku i przetwarza go niezależnie.

4. Kluczowe mechanizmy: RandomAccessFile i FileChannel

RandomAccessFile

RandomAccessFile pozwala „poruszać się” po pliku i czytać od wybranej pozycji.

try (RandomAccessFile raf = new RandomAccessFile(file, "r")) {
    raf.seek(chunkStart); // Przechodzimy do początku kawałka
    byte[] buffer = new byte[chunkSize];
    int bytesRead = raf.read(buffer);
    // Przetwarzamy buffer
}
  • seek(long pos) – przesuwa „kursor” do żądanej pozycji.
  • Można czytać tylko potrzebny zakres bajtów.

FileChannel

FileChannel – nowocześniejszy i szybszy sposób (zwłaszcza dla dużych plików).

try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
    ByteBuffer buffer = ByteBuffer.allocate(chunkSize);
    channel.position(chunkStart);
    int bytesRead = channel.read(buffer);
    // Przetwarzamy buffer
}
  • position(long newPosition) – ustawia pozycję do odczytu.
  • Można czytać tylko potrzebny zakres, nie ruszając reszty pliku.

5. Porównanie chunkingu z transferTo/transferFrom

transferTo/transferFrom

Metody FileChannel.transferTo() i transferFrom() pozwalają użyć tak zwanego zero-copy. Idea jest prosta: dane można kopiować lub przenosić między plikami i strumieniami bezpośrednio, z pominięciem buforów JVM. To sprawia, że operacje są bardzo szybkie. Jedno ograniczenie – danych nie można modyfikować „w locie”, można je tylko kopiować, ale w wielu zadaniach takie podejście znacząco przyspiesza pracę z dużymi wolumenami informacji.

Przykład:

try (FileChannel src = FileChannel.open(srcPath, READ);
     FileChannel dst = FileChannel.open(dstPath, WRITE)) {
    src.transferTo(0, src.size(), dst);
}

Chunking

A zatem chunking to sposób pracy z dużymi plikami po częściach, kawałkach (chunks). Służy nie tylko do kopiowania danych, ale także do ich przetwarzania: można parsować, szyfrować, kompresować lub wyszukiwać informacje od razu w trakcie. Każdy kawałek pliku można przetwarzać niezależnie, a w razie potrzeby nawet równolegle, co wyraźnie przyspiesza pracę.

Idea jest prosta: jeśli zadanie sprowadza się do prostego kopiowania, lepiej użyć transferTo lub transferFrom, gdzie dane przemieszczają się bezpośrednio, szybko i bez zbędnych kopii. Ale jeśli trzeba robić coś z treścią – wyszukiwać, modyfikować, analizować – chunking staje się niezastąpionym narzędziem.

6. Ograniczenia i pułapki

Koszty narzutowe wątków

  • Utworzenie zbyt wielu wątków może obniżyć wydajność (przełączanie kontekstu, rywalizacja o zasoby).
  • Zwykle liczbę wątków wybiera się równą liczbie rdzeni procesora lub nieco większą.

Ograniczenia dysku

  • Nawet jeśli masz 100 wątków, dysk i tak nie będzie czytał szybciej niż z maksymalną swoją prędkością.
  • Na SSD równoległy odczyt może dać zysk, na HDD – prawie wcale.

Konieczność synchronizacji

  • Jeśli przetwarzanie kawałków jest niezależne – wszystko jest proste.
  • Jeśli trzeba zebrać wynik globalny (np. policzyć sumę wszystkich liczb w pliku), trzeba zsynchronizować dostęp do współdzielonych zmiennych (np. użyć AtomicLong lub zbierać wyniki na osobnej liście).

Granice kawałków

  • Jeśli plik jest tekstowy, trzeba uważać, by nie przeciąć wiersza lub znaku w połowie.
  • Dla plików binarnych (archiwa, obrazy) – zwykle można ciąć jak się chce.
  • Dla plików tekstowych często stosuje się „nakładkę” kawałków albo wyszukuje najbliższy znak nowej linii.

7. Przykład: równoległe zliczanie sumy liczb w dużym pliku

Zadanie:
Jest plik z milionami liczb (po jednej w wierszu). Trzeba szybko policzyć ich sumę.

Plan krok po kroku:

  1. Określamy rozmiar pliku.
  2. Wybieramy rozmiar kawałka (np. 10 MB).
  3. Dla każdego kawałka:
    • Znajdujemy najbliższy znak nowej linii (aby nie przeciąć liczby).
    • Czytamy kawałek, parsujemy liczby, liczymy sumę.
  4. Zbieramy sumy ze wszystkich kawałków.

Szkielet kodu:

ExecutorService pool = Executors.newFixedThreadPool(4);
List<Future<Long>> results = new ArrayList<>();

for (int i = 0; i < chunkCount; i++) {
    final int chunkIndex = i;
    results.add(pool.submit(() -> {
        // Otwieramy RandomAccessFile, szukamy granic kawałka
        // Czytamy, parsujemy liczby, liczymy sumę
        long chunkSum = 0L;
        return chunkSum;
    }));
}

long total = 0;
for (Future<Long> f : results) {
    total += f.get();
}
pool.shutdown();
System.out.println("Suma: " + total);

8. Podsumowanie i najlepsze praktyki

  • Chunking – uniwersalny wzorzec do przetwarzania dużych plików: dzielimy na kawałki, przetwarzamy niezależnie, zbieramy wynik.
  • Używaj RandomAccessFile lub FileChannel do czytania od potrzebnej pozycji.
  • Do przetwarzania równoległego – ExecutorService lub ForkJoinPool.
  • Do kopiowania bez przetwarzania – używaj transferTo/transferFrom (zero-copy).
  • Pilnuj rozmiaru kawałków, liczby wątków i ograniczeń dysku.
  • Dla plików tekstowych – uważnie wyszukuj granice wierszy.
  • Dla plików binarnych można ciąć dowolnie, jeśli nie ma specyfiki formatu.

9. Typowe błędy przy pracy z chunkingiem

Błąd nr 1: Zbyt duży plik. Próbujesz czytać cały plik do pamięci – dostajesz OutOfMemoryError.

Błąd nr 2: Za dużo wątków. Tworzysz zbyt wiele wątków – system zaczyna „mulić” przez przełączanie kontekstu.

Błąd nr 3: Przecięte wiersze. Nie uwzględniasz granic wierszy w plikach tekstowych – otrzymujesz „porwane” linie i błędy parsowania.

Błąd nr 4: Niewłaściwe użycie metod. Próbujesz użyć transferTo/transferFrom do przetwarzania danych – to nie działa, te metody służą tylko do kopiowania.

Błąd nr 5: Brak synchronizacji. Nie synchronizujesz zbierania wyników – otrzymujesz niepoprawną sumę lub inne błędy.

Błąd nr 6: Wycieki zasobów. Nie zamykasz plików/kanałów – powoduje to wycieki zasobów.

Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION