tl; dr :
Esistono 3 problemi principali con il multithreading:
1) Condizioni di gara
2) Memoria cache / non aggiornata
3) Ottimizzazione del complier e della CPU
volatilepuò risolvere 2 e 3, ma non può risolvere 1. synchronized/ i blocchi espliciti possono risolvere 1, 2 e 3.
Elaborazione :
1) Considera questo thread un codice non sicuro:
x++;
Sebbene possa sembrare un'operazione, in realtà è 3: leggere il valore corrente di x dalla memoria, aggiungerne 1 e salvarlo in memoria. Se pochi thread tentano di farlo contemporaneamente, il risultato dell'operazione non è definito. Se xoriginariamente era 1, dopo 2 thread che gestivano il codice potrebbe essere 2 e potrebbe essere 3, a seconda di quale thread ha completato quale parte dell'operazione prima che il controllo fosse trasferito all'altro thread. Questa è una forma di condizione di razza .
L'uso synchronizedsu un blocco di codice lo rende atomico , il che significa che lo fa come se le 3 operazioni accadessero contemporaneamente, e non c'è modo per un altro thread di entrare nel mezzo e interferire. Quindi, se xera 1 e 2 filetti cercare di preforme x++che sappiamo , alla fine, sarà uguale a 3. Quindi risolve il problema race condition.
synchronized (this) {
x++; // no problem now
}
Contrassegnare xcome volatilenon rende x++;atomico, quindi non risolve questo problema.
2) Inoltre, i thread hanno il proprio contesto, ovvero possono memorizzare nella cache i valori dalla memoria principale. Ciò significa che alcuni thread possono avere copie di una variabile, ma operano sulla loro copia di lavoro senza condividere il nuovo stato della variabile tra gli altri thread.
Si consideri che in un thread, x = 10;. E un po 'più avanti, in un altro thread, x = 20;. La modifica del valore di xpotrebbe non apparire nel primo thread, poiché l'altro thread ha salvato il nuovo valore nella sua memoria di lavoro, ma non lo ha copiato nella memoria principale. O che lo ha copiato nella memoria principale, ma il primo thread non ha aggiornato la sua copia funzionante. Quindi se ora il primo thread controlla if (x == 20)la risposta sarà false.
Contrassegnare una variabile come volatilesostanzialmente dice a tutti i thread di eseguire operazioni di lettura e scrittura solo sulla memoria principale. synchronizeddice a ogni thread di andare ad aggiornare il loro valore dalla memoria principale quando entrano nel blocco e azzerare il risultato nella memoria principale quando escono dal blocco.
Notare che a differenza delle corse di dati, la memoria non aggiornata non è così facile da (ri) produrre, poiché si verificano comunque scarichi nella memoria principale.
3) Il complier e la CPU possono (senza alcuna forma di sincronizzazione tra thread) trattare tutto il codice come thread singolo. Significa che può guardare un po 'di codice, che è molto significativo in un aspetto multithreading e trattarlo come se fosse a thread singolo, dove non è così significativo. Quindi può guardare un codice e decidere, per motivi di ottimizzazione, di riordinarlo o addirittura rimuoverne parti completamente, se non sa che questo codice è progettato per funzionare su più thread.
Considera il seguente codice:
boolean b = false;
int x = 10;
void threadA() {
x = 20;
b = true;
}
void threadB() {
if (b) {
System.out.println(x);
}
}
Penseresti che threadB potrebbe stampare solo 20 (o non stampare nulla se threadB if-check viene eseguito prima di impostare bsu true), poiché bè impostato su true solo dopo che xè impostato su 20, ma il compilatore / CPU potrebbe decidere di riordinare threadA, in quel caso threadB potrebbe anche stampare 10. Contrassegnare bper volatilegarantire che non venga riordinato (o scartato in alcuni casi). Il che significa che thread B poteva solo stampare 20 (o niente del tutto). Contrassegnare i metodi come sincronizzati otterrà lo stesso risultato. Contrassegnare anche una variabile in quanto volatilegarantisce solo che non venga riordinato, ma tutto ciò che può essere riordinato prima o dopo può essere riordinato, quindi la sincronizzazione può essere più adatta in alcuni scenari.
Si noti che prima di Java 5 New Memory Model, volatile non ha risolto questo problema.