Modello di Memoria Java
Il Java Memory Model: quando le scritture di un thread sono visibili agli altri e come funziona il happens-before.
Il Java Memory Model (JMM) è la parte della specifica del linguaggio che definisce quando un thread ha la garanzia di vedere le scritture di un altro thread. È il manuale di regole dietro a volatile, synchronized e final — e il motivo per cui il codice multithread corretto ha l'aspetto che ha.
Questo capitolo spiega perché il modello esiste, come la relazione happens-before lega tutto insieme, e quale strumento usare quando si ha un problema di visibilità, atomicità o riordinamento.
Perché Esiste il Modello di Memoria
Sull'hardware moderno, il valore che un thread "scrive" in un campo può rimanere in un registro CPU o in una cache locale del core molto prima di raggiungere la memoria principale, e il compilatore è libero di riordinare istruzioni indipendenti. Senza regole, un thread potrebbe impostare un campo mentre un altro thread non vede mai la modifica — o la vede fuori ordine.
Il JMM definisce un'unica garanzia che domina tutto questo: la relazione happens-before. Se l'azione A happens-before l'azione B, allora gli effetti di A sono visibili a B. Tutto il resto — volatile, lock, final, avvio e join dei thread — è solo un modo per creare un arco happens-before.
// Without synchronization, this loop may NEVER terminate:
// the reader thread can cache 'running' forever and miss the write.
static boolean running = true; // plain field — no guarantee
void reader() { while (running) { /* spin */ } } // may hang
void stopper() { running = false; } // may go unseenLa Parola Chiave volatile
Dichiarare un campo volatile fa due cose: ogni lettura va alla memoria principale (visibilità), e una scrittura volatile happens-before ogni successiva lettura volatile dello stesso campo (ordinamento). Non rende atomiche le operazioni composte come count++.
public class Worker {
private volatile boolean running = true; // visible across threads
public void run() {
while (running) { // always sees the latest value
doWork();
}
}
public void stop() {
running = false; // guaranteed visible to run()
}
}Usa volatile per un singolo flag o un riferimento letto da molti thread e scritto da uno solo. Ricorri ad esso quando hai bisogno di visibilità, non di mutua esclusione. Consulta Java volatile per un approfondimento.
Happens-Before: Le Regole Fondamentali
Happens-before è il contratto su cui effettivamente si programma. Questi archi sono quelli che si creano intenzionalmente:
| Regola | Arco happens-before |
|---|---|
| Ordine del programma | Ogni azione in un thread happens-before le azioni successive nello stesso thread |
| Monitor lock | Lo sblocco di un monitor happens-before il successivo lock dello stesso monitor |
| Volatile | Una scrittura in un campo volatile happens-before ogni successiva lettura di esso |
| Avvio thread | thread.start() happens-before qualsiasi azione nel thread avviato |
| Join thread | Tutte le azioni in un thread happen-before la restituzione di un altro thread dal suo join() |
| Campi final | Le scritture del costruttore nei campi final happen-before la pubblicazione dell'oggetto |
// synchronized creates a happens-before edge through the same lock:
synchronized (lock) { shared = compute(); } // unlock here ...
// ... happens-before another thread's:
synchronized (lock) { use(shared); } // ... lock hereAtomicità vs. Visibilità
Questi sono due problemi diversi e richiedono strumenti diversi. volatile risolve la visibilità ma non l'atomicità; synchronized e le classi java.util.concurrent.atomic risolvono entrambi per la sezione che coprono.
| Problema | Sintomo | Soluzione |
|---|---|---|
| Visibilità | Un thread non vede mai un valore aggiornato | volatile, synchronized, final |
| Atomicità | Aggiornamenti persi da x++ sotto contesa | synchronized, AtomicInteger, lock |
| Riordinamento | Le operazioni appaiono fuori ordine | happens-before tramite gli strumenti sopra |
import java.util.concurrent.atomic.AtomicLong;
public class Counter {
private final AtomicLong hits = new AtomicLong();
public void record() { hits.incrementAndGet(); } // atomic + visible
public long total() { return hits.get(); }
}Campi final e Pubblicazione Sicura
Un campo final impostato nel costruttore viene congelato nel momento in cui il costruttore ritorna. Qualsiasi thread che vede un oggetto correttamente costruito (uno il cui riferimento non è trapelato dal costruttore) ha la garanzia di vedere i valori corretti dei suoi campi final — senza bisogno di volatile o lock. Ecco perché gli oggetti immutabili sono intrinsecamente thread-safe.
public final class Point {
private final int x, y; // frozen at construction
public Point(int x, int y) { this.x = x; this.y = y; }
public int x() { return x; }
public int y() { return y; }
}
// Share a Point across threads freely: its final fields are safely published.Un Esempio Autosufficiente
L'esempio eseguibile qui sotto utilizza solo il JDK. Esercita quattro strumenti del modello di memoria in un unico programma: volatile per la visibilità cross-thread, AtomicInteger per il conteggio senza perdita di aggiornamenti, campi final per la pubblicazione sicura, e synchronized per l'accumulazione atomica.
Cosa ricavare dall'esecuzione:
reader saw data = 42dimostra che la scritturavolatilesuflagha pubblicato la scrittura semplice sudata— il reader ha la garanzia di vederla grazie all'arco happens-before.atomic counter = 800000 (expected 800000)mostra cheAtomicInteger.incrementAndGet()non ha perso aggiornamenti con 8 thread che eseguono 100.000 incrementi ciascuno — unint++semplice stamperebbe un numero più piccolo e non deterministico.final config = prod:443dimostra la pubblicazione sicura: i campifinaldiConfigsono corretti senza alcunvolatileo lock.synchronized sum = 10000conferma che i quattro writer (1000+2000+3000+4000) hanno accumulato attraverso lo stesso monitor senza perdite di addizioni.- Ogni riga di output corrisponde a un diverso meccanismo happens-before, eppure si compongono in un unico programma — gli strumenti del JMM sono complementari, non intercambiabili.
Scegliere lo Strumento Giusto
Una guida rapida alle decisioni quando si ricorre a un meccanismo del modello di memoria:
- Un solo writer, molti reader di un flag o riferimento? Usa
volatile. - Contatori, accumulatori o compare-and-set su una singola variabile? Usa le classi atomic — evitano l'overhead dei lock.
- Un aggiornamento in più passi che deve essere tutto-o-niente? Proteggilo con
synchronized(o un lock esplicito). - Condividere stato di sola lettura? Rendilo immutabile con campi
final; consulta Classi immutabili. Non servonovolatileo lock.
Capitoli Correlati
- Java
volatile— la parola chiave di visibilità in profondità. - Java Synchronization — mutua esclusione e monitor lock.
- Atomic Variables — operazioni atomiche lock-free.
finalKeyword e Immutable Classes — pubblicazione sicura per costruzione.