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
volatilepentru contoare:x++rămâne neatomic. FoloseșteAtomicIntegersausynchronized. - Pui
volatilepe o referință la o listă și crezi că operațiile pe listă sunt sigure. Doar referința e volatilă, nu conținutul. - Pui
volatilepe 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ă.