Java Memory Model

Il Java Memory Model (JMM) definisce le regole che governano come i thread interagiscono attraverso la memoria condivisa, specificando quando le modifiche a variabili condivise diventano visibili ad altri thread. Comprendere il JMM è essenziale per scrivere codice concorrente corretto, evitare race condition e garantire consistency in applicazioni multi-thread.
Fondamenti del Memory Model
Il JMM astrae le complessità dei diversi sistemi hardware e fornisce garanzie uniformi su come la memoria viene gestita across different platforms. Questo modello è cruciale perché i processori moderni utilizzano cache, out-of-order execution e altre ottimizzazioni che possono rendere l’ordine di esecuzione diverso dall’ordine nel codice sorgente.
Problemi della Memoria Condivisa
Visibility: Le modifiche effettuate da un thread potrebbero non essere immediatamente visibili ad altri thread a causa di cache locali e ottimizzazioni del processore.
Reordering: Il compilatore e il processore possono riordinare istruzioni per performance, potenzialmente alterando la semantica del programma in contesti multi-thread.
Atomicity: Operazioni che sembrano atomiche nel codice potrebbero essere implementate come sequence di operazioni più piccole a livello hardware.
public class MemoryModelProblems {
private static boolean flag = false;
private static int value = 0;
// Thread 1 - Writer
public static void writerThread() {
value = 42; // Operazione 1
flag = true; // Operazione 2
}
// Thread 2 - Reader
public static void readerThread() {
if (flag) { // Operazione 3
System.out.println("Value: " + value); // Operazione 4
}
}
// PROBLEMA: Senza sincronizzazione, non c'è garanzia che:
// 1. Le modifiche siano visibili al reader thread
// 2. L'ordine delle operazioni sia preservato
// 3. Il reader veda value = 42 quando flag = true
public static void demonstrateVisibilityProblem() {
Thread writer = new Thread(MemoryModelProblems::writerThread);
Thread reader = new Thread(() -> {
while (true) {
readerThread();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
});
reader.start();
writer.start();
// Il reader potrebbe non vedere mai le modifiche del writer
// oppure vedere flag = true ma value = 0
}
}
Memory Visibility
La visibilità della memoria si riferisce a quando le modifiche effettuate da un thread diventano visibili ad altri thread. Senza sincronizzazione appropriata, non c’è garanzia di visibilità.
public class VisibilityDemo {
// Senza volatile - visibilità non garantita
private static boolean stopRequested = false;
public static void demonstrateVisibilityIssue() {
Thread backgroundThread = new Thread(() -> {
int i = 0;
while (!stopRequested) { // Potrebbe non vedere mai il cambiamento
i++;
}
System.out.println("Background thread stopped after " + i + " iterations");
});
backgroundThread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
stopRequested = true; // Modifica potrebbe non essere visibile
// Il background thread potrebbe continuare indefinitamente
}
// Con volatile - visibilità garantita
private static volatile boolean volatileStopRequested = false;
public static void demonstrateVolatileVisibility() {
Thread backgroundThread = new Thread(() -> {
int i = 0;
while (!volatileStopRequested) { // Vedrà la modifica
i++;
}
System.out.println("Volatile background thread stopped after " + i + " iterations");
});
backgroundThread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
volatileStopRequested = true; // Modifica garantita essere visibile
}
}
Happens-Before Relationship
Il JMM è definito in termini di “happens-before” relationship, che stabilisce quando le azioni di un thread sono guaranteed to be visible to another thread.
Regole Happens-Before
Program Order: All’interno di un singolo thread, ogni azione happens-before ogni azione che appare dopo di essa nel program order.
Monitor Lock: Un unlock di un monitor happens-before ogni successivo lock dello stesso monitor.
Volatile Field: Una scrittura a un campo volatile happens-before ogni lettura successiva dello stesso campo.
Thread Start: Una chiamata a Thread.start() happens-before ogni azione nel thread started.
Thread Termination: Qualsiasi azione in un thread happens-before la detection che quel thread è terminato.
public class HappensBeforeDemo {
private int normalField = 0;
private volatile boolean volatileFlag = false;
// Thread 1 - Stabilisce happens-before relationship
public void writerThread() {
normalField = 42; // Azione 1
volatileFlag = true; // Azione 2 (volatile write)
}
// Thread 2 - Beneficia della happens-before relationship
public void readerThread() {
if (volatileFlag) { // Azione 3 (volatile read)
// Grazie alla happens-before relationship:
// Azione 1 happens-before Azione 2 (program order)
// Azione 2 happens-before Azione 3 (volatile semantics)
// Quindi Azione 1 happens-before Azione 3 (transitivity)
int value = normalField; // Garantito vedere 42
System.out.println("Normal field value: " + value);
}
}
// Synchronization stabilisce happens-before
private final Object lock = new Object();
private int sharedData = 0;
public void synchronizedWriter() {
synchronized (lock) { // Lock acquisition
sharedData = 100; // Modifica all'interno del critical section
} // Lock release - establishes happens-before
}
public void synchronizedReader() {
synchronized (lock) { // Lock acquisition - benefits from happens-before
int value = sharedData; // Garantito vedere l'ultima modifica
System.out.println("Shared data: " + value);
}
}
// Thread start/join happens-before
public static void demonstrateThreadHappensBefore() {
final int[] sharedArray = {0};
Thread worker = new Thread(() -> {
sharedArray[0] = 999; // Azione nel worker thread
});
worker.start(); // start() happens-before worker actions
try {
worker.join(); // join() ensures worker completion
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// Dopo join(), tutte le azioni del worker sono visibili
System.out.println("Shared array value: " + sharedArray[0]); // 999
}
}
Volatile Semantics
La parola chiave volatile fornisce garanzie specifiche di visibilità e ordine, ma non atomicity per operazioni composite.
Garanzie Volatile
Visibility: Le scritture a campi volatile sono immediatamente visibili a tutti i thread.
Ordering: Volatile prevents certain types of reordering around volatile operations.
No Caching: Volatile fields non vengono cached in registri del processore.
public class VolatileSemantics {
private volatile long volatileCounter = 0;
private long normalCounter = 0;
// Volatile garantisce visibilità ma NON atomicity
public void incrementVolatileCounter() {
volatileCounter++; // NON ATOMICO! Equivale a:
// 1. Read volatileCounter
// 2. Add 1
// 3. Write volatileCounter
// Race condition possibile tra step 1 e 3
}
// Uso corretto di volatile per flag
private volatile boolean shutdownRequested = false;
public void requestShutdown() {
shutdownRequested = true; // Atomic write - safe
}
public boolean isShutdownRequested() {
return shutdownRequested; // Atomic read - safe
}
// Volatile per double-checked locking pattern
private volatile ExpensiveObject instance;
public ExpensiveObject getInstance() {
if (instance == null) { // First check (no locking)
synchronized (this) {
if (instance == null) { // Second check (with locking)
instance = new ExpensiveObject(); // Volatile prevents reordering
}
}
}
return instance;
}
// Volatile arrays - solo il reference è volatile
private volatile int[] volatileArray;
public void updateArray() {
int[] newArray = {1, 2, 3, 4, 5};
volatileArray = newArray; // Reference assignment è atomic e volatile
// MA: Modifications agli elementi dell'array NON sono volatile
if (volatileArray != null) {
volatileArray[0] = 999; // Questa modifica NON ha garanzie volatile
}
}
static class ExpensiveObject {
// Simulazione oggetto costoso da creare
public ExpensiveObject() {
try {
Thread.sleep(100); // Simula costruzione costosa
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
Volatile vs Synchronized
public class VolatileVsSynchronized {
private volatile int volatileValue = 0;
private int synchronizedValue = 0;
private final Object lock = new Object();
// Volatile - visibilità garantita, NO atomicity per operazioni composite
public int readVolatile() {
return volatileValue; // Atomic read
}
public void writeVolatile(int value) {
volatileValue = value; // Atomic write
}
public void incrementVolatile() {
volatileValue++; // NON ATOMICO - race condition possible
}
// Synchronized - atomicity E visibilità garantite
public int readSynchronized() {
synchronized (lock) {
return synchronizedValue; // Atomic + visibile
}
}
public void writeSynchronized(int value) {
synchronized (lock) {
synchronizedValue = value; // Atomic + visibile
}
}
public void incrementSynchronized() {
synchronized (lock) {
synchronizedValue++; // ATOMICO - thread-safe
}
}
// Performance comparison
public static void comparePerformance() {
VolatileVsSynchronized demo = new VolatileVsSynchronized();
int iterations = 1_000_000;
// Benchmark volatile operations
long startTime = System.nanoTime();
for (int i = 0; i < iterations; i++) {
demo.writeVolatile(i);
demo.readVolatile();
}
long volatileTime = System.nanoTime() - startTime;
// Benchmark synchronized operations
startTime = System.nanoTime();
for (int i = 0; i < iterations; i++) {
demo.writeSynchronized(i);
demo.readSynchronized();
}
long synchronizedTime = System.nanoTime() - startTime;
System.out.printf("Volatile operations: %.2f ms%n", volatileTime / 1e6);
System.out.printf("Synchronized operations: %.2f ms%n", synchronizedTime / 1e6);
System.out.printf("Synchronized overhead: %.1fx%n",
(double) synchronizedTime / volatileTime);
}
}
Atomic Operations
Le operazioni atomiche forniscono thread-safety per operazioni comuni senza la necessità di synchronization esplicita.
Atomic Classes
import java.util.concurrent.atomic.*;
public class AtomicOperationsDemo {
private final AtomicInteger atomicCounter = new AtomicInteger(0);
private final AtomicLong atomicLong = new AtomicLong(0);
private final AtomicReference<String> atomicString = new AtomicReference<>("initial");
// Atomic increment - thread-safe senza synchronization
public void atomicIncrement() {
int newValue = atomicCounter.incrementAndGet(); // Atomic
System.out.println("New counter value: " + newValue);
}
// Compare-and-swap operations
public boolean tryUpdateString(String expectedValue, String newValue) {
return atomicString.compareAndSet(expectedValue, newValue);
}
// Complex atomic operations
public void complexAtomicUpdate() {
atomicLong.updateAndGet(current -> {
// Complex calculation performed atomically
long result = current * 2 + 1;
return result > 1000 ? 0 : result; // Reset if too large
});
}
// Lock-free data structures with atomic references
static class LockFreeStack<T> {
private final AtomicReference<Node<T>> head = new AtomicReference<>();
private static class Node<T> {
final T data;
final Node<T> next;
Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
}
public void push(T item) {
Node<T> newNode = new Node<>(item, null);
Node<T> currentHead;
do {
currentHead = head.get();
newNode.next = currentHead;
// Retry until successful CAS
} while (!head.compareAndSet(currentHead, newNode));
}
public T pop() {
Node<T> currentHead;
Node<T> newHead;
do {
currentHead = head.get();
if (currentHead == null) {
return null; // Stack empty
}
newHead = currentHead.next;
// Retry until successful CAS
} while (!head.compareAndSet(currentHead, newHead));
return currentHead.data;
}
}
// Atomic field updaters for performance-critical code
private volatile int updatableField = 0;
private static final AtomicIntegerFieldUpdater<AtomicOperationsDemo> FIELD_UPDATER =
AtomicIntegerFieldUpdater.newUpdater(AtomicOperationsDemo.class, "updatableField");
public void updateFieldAtomically() {
FIELD_UPDATER.incrementAndGet(this); // No object allocation
}
// Performance comparison: atomic vs synchronized
public static void compareAtomicVsSynchronized() {
final int iterations = 1_000_000;
final int threadCount = 4;
// Test atomic counter
AtomicInteger atomicCounter = new AtomicInteger(0);
long startTime = System.nanoTime();
Thread[] atomicThreads = new Thread[threadCount];
for (int i = 0; i < threadCount; i++) {
atomicThreads[i] = new Thread(() -> {
for (int j = 0; j < iterations; j++) {
atomicCounter.incrementAndGet();
}
});
}
for (Thread thread : atomicThreads) thread.start();
for (Thread thread : atomicThreads) {
try {
thread.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
long atomicTime = System.nanoTime() - startTime;
// Test synchronized counter
Counter synchronizedCounter = new Counter();
startTime = System.nanoTime();
Thread[] syncThreads = new Thread[threadCount];
for (int i = 0; i < threadCount; i++) {
syncThreads[i] = new Thread(() -> {
for (int j = 0; j < iterations; j++) {
synchronizedCounter.increment();
}
});
}
for (Thread thread : syncThreads) thread.start();
for (Thread thread : syncThreads) {
try {
thread.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
long synchronizedTime = System.nanoTime() - startTime;
System.out.printf("Atomic operations: %.2f ms%n", atomicTime / 1e6);
System.out.printf("Synchronized operations: %.2f ms%n", synchronizedTime / 1e6);
System.out.printf("Atomic speedup: %.1fx%n", (double) synchronizedTime / atomicTime);
}
static class Counter {
private int value = 0;
public synchronized void increment() {
value++;
}
public synchronized int getValue() {
return value;
}
}
}
Final Fields e Immutability
I campi final hanno semantiche speciali nel JMM che garantiscono visibilità sicura per oggetti immutabili.
Final Field Semantics
public class FinalFieldSemantics {
// Oggetto immutabile con final fields
public static final class ImmutablePoint {
private final int x;
private final int y;
private final String label;
public ImmutablePoint(int x, int y, String label) {
this.x = x; // Final field initialization
this.y = y; // Final field initialization
this.label = label; // Final field initialization
}
// Final fields garantiscono che una volta che un riferimento
// a ImmutablePoint è visibile ad un thread, tutti i final fields
// sono guaranteed to be visible con i valori corretti
public int getX() { return x; }
public int getY() { return y; }
public String getLabel() { return label; }
@Override
public String toString() {
return String.format("Point(%d, %d, %s)", x, y, label);
}
}
// Safe publication attraverso final fields
public static class Container {
private final ImmutablePoint point; // Final reference
public Container(int x, int y, String label) {
this.point = new ImmutablePoint(x, y, label);
// Dopo constructor completion, point e tutti i suoi final fields
// sono safely published a tutti i thread
}
public ImmutablePoint getPoint() {
return point; // Safe - nessuna sincronizzazione necessaria
}
}
// ATTENZIONE: Final non è sufficiente per mutable objects
public static class ProblematicContainer {
private final List<String> list; // Final reference a mutable object
public ProblematicContainer() {
this.list = new ArrayList<>();
this.list.add("initial"); // Modifica durante construction
}
public List<String> getList() {
return list; // PROBLEMA: la lista stessa è mutable
}
// Altri thread potrebbero vedere la lista ma non il contenuto
// aggiunto durante construction senza ulteriore sincronizzazione
}
// Soluzione corretta per contenitori immutabili
public static class ImmutableContainer {
private final List<String> list;
public ImmutableContainer(List<String> items) {
// Crea copia immutabile
this.list = List.copyOf(items);
}
public List<String> getList() {
return list; // Safe - lista è immutabile
}
}
// Double-checked locking corretto con final
public static class SafeSingleton {
private static volatile SafeSingleton instance;
private final String data; // Final field
private SafeSingleton() {
this.data = "Singleton data"; // Final field initialization
}
public static SafeSingleton getInstance() {
if (instance == null) {
synchronized (SafeSingleton.class) {
if (instance == null) {
instance = new SafeSingleton();
// Final field semantics garantiscono che quando
// instance diventa visibile, data è safely published
}
}
}
return instance;
}
public String getData() {
return data; // Always safe to read
}
}
}
Safe Publication
Safe publication si riferisce a techniche per rendere oggetti safely visible ad altri threads senza race conditions.
Publication Patterns
import java.util.concurrent.ConcurrentHashMap;
public class SafePublicationPatterns {
// 1. Publication through static initializer
public static final List<String> STATIC_LIST =
List.of("safely", "published", "through", "static", "init");
// 2. Publication through volatile field
private volatile ExpensiveResource volatileResource;
public void publishThroughVolatile() {
ExpensiveResource resource = new ExpensiveResource();
resource.initialize(); // Complete initialization
volatileResource = resource; // Safe publication
}
public ExpensiveResource getVolatileResource() {
return volatileResource; // Safe to read
}
// 3. Publication through synchronized method
private ExpensiveResource synchronizedResource;
private final Object lock = new Object();
public void publishThroughSynchronization() {
ExpensiveResource resource = new ExpensiveResource();
resource.initialize();
synchronized (lock) {
synchronizedResource = resource; // Safe publication
}
}
public ExpensiveResource getSynchronizedResource() {
synchronized (lock) {
return synchronizedResource; // Safe to read
}
}
// 4. Publication through concurrent collections
private final ConcurrentHashMap<String, ExpensiveResource> resourceMap =
new ConcurrentHashMap<>();
public void publishThroughConcurrentCollection(String key) {
ExpensiveResource resource = new ExpensiveResource();
resource.initialize();
resourceMap.put(key, resource); // Safe publication
}
public ExpensiveResource getFromConcurrentCollection(String key) {
return resourceMap.get(key); // Safe to read
}
// 5. Publication through final field
public static class FinalFieldPublication {
private final ExpensiveResource finalResource;
public FinalFieldPublication() {
ExpensiveResource resource = new ExpensiveResource();
resource.initialize();
this.finalResource = resource; // Safe publication through final
}
public ExpensiveResource getResource() {
return finalResource; // Always safe
}
}
// UNSAFE publication examples
public static class UnsafePublication {
private ExpensiveResource unsafeResource; // Not volatile, not synchronized
public void unsafePublish() {
ExpensiveResource resource = new ExpensiveResource();
resource.initialize();
unsafeResource = resource; // UNSAFE! Other threads might see partially constructed object
}
public ExpensiveResource getUnsafeResource() {
return unsafeResource; // UNSAFE! Might return null or partially constructed object
}
}
static class ExpensiveResource {
private String data;
private boolean initialized = false;
public void initialize() {
// Simulate expensive initialization
data = "Expensive data";
initialized = true;
}
public String getData() {
if (!initialized) {
throw new IllegalStateException("Resource not initialized");
}
return data;
}
}
// Thread-safe lazy initialization
public static class LazyInitialization {
private volatile ExpensiveResource resource;
public ExpensiveResource getResource() {
ExpensiveResource result = resource;
if (result == null) {
synchronized (this) {
result = resource;
if (result == null) {
resource = result = new ExpensiveResource();
result.initialize();
}
}
}
return result;
}
}
}
Best Practices
Minimize Shared Mutable State: Preferisci immutable objects e thread-local storage.
Use Appropriate Synchronization: volatile per simple flags, synchronized per compound operations, atomic classes per counters.
Understand Happens-Before: Comprendi le regole di ordinamento per evitare race conditions.
Safe Publication: Usa pattern appropriati per rendere oggetti safely visible.
Avoid Double-Checked Locking Pitfalls: Usa volatile correttamente o considera alternative.
Profile Synchronization Overhead: Misura l’impatto di diverse strategie di sincronizzazione.
Document Concurrency Invariants: Chiarisci quali campi richiedono sincronizzazione e perché.
Conclusione
Il Java Memory Model fornisce le garanzie fondamentali per la programmazione concorrente, ma richiede una comprensione approfondita per essere utilizzato efficacemente. La combinazione di happens-before relationships, volatile semantics, atomic operations e safe publication patterns permette di costruire applicazioni concurrent robuste e performanti.
La chiave del successo è bilanciare correttezza e performance, utilizzando il livello di sincronizzazione appropriato per ogni scenario. Un design attento che minimizza lo stato condiviso mutabile e applica i pattern di sicurezza appropriati risulta in codice concurrent che è sia corretto che efficiente.
L’evoluzione continua della JVM introduce nuove ottimizzazioni e meccanismi di sincronizzazione, ma i principi fondamentali del JMM rimangono stabili, fornendo una base solida per la programmazione concorrente in Java.