volatile în Java

Fire de execuție Java 1.0 (semantica actuală: Java 5)

volatile pe un câmp garantează că fiecare fir vede imediat ultima valoare scrisă; nu face însă operațiile compuse (x++) atomice.

Sintaxă
private volatile boolean activ = true;

Ce face volatile

Din motive de viteză, procesorul și JVM-ul pot ține o copie a unei variabile într-un registru sau cache, iar un fir poate să nu observe modificarea făcută de alt fir. Un câmp volatile este mereu citit și scris direct în memoria comună: o scriere făcută de un fir este vizibilă imediat celorlalte.

Cazul tipic este un semnal: un fir lucrează cât timp activ este true, iar alt fir îl oprește setând activ = false. Fără volatile, bucla poate să nu vadă niciodată schimbarea.

volatile nu înlocuiește synchronized: contor++ pe un câmp volatil tot pierde incrementări, pentru că citirea și scrierea rămân pași separați. Pentru contoare folosește AtomicInteger. Cuvântul cheie se folosește rar în codul obișnuit; clasele din java.util.concurrent rezolvă de obicei problema mai bine.

Exemple

Exemplul 1: Semnal de oprire între fire

Firul „senzor” citește până când main setează activ = false. Datorită lui volatile, schimbarea este văzută imediat. Numărul exact de citiri diferă de la o rulare la alta, așa că se afișează doar faptul că firul s-a oprit.

public class Main {
    private static volatile boolean activ = true;

    public static void main(String[] args) throws InterruptedException {
        Thread senzor = new Thread(() -> {
            long citiri = 0;
            while (activ) {
                citiri++;
            }
            System.out.println("senzor oprit (a făcut " + (citiri > 0 ? "mai multe" : "zero") + " citiri)");
        });
        senzor.start();

        Thread.sleep(100);
        activ = false;          // vizibil imediat în celălalt fir
        senzor.join();
        System.out.println("firul mai rulează? " + senzor.isAlive());
    }
}

Rezultat

senzor oprit (a făcut mai multe citiri)
firul mai rulează? false

Exemplul 2: volatile nu face x++ atomic

volatil pierde incrementări, deși e volatile. AtomicInteger.incrementAndGet() face citirea și scrierea într-un singur pas și dă mereu 400.000. Rezultatul primului contor diferă la fiecare rulare.

import java.util.concurrent.atomic.AtomicInteger;

public class Main {
    static volatile int volatil = 0;
    static final AtomicInteger atomic = new AtomicInteger();

    public static void main(String[] args) throws InterruptedException {
        Thread[] fire = new Thread[4];
        for (int i = 0; i < fire.length; i++) {
            fire[i] = new Thread(() -> {
                for (int j = 0; j < 100_000; j++) {
                    volatil++;
                    atomic.incrementAndGet();
                }
            });
            fire[i].start();
        }
        for (Thread t : fire) {
            t.join();
        }
        System.out.println("volatile: " + volatil);
        System.out.println("atomic:   " + atomic.get());
    }
}

Rezultat posibil (diferă la fiecare rulare)

volatile: 332749
atomic:   400000

Greșeli frecvente

  • Folosești volatile pentru contoare: x++ rămâne neatomic. Folosește AtomicInteger sau synchronized.
  • Pui volatile pe o referință la o listă și crezi că operațiile pe listă sunt sigure. Doar referința e volatilă, nu conținutul.
  • Pui volatile pe o variabilă locală: modifier volatile not allowed here. Se aplică doar câmpurilor.

Comenzi înrudite

  • synchronized: synchronized lasă un singur fir de execuție odată într-o metodă sau un bloc, ca datele comune să nu fie corupte.
  • Fire de execuție virtuale: Firele virtuale sunt fire de execuție foarte ușoare, gestionate de JVM: poți porni zeci de mii de sarcini care așteaptă fără să blochezi sistemul.
  • Thread: Un fir de execuție: cod care rulează în paralel cu restul programului. start() îl pornește, join() așteaptă să se termine.
  • transient: transient marchează un câmp care nu trebuie salvat când obiectul este serializat; la citire primește valoarea implicită.