CodeGym /Corsi /JAVA 25 SELF /synchronized, volatile: sintassi, utilizzo

synchronized, volatile: sintassi, utilizzo

JAVA 25 SELF
Livello 52 , Lezione 1
Disponibile

1. Parola chiave synchronized: perché e come

In Java la parola chiave synchronized — è come un cartello «Occupato!» sulla porta del bagno: finché un thread si trova all’interno della «sezione critica», gli altri attendono educatamente il proprio turno. Solo quando il primo esce, il successivo può entrare ed eseguire il proprio codice.

Sintassi: blocco e metodo

Blocco sincronizzato

synchronized (object) {
    // sezione critica
}
  • object — è qualsiasi oggetto su cui vuoi applicare un lock. Finché un thread esegue questo blocco, gli altri thread che vogliono entrare in un blocco con lo stesso oggetto attendono.

Metodo sincronizzato

public synchronized void increment() {
    // sezione critica
}
  • Qui il lock è posto sull’oggetto stesso (this). Vale a dire che un solo thread alla volta può eseguire qualsiasi metodo sincronizzato di questo oggetto.

Metodo sincronizzato statico

public static synchronized void foo() {
    // sezione critica
}
  • Qui il blocco avviene a livello di classe (ClassName.class), non del singolo oggetto.

Come funziona sotto il cofano

Quando un thread entra in un blocco o metodo sincronizzato, acquisisce il monitor dell’oggetto (o della classe per i metodi statici). Se il monitor è già occupato — il thread aspetta. Non appena il monitor si libera, il thread successivo può entrare.

2. Esempio: incremento di un contatore con e senza sincronizzazione

Senza sincronizzazione

public class Counter {
    private int count = 0;

    public void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}
public class CounterDemo {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("Valore finale: " + counter.getCount());
    }
}

Valore atteso: 2000
Valore reale: può essere inferiore (per esempio, 1995, 1987...), e a ogni esecuzione — la sua «sorpresa».

Perché? Perché l’operazione count++ non è atomica: è suddivisa in tre azioni — leggere il valore, incrementarlo, scriverlo di nuovo. Se due thread lo fanno contemporaneamente, possono «sovrascrivere» il risultato l’uno dell’altro.

Soluzione: synchronized

public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

Ora solo un thread alla volta può eseguire il metodo increment(). Il valore finale sarà sempre 2000.

Alternativa: blocco sincronizzato

public class Counter {
    private int count = 0;

    public void increment() {
        synchronized (this) {
            count++;
        }
    }
}

Il risultato sarà lo stesso. Si può sincronizzare non l’intero metodo, ma solo la parte necessaria.

3. Introduzione al «monitor dell’oggetto»

Il monitor è un lock incorporato in ogni oggetto in Java. Quando scrivi synchronized(object), il thread tenta di «bloccare» quell’oggetto. Se il lock è libero — il thread lo acquisisce, altrimenti aspetta. Non appena il thread esce dal blocco, il lock viene rilasciato.

Importante! Se sincronizzi su oggetti diversi — i thread non si attenderanno a vicenda. Perciò è fondamentale scegliere l’oggetto giusto su cui sincronizzare.

Metodi sincronizzati statici

A volte la risorsa condivisa non è un oggetto, ma qualcosa di comune a tutte le istanze della classe (per esempio, una variabile statica). In tal caso la sincronizzazione deve avvenire a livello di classe.

public class StaticCounter {
    private static int count = 0;

    public static synchronized void increment() {
        count++;
    }

    public static int getCount() {
        return count;
    }
}

È equivalente a:

public static void increment() {
    synchronized (StaticCounter.class) {
        count++;
    }
}

Il monitor è applicato all’oggetto della classe (Class), non alla singola istanza.

4. Parola chiave volatile: che cos’è e a cosa serve

Problema di visibilità tra thread

In Java ogni thread può memorizzare nella cache i valori delle variabili per accelerare l’esecuzione. Ciò significa che se un thread modifica una variabile, un altro thread potrebbe «non accorgersene», continuando a leggere il valore dalla propria cache locale. Questo è particolarmente critico per i flag con cui i thread si segnalano a vicenda.

Come funziona volatile

Se una variabile è dichiarata volatile, significa che:

  • Tutti i thread la leggono e la scrivono sempre nella memoria principale, bypassando la cache.
  • Qualsiasi modifica della variabile diventa immediatamente visibile a tutti i thread.

Ma! Le operazioni con volatile non sono di per sé atomiche (tranne la semplice lettura/scrittura di primitivi come boolean, int ecc.). Se fai qualcosa di più complesso di un’assegnazione — serve la sincronizzazione.

Esempio: flag di terminazione

public class Worker extends Thread {
    private volatile boolean running = true;

    public void run() {
        while (running) {
            // facciamo qualcosa di utile
        }
        System.out.println("Thread terminato");
    }

    public void shutdown() {
        running = false;
    }
}
Worker w = new Worker();
w.start();
// ... dopo un po’ di tempo
w.shutdown();

Senza volatile il thread potrebbe «non notare» la modifica del flag e andare in loop infinito (soprattutto su sistemi multi-core). Con volatile — tutto funziona come dovrebbe.

5. Limiti di volatile: non atomicità

Molti principianti pensano: «Se rendo volatile un int, posso scrivere count++ senza preoccuparmi». Purtroppo non è così:

private volatile int count = 0;

public void increment() {
    count++;
}

Errore! L’operazione count++ non è comunque atomica — sono tre passaggi: (1) leggere, (2) incrementare, (3) scrivere di nuovo. Se due thread leggono contemporaneamente lo stesso valore, lo incrementano e scrivono lo stesso risultato — un incremento andrà perso.

Conclusione: volatile garantisce solo la visibilità delle modifiche, ma non protegge dalle race condition nelle operazioni complesse.

6. Quando usare synchronized e quando — volatile

  • volatile — quando hai un flag semplice (per esempio, boolean) che un thread scrive e un altro legge. Esempio: terminazione di un thread, segnalazione di un evento.
  • synchronized — quando serve garantire l’atomicità di operazioni complesse (per esempio, un incremento, la modifica di più variabili, il lavoro con strutture dati).

Tabella promemoria

Scenario volatile synchronized
Segnalare tra thread
Operazione atomica (incremento)
Più passaggi in una sezione critica
Solo visibilità delle modifiche

7. Errori tipici nell’uso di synchronized e volatile

Errore n° 1: sincronizzazione sull’oggetto sbagliato. Se sincronizzi su una variabile locale o su oggetti diversi in ogni thread — non otterrai alcuna protezione.

Object lock = new Object();
synchronized (lock) {
    // ...
}

Se ogni thread crea il proprio lock — è inutile. Serve un unico punto di sincronizzazione, un oggetto comune a tutti i thread.

Errore n° 2: aspettarsi l’atomicità da volatile. volatile garantisce la visibilità, non l’atomicità. Operazioni come count++ restano non sicure senza sincronizzazione.

Errore n° 3: sincronizzare una porzione di codice troppo grande. Se sincronizzi l’intero metodo mentre serve solo una riga, blocchi inutilmente altri thread e perdi prestazioni. Cerca di ridurre la «sezione critica».

Errore n° 4: dimenticare la sincronizzazione «statica» per i dati statici. Se hai una variabile statica ma sincronizzi su this, non aiuterà. Per i dati statici serve la sincronizzazione a livello di classe: synchronized(ClassName.class).

Errore n° 5: sincronizzare su un literal di stringa. Sincronizzare sulle stringhe è rischioso perché i literal uguali vengono internati dalla JVM. Si può ottenere accidentalmente un lock condiviso tra parti diverse del programma.

Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION