CodeGym /Corsi /JAVA 25 SELF /Buone pratiche della programmazione parallela

Buone pratiche della programmazione parallela

JAVA 25 SELF
Livello 54, Lezione 4
Disponibile

1. Non tutto ciò che si può parallelizzare va parallelizzato

In Java ci sono molti modi per parallelizzare i compiti. Ma «parallelismo = sempre più veloce» — è come pensare che aggiungendo altro sale alla zuppa diventi più buona: fino a un certo punto — sì, poi è meglio non provarci.

ExecutorService è perfetto quando avete compiti espliciti da avviare e controllare: gestione di richieste, caricamento asincrono di dati, calcoli indipendenti. Decidete voi quanti thread ci sono nel pool e gestite il ciclo di vita dei task.

parallelStream — un modo rapido per parallelizzare l’elaborazione delle collezioni quando le operazioni sono indipendenti e senza effetti collaterali. Conviene per collezioni «pesanti» (decine di migliaia di elementi e oltre).

ForkJoinPool — la scelta per compiti che si dividono bene in sotto-compiti (divide & conquer): ordinamento, ricerca, aggregazione di grandi array. È usato internamente da parallelStream, ma lo si può gestire anche direttamente.

Non usate il parallelismo «per ogni evenienza». Se il compito è piccolo, i costi di scheduling, context switching e sincronizzazione possono annullare tutto il guadagno.

Esempio: quando il parallelismo non serve

List<Integer> smallList = List.of(1, 2, 3, 4, 5);
int sum = smallList.parallelStream()
    .mapToInt(x -> x)
    .sum(); // Parallelizzare per 5 numeri — overkill!

2. Thread-safety: evitare dati condivisi e mutabili

Nel mondo parallelo la minaccia principale sono le race condition. Se più thread modificano la stessa variabile, il risultato può essere imprevedibile.

  • Evitate variabili condivise e mutabili. Anche un’espressione come counter++ non è atomica.
  • Usate collezioni thread-safe e operazioni atomiche. Per esempio, ConcurrentHashMap, CopyOnWriteArrayList, AtomicInteger, AtomicLong.
  • Niente effetti collaterali negli stream paralleli. Non modificate strutture esterne da parallelStream.

Esempio di cattivo codice

List<Integer> numbers = Arrays.asList(1,2,3,4,5);
List<Integer> result = new ArrayList<>();
numbers.parallelStream().forEach(n -> result.add(n * 2)); // PERICOLOSO!

Qui result.add() non è thread-safe. Il risultato — elementi persi o eccezioni.

Come correttamente?

List<Integer> result = numbers.parallelStream()
    .map(n -> n * 2)
    .collect(Collectors.toList());

3. Prestazioni: più thread non è sempre meglio

Parallelizzare compiti piccoli non conviene. Se il lavoro dura millisecondi, l’avvio in parallelo spesso rallenta l’esecuzione a causa dell’overhead.

Misurate le prestazioni. Per misure rapide va bene System.nanoTime():

long start = System.nanoTime();
// ... il vostro codice ...
long end = System.nanoTime();
System.out.println("Tempo di esecuzione: " + (end - start) + " ns");

Per microbenchmark seri usate JMH (Java Microbenchmark Harness).

Esempio: confronto tra stream sequenziale e parallelo

List<Integer> bigList = IntStream.range(0, 1_000_000)
    .boxed().collect(Collectors.toList());

long t1 = System.nanoTime();
long sum1 = bigList.stream().mapToLong(x -> x).sum();
long t2 = System.nanoTime();
long sum2 = bigList.parallelStream().mapToLong(x -> x).sum();
long t3 = System.nanoTime();

System.out.println("Sequenziale: " + (t2 - t1) / 1_000_000 + " ms");
System.out.println("Parallelo:     " + (t3 - t2) / 1_000_000 + " ms");

Provate sul vostro computer — il vantaggio si nota con collezioni davvero grandi e operazioni pesanti.

4. Gestione degli errori: non ignorate le eccezioni nei thread

Future e gestione delle eccezioni

Se avete lanciato un task tramite ExecutorService.submit(), le eccezioni non verranno «propagate» automaticamente — vanno gestite tramite Future.get():

Future<Integer> future = executor.submit(() -> {
    if (Math.random() > 0.5) throw new RuntimeException("Ops!");
    return 42;
});
try {
    Integer result = future.get(); // può lanciare ExecutionException
} catch (ExecutionException e) {
    System.err.println("Errore nel task: " + e.getCause());
}

ForkJoin e gestione degli errori

In ForkJoinPool le eccezioni vengono «impacchettate» nel task. Alla chiamata di join()/get() riemergeranno:

ForkJoinPool pool = new ForkJoinPool();
RecursiveTask<Integer> task = new MyTask();
try {
    int result = pool.invoke(task);
} catch (Exception e) {
    System.err.println("Errore in ForkJoin: " + e);
}

Non dimenticate di gestire InterruptedException

Molti metodi (per esempio, Future.get(), Thread.sleep()) possono lanciare InterruptedException. Non «inghiottitela» — reagite correttamente: ripristinate il flag di interruzione o terminate il task.

5. Debug e test del codice parallelo

Logging e debug

I bug concorrenti sono insidiosi e spesso si manifestano in modo instabile. Fate logging indicando il thread: Thread.currentThread().getName(). Aiuta a capire chi e quando esegue il codice.

Nei casi complessi usate un debugger con supporto multithreading (ad esempio, IntelliJ IDEA). Dei Thread.sleep() temporanei a volte aiutano a «catturare» una race condition rara.

Test di scenari multithread

Dedicate test separati alle operazioni parallele e usate utility di attesa delle condizioni, ad esempio Awaitility. Eseguite tali test molte volte: alcuni problemi emergono solo alla centesima o millesima esecuzione.

6. Suggerimenti e dettagli utili

Leggibilità e manutenibilità: scrivete codice parallelo comprensibile

  • Documentate. Commentate le parti complesse e la scelta degli strumenti.
  • Usate astrazioni di alto livello. Preferite ExecutorService, parallelStream, ForkJoinPool alla gestione manuale dei thread.
  • Evitate la «magia». Non complicate la sincronizzazione se si può fare più semplice.

Tabella: quando usare quale strumento

Scenario Strumento consigliato
Molti task indipendenti
ExecutorService
Elaborazione di una grande collezione parallelStream o ForkJoin
Compito «divide et impera»
ForkJoinPool + RecursiveTask
Task asincrono semplice
CompletableFuture
Molti task piccoli Stream sequenziale
Task con effetti collaterali Solo collezioni thread-safe!

I «comandamenti» del programmatore parallelo

  • Non create variabili condivise e mutabili — a meno che non siate certi che siano thread-safe.
  • Non parallelizzate per il gusto di farlo: valutate il potenziale guadagno.
  • Non dimenticate di chiudere i pool: shutdown()/shutdownNow().
  • Non usate parallelStream per operazioni con effetti collaterali.
  • Non dimenticate di gestire le eccezioni da Future e ForkJoinTask.
  • Non «inghiottite» InterruptedException — terminate correttamente il task.

7. Errori tipici nella programmazione parallela

Errore n. 1: parallelizzazione di compiti piccoli. Spesso i principianti parallelizzano tutto, anche quando il lavoro dura microsecondi. Risultato — più lento a causa dell’overhead.

Errore n. 2: effetti collaterali negli stream. In parallelStream non si possono modificare variabili o collezioni esterne — incapperete in race condition e bug imprevedibili.

Errore n. 3: ignorare le eccezioni. Se non gestite gli errori da Future.get() o ForkJoinTask, non saprete perché il task non è stato eseguito.

Errore n. 4: shutdown() dimenticato su ExecutorService. Senza una chiusura esplicita, l’applicazione può «bloccarsi» in uscita.

Errore n. 5: uso di collezioni non thread-safe. Scrivere da più thread in un semplice ArrayList — strada sicura verso gli errori.

Errore n. 6: «inghiottire» InterruptedException. Se un thread è stato interrotto, rispettatelo e terminate il lavoro correttamente.

Errore n. 7: logica di sincronizzazione troppo complessa. Blocchi synchronized eccessivi portano a deadlock/livelock. Preferite astrazioni di alto livello.

1
Compito
JAVA 25 SELF, livello 54, lezione 4
Bloccato
Esperimento di magia caotica per duplicare artefatti 🧪
Esperimento di magia caotica per duplicare artefatti 🧪
1
Compito
JAVA 25 SELF, livello 54, lezione 4
Bloccato
Analisi dei rotoli antichi nella biblioteca digitale 📜
Analisi dei rotoli antichi nella biblioteca digitale 📜
1
Sondaggio/quiz
Parallelismo e ForkJoin, livello 54, lezione 4
Non disponibile
Parallelismo e ForkJoin
Parallelismo e ForkJoin
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION