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?
- Określenie rozmiaru pliku.
- Za pomocą File.length() lub Files.size(Path) sprawdzamy, ile bajtów ma plik.
- 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.
- 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:
- Określamy rozmiar pliku.
- Wybieramy rozmiar kawałka (np. 10 MB).
- Dla każdego kawałka:
- Znajdujemy najbliższy znak nowej linii (aby nie przeciąć liczby).
- Czytamy kawałek, parsujemy liczby, liczymy sumę.
- 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.
GO TO FULL VERSION