CodeGym /Corsi /JAVA 25 SELF /AtomicInteger, AtomicReference: operazioni atomiche

AtomicInteger, AtomicReference: operazioni atomiche

JAVA 25 SELF
Livello 53 , Lezione 3
Disponibile

1. Perché i++ non funziona nel multithreading?

Cominciamo con un problema classico: abbiamo una variabile contatore, ad esempio il numero di richieste elaborate o di file scaricati. Vogliamo che più thread incrementino questo contatore. Cosa può andare storto se scriviamo semplicemente i++?

Esempio: race condition sull'incremento

public class Counter {
    public int count = 0;

    public void increment() {
        count++; // Non atomico!
    }
}

Supponiamo che due thread chiamino contemporaneamente increment(). Entrambi leggono il vecchio valore, entrambi lo incrementano di 1 ed entrambi scrivono… lo stesso nuovo valore! Di conseguenza un incremento «va perso». Se lo si ripete molte volte, il valore finale sarà inferiore a quello atteso.

Perché succede?
L'operazione i++ in realtà consiste in tre passaggi:

  1. Leggere il valore della variabile (per esempio, 5).
  2. Incrementarlo di 1.
  3. Scrivere il nuovo valore in memoria.

In un ambiente multithread un altro thread può riuscire a modificare la variabile tra questi passaggi. Il risultato è una «race condition».

Che cosa sono le operazioni atomiche?

Una operazione atomica è un'azione che o viene eseguita completamente, o non viene eseguita affatto, e nessun altro thread può «infilarsi» nel mezzo di tale operazione.

In Java c'è una serie di classi che fornisce tali operazioni per primitivi e riferimenti. Si trovano nel package java.util.concurrent.atomic. Le più diffuse:

  • AtomicInteger — tipo intero atomico.
  • AtomicLonglong atomico.
  • AtomicBooleanboolean atomico.
  • AtomicReference<T> — riferimento atomico a un oggetto di qualsiasi tipo.

2. AtomicInteger: contatore thread-safe

Dichiarazione e uso di base

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicCounter {
    private final AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        count.incrementAndGet(); // Incremento atomico
    }

    public int get() {
        return count.get();
    }
}

Qui incrementAndGet() esegue «incrementa e restituisci il nuovo valore» come un'operazione indivisibile. Anche se 100 thread chiamano contemporaneamente questo metodo, nessun incremento andrà perso.

Metodi utili:

Metodo Descrizione
get()
Ottenere il valore corrente
set(int value)
Impostare il valore
incrementAndGet()
Incrementare di 1 e restituire il nuovo valore
getAndIncrement()
Restituire il valore corrente e incrementarlo di 1
addAndGet(int delta)
Aumentare di delta e restituire il nuovo valore
compareAndSet(expect, update)
Se il valore corrente è uguale a expect, impostare update (CAS)

Esempio: contatore multithread

Supponiamo di avere una classe che conteggia il numero di messaggi elaborati in una chat.

public class MessageStatistics {
    private final AtomicInteger messageCount = new AtomicInteger(0);

    public void onMessageReceived() {
        int newCount = messageCount.incrementAndGet();
        System.out.println("Messaggi totali: " + newCount);
    }

    public int getMessageCount() {
        return messageCount.get();
    }
}

Interno: come funziona AtomicInteger?

Internamente, AtomicInteger utilizza un'istruzione speciale del processore — CAS (Compare-And-Swap, «confronta e sostituisci»). È un'operazione atomica che confronta il valore corrente della variabile con quello atteso e, se coincidono, scrive il nuovo valore. Se un altro thread è riuscito a modificare la variabile, l'operazione non viene eseguita e si ripete il tentativo.

Schema di funzionamento:

1. Leggiamo il valore corrente (per esempio, 5)
2. Confrontiamo con quello atteso (5)
3. Se coincide — scriviamo il nuovo valore (6)
4. Se non coincide — riproviamo

Tutto ciò avviene molto velocemente e senza blocchi (lock‑free). Per questo le classi atomiche sono spesso più veloci di synchronized, soprattutto con un numero elevato di thread.

3. AtomicReference: riferimento atomico a un oggetto

AtomicReference<T> è un contenitore atomico generico per qualsiasi oggetto. Consente di modificare in modo sicuro il riferimento a un oggetto da thread diversi.

Esempio: aggiornamento thread-safe del riferimento

import java.util.concurrent.atomic.AtomicReference;

public class AtomicReferenceExample {
    private final AtomicReference<String> latestMessage = new AtomicReference<>("");

    public void updateMessage(String message) {
        latestMessage.set(message);
    }

    public String getLatestMessage() {
        return latestMessage.get();
    }
}

Uso di compareAndSet

L'operazione più interessante è compareAndSet(expected, newValue). Consente di aggiornare il valore solo se non è cambiato dall'ultima lettura.

public void safeUpdate(String oldValue, String newValue) {
    boolean success = latestMessage.compareAndSet(oldValue, newValue);
    if (success) {
        System.out.println("Aggiornamento riuscito!");
    } else {
        System.out.println("Qualcuno ha già modificato il valore, riprova.");
    }
}

Questa è la base degli algoritmi non bloccanti: da code e stack fino alle cache, dove è importante evitare blocchi superflui.

4. Esempi d'uso in un'applicazione

Esempio 1: contatore di messaggi multithread

public class ChatRoom {
    private final AtomicInteger messageCount = new AtomicInteger(0);

    public void receiveMessage(String message) {
        // ... elaborazione del messaggio ...
        int count = messageCount.incrementAndGet();
        System.out.println("Nuovo messaggio: " + message + ". Messaggi totali: " + count);
    }
}

Esempio 2: aggiornamento sicuro del riferimento all'ultimo messaggio

public class ChatRoom {
    private final AtomicReference<String> lastMessage = new AtomicReference<>("");

    public void receiveMessage(String message) {
        lastMessage.set(message);
        // ... elaborazione ...
    }

    public String getLastMessage() {
        return lastMessage.get();
    }
}

Se occorre aggiornare il riferimento solo se l'ultimo messaggio non è cambiato (per evitare «perdite» con aggiornamenti simultanei), usa compareAndSet.

5. Limitazioni e insidie

Quando le classi atomiche non sono la panacea?

Le variabili atomiche sono perfette per operazioni semplici: incremento, impostazione del valore, verifica e sostituzione. Ma se è necessario aggiornare più variabili contemporaneamente, l'atomicità non è più garantita. Per esempio, se hai due contatori e vuoi incrementare entrambi come un'unica operazione — qui serve synchronized o un altro meccanismo di sincronizzazione.

Esempio di uso errato

// NON atomico!
if (ref.get() == null) {
    ref.set("Hello");
}

Tra get() e set(...) un altro thread può modificare il valore e la condizione non sarà più vera. Per tali casi usa compareAndSet.

Classi atomiche ≠ oggetti thread-safe

Se l'oggetto a cui punta AtomicReference non è thread-safe, la sostituzione del riferimento sarà atomica, ma la modifica dei campi dell'oggetto — no. Per esempio, se memorizzi in AtomicReference<List<String>> un normale ArrayList, la lista in sé non diventa thread‑safe.

6. Classi atomiche avanzate

Nel package java.util.concurrent.atomic ci sono anche altre classi utili:

  • AtomicLong, AtomicBoolean — per long e boolean.
  • AtomicIntegerArray, AtomicReferenceArray — operazioni atomiche sugli array.
  • LongAdder, LongAccumulator — per contatori ad alto carico.

LongAdder e LongAccumulator

Se hai moltissimi thread e il normale AtomicInteger diventa un «collo di bottiglia» (tutti i thread competono per una sola variabile), usa LongAdder. Suddivide il contatore in più celle interne e le somma alla richiesta del valore, offrendo vantaggio in caso di alta contesa.

import java.util.concurrent.atomic.LongAdder;

public class FastCounter {
    private final LongAdder adder = new LongAdder();

    public void increment() {
        adder.increment();
    }

    public long getCount() {
        return adder.sum();
    }
}

7. Errori tipici nell'uso delle variabili atomiche

Errore n. 1: aspettarsi l'atomicità di operazioni complesse.
Se devi eseguire più azioni su un valore, le classi atomiche non ti salveranno — tra i passaggi un altro thread può modificare i dati. Per operazioni composte usa compareAndSet o la sincronizzazione.

Errore n. 2: ignorare la thread‑safety degli oggetti annidati.
Se in AtomicReference c'è un oggetto normale, i suoi metodi e campi non diventano thread-safe. È atomica solo la sostituzione del riferimento.

Errore n. 3: usare le classi atomiche senza necessità.
Nel codice single-thread i tipi atomici sono ridondanti e leggermente più lenti delle variabili normali a causa di controlli aggiuntivi.

Errore n. 4: ottimizzazione prematura.
A volte è più semplice e affidabile usare synchronized, soprattutto quando la logica è complessa e coinvolge più variabili contemporaneamente. Non sempre vale la pena costruire soluzioni lock‑free.

Errore n. 5: dimenticare il problema ABA.
Caso raro ma importante: il valore cambia da A a B e di nuovo ad A — compareAndSet «pensa» che nulla sia cambiato. Per tali scenari usa classi speciali come AtomicStampedReference (o AtomicMarkableReference).

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