Storia della questione:
Quando Java è stato lanciato, non esisteva una descrizione formale di come la memoria e i thread interagissero tra loro. Di conseguenza, gli stessi programmi potevano comportarsi in modo diverso su diverse JVM, portando a bug difficili da rilevare. Nel 2004, il Java Memory Model (JMM) è stato aggiunto alle specifiche di Java per definire rigorosamente come i thread vedono gli aggiornamenti delle variabili.
Problema:
Senza un modello di memoria chiaro, le operazioni di scrittura e lettura possono essere mescolate dal compilatore o dal processore, causando comportamenti inaspettati durante l'accesso a variabili condivise da più thread. Questo può causare condizioni di competizione, problemi di visibilità e errori di sincronizzazione difficili da debug.
Soluzione:
Il JMM definisce le regole: quando le modifiche apportate da un thread diventano visibili agli altri. Stabilisce concetti di happens-before, sincronizzatori (synchronized, volatile, final), lock interni e limita la riorganizzazione delle istruzioni in un ambiente multithreading.
Esempio di codice:
class Counter { private int count = 0; public synchronized void increment() { count++; } public synchronized int get() { return count; } }
Caratteristiche chiave:
Perché volatile non rende le operazioni atomiche, ma garantisce solo la visibilità?
Volatile garantisce solo la visibilità delle modifiche tra i thread, ma non l'atomicità della modifica. Ad esempio, l'incremento di una variabile volatile non è un'operazione atomica, poiché l'azione stessa consiste in lettura, modifica e scrittura.
Esempio di codice:
volatile int count = 0; count++; // Non atomico!
Il blocco synchronized può garantire la visibilità delle modifiche al di fuori di esso?
Sì. Dopo l'uscita da un blocco synchronized, tutte le modifiche fatte all'interno sono visibili agli altri thread che entreranno nel blocco sullo stesso oggetto monitor.
Quando diventano visibili agli altri thread le modifiche ai campi final?
I campi final garantiscono una corretta pubblicazione solo se l'oggetto è completamente costruito prima di passare i riferimenti ad altri thread, ad esempio, tramite oggetti immutabili. Altrimenti, potrebbe esserci visibilità di uno stato non inizializzato.
Caso negativo
Un sviluppatore ha deciso di aumentare il contatore degli accessi al sito utilizzando un semplice incremento volatile da più thread.
Vantaggi:
Svantaggi:
Caso positivo
È stato utilizzato AtomicInteger per il contatore o metodi synchronized.
Vantaggi:
Svantaggi: