W3docs

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 unseen

La 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:

RegolaArco happens-before
Ordine del programmaOgni azione in un thread happens-before le azioni successive nello stesso thread
Monitor lockLo sblocco di un monitor happens-before il successivo lock dello stesso monitor
VolatileUna scrittura in un campo volatile happens-before ogni successiva lettura di esso
Avvio threadthread.start() happens-before qualsiasi azione nel thread avviato
Join threadTutte le azioni in un thread happen-before la restituzione di un altro thread dal suo join()
Campi finalLe 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 here

Atomicità 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.

ProblemaSintomoSoluzione
VisibilitàUn thread non vede mai un valore aggiornatovolatile, synchronized, final
AtomicitàAggiornamenti persi da x++ sotto contesasynchronized, AtomicInteger, lock
RiordinamentoLe operazioni appaiono fuori ordinehappens-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.

java— editable, runs on the server

Cosa ricavare dall'esecuzione:

  • reader saw data = 42 dimostra che la scrittura volatile su flag ha pubblicato la scrittura semplice su data — il reader ha la garanzia di vederla grazie all'arco happens-before.
  • atomic counter = 800000 (expected 800000) mostra che AtomicInteger.incrementAndGet() non ha perso aggiornamenti con 8 thread che eseguono 100.000 incrementi ciascuno — un int++ semplice stamperebbe un numero più piccolo e non deterministico.
  • final config = prod:443 dimostra la pubblicazione sicura: i campi final di Config sono corretti senza alcun volatile o lock.
  • synchronized sum = 10000 conferma 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 servono volatile o lock.

Capitoli Correlati

Pratica

Pratica
Quale garanzia offre la dichiarazione di un campo come 'volatile' nel Java Memory Model?
Quale garanzia offre la dichiarazione di un campo come 'volatile' nel Java Memory Model?
Was this page helpful?