CodeGym /Corsi /JAVA 25 SELF /File grandi: pattern di chunking

File grandi: pattern di chunking

JAVA 25 SELF
Livello 41, Lezione 2
Disponibile

1. Introduzione

Nel mondo moderno i dati crescono più in fretta dei funghi dopo la pioggia. A volte bisogna gestire file grandi decine o persino centinaia di gigabyte — possono essere log, dump di database o archivi enormi. Tentare di leggere un file del genere interamente in memoria di solito finisce male: l’applicazione o “divora” tutta la RAM, oppure inizia a funzionare dolorosamente lenta.

Le ragioni sono evidenti. La memoria RAM non è infinita e, se il file supera la sua capacità, rischi un OutOfMemoryError. Anche se la memoria basta, la lettura e l’elaborazione sequenziale di un file enorme in un singolo thread possono durare ore. A ciò si aggiunge il limite del disco stesso: la velocità di lettura è fissa, ma se attivi più thread, soprattutto su SSD, puoi accelerare sensibilmente il processo.

Quindi la conclusione è semplice: i file grandi vanno elaborati a pezzi, i cosiddetti chunk, e, quando possibile, in parallelo. Questo approccio consente di lavorare con gigabyte di dati senza sofferenze inutili.

2. Soluzione: il pattern di chunking

Chunking — è un pattern in cui un file grande viene suddiviso in piccoli blocchi gestibili (chunk), che possono essere elaborati indipendentemente l’uno dall’altro.

Analogia:
Invece di mangiare un’anguria intera in una volta, la tagli a fette e le mangi una alla volta. È più semplice e veloce!

Come funziona?

  1. Determinare la dimensione del file.
    • Con File.length() o Files.size(Path) scopriamo quanti byte contiene il file.
  2. Calcolo della dimensione del chunk (chunk size).
    • Di solito si sceglie 10–20 MB (o più/meno — dipende dal caso d’uso e dall’hardware).
    • È comodo memorizzare la dimensione del chunk nella variabile chunkSize e sceglierla multipla della dimensione del blocco del disco per la massima prestazione.
  3. Creazione della lista di task.
    • Ogni task elabora un chunk: lettura, parsing, cifratura, compressione, ecc.
    • I task possono essere eseguiti in parallelo usando un pool di thread.

Visualizzazione:

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

3. Implementazione dell’elaborazione parallela

Uso di ExecutorService o ForkJoinPool

Per elaborare i chunk in parallelo, usa gli strumenti standard di multithreading di Java:

  • ExecutorService — un pool di thread a dimensione fissa (Executors.newFixedThreadPool(n)).
  • ForkJoinPool — per compiti ricorsivi e per l’approccio “divide et impera”.

Esempio:

ExecutorService pool = Executors.newFixedThreadPool(4); // 4 thread

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

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

Ogni task legge il proprio chunk del file e lo elabora in modo indipendente.

4. Meccanismi chiave: RandomAccessFile e FileChannel

RandomAccessFile

RandomAccessFile permette di “spostarsi” all’interno del file e leggere dalla posizione desiderata.

try (RandomAccessFile raf = new RandomAccessFile(file, "r")) {
    raf.seek(chunkStart); // Ci spostiamo all’inizio del chunk
    byte[] buffer = new byte[chunkSize];
    int bytesRead = raf.read(buffer);
    // Elaboriamo il buffer
}
  • seek(long pos) — sposta il “cursore” alla posizione desiderata.
  • Si può leggere solo l’intervallo di byte necessario.

FileChannel

FileChannel — un metodo più moderno e veloce (soprattutto per file di grandi dimensioni).

try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
    ByteBuffer buffer = ByteBuffer.allocate(chunkSize);
    channel.position(chunkStart);
    int bytesRead = channel.read(buffer);
    // Elaboriamo il buffer
}
  • position(long newPosition) — imposta la posizione di lettura.
  • Si può leggere solo l’intervallo necessario senza toccare il resto del file.

5. Confronto tra chunking e transferTo/transferFrom

transferTo/transferFrom

I metodi FileChannel.transferTo() e transferFrom() consentono di usare il cosiddetto zero‑copy. L’idea è semplice: i dati possono essere copiati o trasferiti direttamente tra file e stream, bypassando i buffer della JVM. Questo rende le operazioni molto veloci. L’unico limite è che i dati non possono essere modificati “al volo”, si possono solo copiare; ma per molti casi questo approccio accelera sensibilmente il lavoro con grandi volumi di informazioni.

Esempio:

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

Chunking

Dunque, il chunking è un modo di lavorare con file grandi per parti, i chunk. Serve non solo per copiare i dati, ma anche per elaborarli: si può fare parsing, cifrare, comprimere o cercare informazioni “in corsa”. Ogni chunk del file può essere elaborato indipendentemente e, volendo, anche in parallelo, il che accelera sensibilmente il lavoro.

L’idea è semplice: se il compito si riduce alla sola copia, meglio usare transferTo o transferFrom, dove i dati si muovono direttamente, in modo rapido e senza copie superflue. Ma se occorre fare qualcosa sul contenuto — cercare, modificare, analizzare — il chunking diventa uno strumento indispensabile.

6. Limitazioni e insidie

Overhead dei thread

  • Creare troppi thread può ridurre le prestazioni (context switch, contesa sulle risorse).
  • Di solito il numero di thread si sceglie pari al numero di core della CPU o poco superiore.

Limiti del disco

  • Anche con 100 thread, il disco non può leggere oltre la sua velocità massima.
  • Su SSD la lettura parallela può dare benefici, su HDD — quasi nessuno.

Necessità di sincronizzazione

  • Se l’elaborazione dei chunk è indipendente — tutto è semplice.
  • Se serve aggregare un risultato globale (per esempio sommare tutti i numeri nel file), occorre sincronizzare l’accesso alle variabili condivise (ad esempio usare AtomicLong o raccogliere i risultati in una lista separata).

Confini dei chunk

  • Se il file è testuale, bisogna fare attenzione a non spezzare una riga o un carattere a metà.
  • Per i file binari (archivi, immagini) — di solito si può tagliare come si vuole.
  • Per i file testuali spesso si crea una “sovrapposizione” tra chunk oppure si cerca il newline più vicino.

7. Esempio: somma parallela dei numeri in un file grande

Obiettivo:
C’è un file con milioni di numeri (uno per riga). Bisogna calcolarne rapidamente la somma.

Piano passo‑passo:

  1. Determiniamo la dimensione del file.
  2. Scegliamo la dimensione del chunk (ad esempio 10 MB).
  3. Per ogni chunk:
    • Troviamo il newline più vicino (per non spezzare un numero).
    • Leggiamo il chunk, facciamo il parsing dei numeri, calcoliamo la somma.
  4. Aggregiamo le somme di tutti i chunk.

Scheletro di codice:

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(() -> {
        // Apriamo RandomAccessFile, cerchiamo i confini del chunk
        // Leggiamo, facciamo il parsing dei numeri, calcoliamo la somma
        long chunkSum = 0L;
        return chunkSum;
    }));
}

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

8. Conclusioni e best practices

  • Chunking — un pattern universale per l’elaborazione di file grandi: si divide in chunk, si elabora indipendentemente, si raccoglie il risultato.
  • Usa RandomAccessFile o FileChannel per leggere dalla posizione desiderata.
  • Per l’elaborazione parallela — ExecutorService o ForkJoinPool.
  • Per la copia senza elaborazione — usa transferTo/transferFrom (zero‑copy).
  • Fai attenzione alla dimensione dei chunk, al numero di thread e ai limiti del disco.
  • Per i file testuali — individua con cura i confini delle righe.
  • Per i file binari si può tagliare come si vuole, salvo specificità del formato.

9. Errori comuni nel lavorare con il chunking

Errore n. 1: file troppo grande. Cerchi di leggere l’intero file in memoria — ottieni OutOfMemoryError.

Errore n. 2: troppi thread. Crei troppi thread — il sistema “rallenta” per via del context switch.

Errore n. 3: righe spezzate. Non consideri i confini delle righe nei file testuali — ottieni righe “strappate” ed errori di parsing.

Errore n. 4: uso scorretto dei metodi. Cerchi di usare transferTo/transferFrom per elaborare i dati — non funziona: questi metodi servono solo per copiare.

Errore n. 5: dimentichi la sincronizzazione. Non sincronizzi la raccolta dei risultati — ottieni una somma scorretta o altri bug.

Errore n. 6: perdita di risorse. Non chiudi file/canali — si verificano perdite di risorse.

1
Compito
JAVA 25 SELF, livello 41, lezione 2
Bloccato
Ricerca di frammenti persi negli archivi 🔍
Ricerca di frammenti persi negli archivi 🔍
1
Compito
JAVA 25 SELF, livello 41, lezione 2
Bloccato
Divisione dei progetti galattici in moduli 🏗️
Divisione dei progetti galattici in moduli 🏗️
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION