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
volatile
può 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 x
originariamente 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 synchronized
su 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 x
era 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 x
come volatile
non 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 x
potrebbe 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 volatile
sostanzialmente dice a tutti i thread di eseguire operazioni di lettura e scrittura solo sulla memoria principale. synchronized
dice 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 b
su 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 b
per volatile
garantire 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 volatile
garantisce 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.