1. Che cos’è un thread di esecuzione (thread)
Il thread come linea di lavoro autonoma
In Java (e nella programmazione in generale) un thread di esecuzione — è una sequenza indipendente di istruzioni che procede in parallelo con altri thread all’interno dello stesso programma. Immaginate una fabbrica: ogni sarta ha il proprio banco e un compito; lavora in modo indipendente, ma tutto confluisce nel risultato comune.
Per impostazione predefinita un programma Java parte con un solo thread — quello che avvia il metodo main. Ma nulla ci impedisce di creare thread aggiuntivi affinché parti diverse del programma vengano eseguite contemporaneamente.
Processi e thread: qual è la differenza?
- Processo — è un’unità di esecuzione «pesante». Ogni processo ha il proprio spazio di memoria, le proprie variabili, le proprie risorse. I processi sono completamente isolati tra loro — se uno «si rompe», gli altri non ne risentono.
- Thread — è un’unità di esecuzione «leggera» all’interno di un processo. Tutti i thread di uno stesso processo condividono memoria e risorse. Ciò significa che possono scambiarsi dati facilmente (e, ahimè, anche intralciarsi altrettanto facilmente).
Analogia:
Un processo è un appartamento separato: ha le proprie mura e i propri inquilini.
I thread sono gli inquilini dentro lo stesso appartamento: ognuno ha le proprie faccende, ma cucina e bagno sono in comune.
Come appare in Java?
Quando avviate un programma, la JVM crea almeno un thread — quello principale (main). Ma potete creare nuovi thread per eseguire i compiti in parallelo.
2. Perché serve il multithreading
Reattività: l’UI non deve «bloccarsi»
Supponiamo che stiate scrivendo un programma grafico — per esempio, un editor di testo. L’utente preme il pulsante «Salva» e voi iniziate a scrivere il file su disco in modo lungo e noioso. Se fate tutto nel thread principale, la finestra dell’app si «congelerà»: l’utente non potrà cliccare nulla, il cursore non si muove, l’interfaccia non risponde. Se invece salvate il file in un thread separato, l’interfaccia resterà reattiva e l’utente potrà persino cambiare idea e chiudere il programma.
Esempio reale:
Aprite il browser e iniziate a scaricare un file grande. Se il browser non usasse i thread, non potreste né aprire una nuova scheda, né scorrere la pagina finché il file non fosse stato scaricato!
Elaborazione dati in parallelo
Supponiamo di avere un elenco di mille file da elaborare (per esempio, ricalcolare gli hash o sostituire del testo). Perché non elaborarli in parallelo? Ogni thread prende il proprio file e ci lavora in modo indipendente, e l’intero lavoro si conclude molto più velocemente.
Esempio:
Un server gestisce le richieste di centinaia di client. Se lo facesse in un solo thread, gli altri client attenderebbero in coda all’infinito. Con i thread, invece, ogni richiesta viene gestita in modo indipendente!
Sfruttare i processori multi-core
I processori moderni non sono un solo «cervello», ma un’intera squadra (core) che può lavorare in parallelo. Se il vostro programma usa un solo thread, gli altri core si annoiano e giocano a Campo minato. Se invece avviate più thread, tutti i core hanno qualcosa da fare e il programma gira più veloce.
Fatto interessante:
Anche il vostro telefono ha più core, e i laptop e i server — decine! Non usarli tutti è come comprare un autobus e andarci in giro da soli.
3. Esempi dalla vita reale
| Ambito | Esempio di multithreading |
|---|---|
| Download di file | Scaricare più file contemporaneamente |
| Interfaccia utente | L’app non si «blocca» durante il caricamento/salvataggio dei dati |
| Server | Gestione di molte richieste di rete in parallelo |
| Giochi | Thread separati per fisica, grafica, musica, AI |
| App di messaggistica | Ricezione dei messaggi, invio dei file, aggiornamento dell’interfaccia |
| Elaborazione video | Elaborazione dei frame in parallelo |
Mini-analogia:
Il cuoco prepara la zuppa, nel frattempo il forno cuoce la torta e il robot aspirapolvere pulisce il pavimento — tutto avviene contemporaneamente e la cena è pronta più in fretta!
4. Potenziali difficoltà del multithreading
Race condition
Quando più thread modificano contemporaneamente la stessa variabile, il risultato può essere imprevedibile. Per esempio, se due thread incrementano nello stesso momento un contatore condiviso, il valore finale può essere errato. Ne parleremo più in dettaglio in una delle prossime lezioni.
Sincronizzazione
Per evitare che i thread si intralcino a vicenda, bisogna trovare modi per «mettersi d’accordo» — chi può modificare i dati e quando. Questo si chiama sincronizzazione. A tal fine esistono parole chiave e costrutti speciali (synchronized, lock, ecc.), di cui parleremo più avanti.
Deadlock (blocco reciproco)
A volte i thread possono «farsi così amici» da restare in attesa l’uno dell’altro all’infinito, bloccando il programma. Questo si chiama deadlock — ed è uno degli errori più insidiosi nella programmazione concorrente.
Debug e test
I bug nei programmi multithread sono molto difficili da catturare: a volte tutto funziona, a volte no. A volte il problema si manifesta solo sul server o presso l’utente, mentre sul vostro computer è tutto perfetto. Questo rende il testing e il debug del codice concorrente una vera e propria quest per lo sviluppatore.
5. Panoramica rapida: com’è un programma multithread
Esempio senza thread:
public class Main {
public static void main(String[] args) {
// Contiamo fino a 5
for (int i = 1; i <= 5; i++) {
System.out.println(i);
}
// Stampiamo le lettere
for (char c = 'A'; c <= 'E'; c++) {
System.out.println(c);
}
}
}
L’output è sempre lo stesso:
1
2
3
4
5
A
B
C
D
E
Esempio con i thread:
public class Main {
public static void main(String[] args) {
Thread numbers = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
System.out.println(i);
try {
Thread.sleep(100); // Aspettiamo un po'
} catch (InterruptedException e) {
// Ignoriamo
}
}
});
Thread letters = new Thread(() -> {
for (char c = 'A'; c <= 'E'; c++) {
System.out.println(c);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// Ignoriamo
}
}
});
numbers.start();
letters.start();
}
}
L’output sarà mescolato:
1
A
2
B
3
C
4
D
5
E
oppure, se i thread «si contendono» la CPU, l’ordine può essere diverso. L’importante — entrambi i cicli procedono in parallelo!
6. Dettagli utili
Schema visivo: come i thread collaborano
+-------------------+ +-------------------+
| Glavnyy potok | | Vtoroy potok |
+-------------------+ +-------------------+
| 1 | 2 | 3 | 4 | 5 | | A | B | C | D | E |
+-------------------+ +-------------------+
| |
| Oba rabotayut |
| odnovremenno |
+-------------------------+
Dove Java usa i thread «sotto il cofano»
- Garbage collection (Garbage Collector) — un thread separato ripulisce gli oggetti non utilizzati.
- Input/Output (I/O) — lettura e scrittura di file, connessioni di rete.
- Server e applicazioni web — ogni richiesta del client è gestita in un thread separato.
- Timer, pianificatori di attività — esecuzione di task pianificati.
7. Errori tipici dei principianti
Errore n. 1: aspettarsi che i thread accelerino sempre il programma.
In realtà, se avete una macchina a singolo processore o organizzate il lavoro in modo errato, il multithreading può solo rallentare l’esecuzione a causa della «confusione» e dei costi di commutazione tra thread.
Errore n. 2: ignorare i problemi di sincronizzazione.
Molti sono sicuri: «Lancerei solo due thread, cosa mai potrebbe andare storto?» Ma se entrambi modificano la stessa variabile, il risultato può essere del tutto inatteso.
Errore n. 3: usare i thread per qualsiasi cosa.
Non ha senso avviare un thread separato per ogni sciocchezza. I thread sono una risorsa, e un numero eccessivo può portare a rallentamenti e perfino al crash del programma.
Errore n. 4: assenza di gestione degli errori.
I thread possono lanciare eccezioni (per esempio, lavorando con file o rete). Se non gestite questi errori, il programma può terminare in modo anomalo o «bloccarsi».
GO TO FULL VERSION