Java Memory Model

Edoardo Midali
Edoardo Midali

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.