Stai specificatamente chiedendo come funzionano internamente , quindi eccoti qui:
Nessuna sincronizzazione
private int counter;
public int getNextUniqueIndex() {
return counter++;
}
Praticamente legge il valore dalla memoria, lo incrementa e lo rimette in memoria. Funziona su un singolo thread ma al giorno d'oggi, nell'era delle cache multi-core, multi-CPU e multi-livello, non funzionerà correttamente. Innanzitutto introduce le condizioni di gara (diversi thread possono leggere il valore contemporaneamente), ma anche problemi di visibilità. Il valore potrebbe essere memorizzato solo nella memoria della CPU " locale " (alcune cache) e non essere visibile per altre CPU / core (e quindi - thread). Questo è il motivo per cui molti fanno riferimento alla copia locale di una variabile in un thread. È molto pericoloso. Considera questo codice di interruzione thread popolare ma rotto:
private boolean stopped;
public void run() {
while(!stopped) {
//do some work
}
}
public void pleaseStop() {
stopped = true;
}
Aggiungi volatile
alla stopped
variabile e funziona bene - se qualsiasi altro thread modifica la stopped
variabile tramite il pleaseStop()
metodo, sei sicuro di vedere immediatamente quel cambiamento nel while(!stopped)
ciclo del thread di lavoro . A proposito, questo non è un buon modo per interrompere un thread, vedi: Come fermare un thread che è in esecuzione per sempre senza alcun uso e Arrestare un thread Java specifico .
AtomicInteger
private AtomicInteger counter = new AtomicInteger();
public int getNextUniqueIndex() {
return counter.getAndIncrement();
}
La AtomicInteger
classe utilizza operazioni CPU di basso livello CAS ( confronta e scambia ) (non è necessaria alcuna sincronizzazione!) Consentono di modificare una particolare variabile solo se il valore attuale è uguale a qualcos'altro (e viene restituito correttamente). Quindi, quando lo esegui, getAndIncrement()
viene effettivamente eseguito in un ciclo (implementazione reale semplificata):
int current;
do {
current = get();
} while(!compareAndSet(current, current + 1));
Quindi in sostanza: leggi; prova a memorizzare il valore incrementato; in caso contrario (il valore non è più uguale a current
), leggi e riprova. Il compareAndSet()
è implementato nel codice nativo (assembly).
volatile
senza sincronizzazione
private volatile int counter;
public int getNextUniqueIndex() {
return counter++;
}
Questo codice non è corretto Risolve il problema di visibilità (si volatile
assicura che altri thread possano vedere le modifiche apportate counter
) ma ha ancora una condizione di competizione. Questo è stato spiegato più volte: pre / post-incremento non è atomico.
L'unico effetto collaterale di volatile
" svuotare " le cache in modo che tutte le altre parti vedano la versione più recente dei dati. Questo è troppo severo nella maggior parte delle situazioni; ecco perché volatile
non è predefinito.
volatile
senza sincronizzazione (2)
volatile int i = 0;
void incIBy5() {
i += 5;
}
Lo stesso problema di cui sopra, ma ancora peggio perché i
non lo è private
. Le condizioni di gara sono ancora presenti. Perché è un problema? Se, diciamo, due thread eseguono questo codice contemporaneamente, l'output potrebbe essere + 5
o + 10
. Tuttavia, sei sicuro di vedere il cambiamento.
Indipendente multiplo synchronized
void incIBy5() {
int temp;
synchronized(i) { temp = i }
synchronized(i) { i = temp + 5 }
}
Sorpresa, anche questo codice non è corretto. In realtà, è completamente sbagliato. Prima di tutto ti stai sincronizzando i
, che sta per essere cambiato (inoltre, i
è una primitiva, quindi immagino che ti stai sincronizzando su un temporaneo Integer
creato tramite autoboxing ...) Completamente imperfetto. Puoi anche scrivere:
synchronized(new Object()) {
//thread-safe, SRSLy?
}
Non è possibile inserire due thread nello stesso synchronized
blocco con lo stesso blocco . In questo caso (e similmente nel tuo codice) l'oggetto lock cambia ad ogni esecuzione, quindi synchronized
efficacemente non ha alcun effetto.
Anche se hai utilizzato una variabile finale (o this
) per la sincronizzazione, il codice è ancora errato. Due thread possono prima leggere i
in modo temp
sincrono (avendo lo stesso valore localmente in temp
), quindi il primo assegna un nuovo valore a i
(diciamo, da 1 a 6) e l'altro fa la stessa cosa (da 1 a 6).
La sincronizzazione deve andare dalla lettura all'assegnazione di un valore. La tua prima sincronizzazione non ha alcun effetto (la lettura int
è atomica) e anche la seconda. Secondo me, queste sono le forme corrette:
void synchronized incIBy5() {
i += 5
}
void incIBy5() {
synchronized(this) {
i += 5
}
}
void incIBy5() {
synchronized(this) {
int temp = i;
i = temp + 5;
}
}