Qual è la differenza tra atomico / volatile / sincronizzato?

Come funzionano atomicamente / volatile / sincronizzato internamente?

Qual è la differenza tra i seguenti blocchi di codice?

Codice 1

private int counter; public int getNextUniqueIndex() { return counter++; } 

Codice 2

 private AtomicInteger counter; public int getNextUniqueIndex() { return counter.getAndIncrement(); } 

Codice 3

 private volatile int counter; public int getNextUniqueIndex() { return counter++; } 

Funziona volatile nel modo seguente? È

 volatile int i = 0; void incIBy5() { i += 5; } 

equivalente a

 Integer i = 5; void incIBy5() { int temp; synchronized(i) { temp = i } synchronized(i) { i = temp + 5 } } 

Penso che due thread non possano entrare in un blocco sincronizzato allo stesso tempo … ho ragione? Se questo è vero, allora come atomic.incrementAndGet() funziona senza synchronized ? Ed è thread-safe?

E qual è la differenza tra lettura e scrittura interne a variabili volatili / variabili atomiche? Ho letto in alcuni articoli che il thread ha una copia locale delle variabili – che cos’è?

Stai chiedendo specificamente come funzionano internamente , quindi eccoti qui:

Nessuna sincronizzazione

 private int counter; public int getNextUniqueIndex() { return counter++; } 

In pratica legge il valore dalla memoria, lo incrementa e rimette in memoria. Funziona su thread singolo 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 allo stesso tempo), ma anche problemi di visibilità. Il valore potrebbe essere memorizzato solo nella memoria della CPU ” locale ” (un po ‘di cache) e non essere visibile per altre CPU / core (e quindi – thread). Questo è il motivo per cui molti si riferiscono alla copia locale di una variabile in una discussione. È molto pericoloso Considera questo codice di interruzione del thread popolare ma rotto:

 private boolean stopped; public void run() { while(!stopped) { //do some work } } public void pleaseStop() { stopped = true; } 

Aggiungi variabile volatile ad stopped e funziona bene – se qualsiasi altro thread modifica la variabile stopped tramite il metodo pleaseStop() , hai la pleaseStop() di vederlo immediatamente nel ciclo di lavoro while(!stopped) . BTW non è un buon modo per interrompere un thread, vedi: Come fermare un thread che gira per sempre senza alcun utilizzo e Arresto di un thread java specifico .

AtomicInteger

 private AtomicInteger counter = new AtomicInteger(); public int getNextUniqueIndex() { return counter.getAndIncrement(); } 

La class AtomicInteger utilizza operazioni CPU di basso livello CAS ( compare-and-swap ) (non è necessaria alcuna sincronizzazione!) Permettono di modificare una particolare variabile solo se il valore attuale è uguale a qualcos’altro (e viene restituito correttamente). Quindi, quando esegui getAndIncrement() , in realtà viene eseguito in un ciclo (implementazione reale semplificata):

 int current; do { current = get(); } while(!compareAndSet(current, current + 1)); 

Quindi in pratica: leggi; provare a memorizzare il valore incrementato; se non ha successo (il valore non è più uguale a current ), leggere e riprovare. 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 della visibilità ( volatile fa in modo che altri thread possano vedere le modifiche apportate al counter ) ma ha ancora una condizione di competizione. Questo è stato spiegato più volte: pre / post-incremento non è atomico.

L’unico effetto collaterale di volatile è il ” flushing ” delle cache in modo che tutte le altre parti vedano la versione più recente dei dati. Questo è troppo severo nella maggior parte delle situazioni; questo è il motivo per cui volatile non è predefinito.

volatile without synchronization (2)

 volatile int i = 0; void incIBy5() { i += 5; } 

Lo stesso problema di cui sopra, ma ancora peggio perché non sono private . La condizione della gara è ancora presente. Perché è un problema? Se, ad esempio, due thread eseguono questo codice contemporaneamente, l’output potrebbe essere + 5 o + 10 . Tuttavia, hai la garanzia di vedere il cambiamento.

Più synchronized indipendenti

 void incIBy5() { int temp; synchronized(i) { temp = i } synchronized(i) { i = temp + 5 } } 

Sorpresa, questo codice è errato pure. In realtà, è completamente sbagliato. Prima di tutto ti stai sincronizzando su i , che sta per essere cambiato (inoltre, i sono un primitivo, quindi suppongo che tu stia sincronizzando su un Integer temporaneo creato tramite autoboxing …) Completamente imperfetto. Potresti anche scrivere:

 synchronized(new Object()) { //thread-safe, SRSLy? } 

Non è ansible inserire due thread nello stesso blocco synchronized con lo stesso blocco . In questo caso (e analogamente nel tuo codice) l’object di blocco cambia ad ogni esecuzione, quindi la synchronized effettivamente non ha alcun effetto.

Anche se hai usato una variabile finale (o this ) per la sincronizzazione, il codice è ancora errato. Due thread possono prima leggere i in temp sincrono (avendo lo stesso valore localmente in temp ), quindi il primo assegna un nuovo valore a i (ad esempio, da 1 a 6) e l’altro fa la stessa cosa (da 1 a 6) .

La sincronizzazione deve passare dalla lettura all’assegnazione di un valore. La tua prima sincronizzazione non ha alcun effetto (la lettura di un int è atomica) e la seconda. Secondo me, queste sono le forms corrette:

 void synchronized incIBy5() { i += 5 } void incIBy5() { synchronized(this) { i += 5 } } void incIBy5() { synchronized(this) { int temp = i; i = temp + 5; } } 

Dichiarare una variabile come volatile significa che modificarne il valore influisce immediatamente sulla memoria effettiva della memoria. Il compilatore non può ottimizzare i riferimenti fatti alla variabile. Ciò garantisce che quando un thread modifica la variabile, tutti gli altri thread vedano immediatamente il nuovo valore. (Questo non è garantito per le variabili non volatili.)

La dichiarazione di una variabile atomica garantisce che le operazioni eseguite sulla variabile avvengano in modo atomico, ovvero che tutti i passaggi secondari dell’operazione siano completati all’interno del thread che vengono eseguiti e non siano interrotti da altri thread. Ad esempio, un’operazione di incremento e test richiede che la variabile venga incrementata e quindi confrontata con un altro valore; un’operazione atomica garantisce che entrambe le fasi saranno completate come se fossero un’unica operazione indivisibile / non interrompibile.

La sincronizzazione di tutti gli accessi a una variabile consente solo un singolo thread alla volta di accedere alla variabile e forza tutti gli altri thread ad attendere che quel thread di accesso rilasci il suo accesso alla variabile.

L’accesso sincronizzato è simile all’accesso atomico, ma le operazioni atomiche sono generalmente implementate a un livello inferiore di programmazione. Inoltre, è completamente ansible sincronizzare solo alcuni accessi a una variabile e consentire ad altri accessi di essere non sincronizzati (ad esempio, sincronizzare tutte le scritture su una variabile ma nessuna delle letture da essa).

Atomicità, sincronizzazione e volatilità sono attributi indipendenti, ma sono tipicamente usati in combinazione per rinforzare la corretta cooperazione tra thread per l’accesso alle variabili.

Addendum (aprile 2016)

L’accesso sincronizzato a una variabile viene solitamente implementato utilizzando un monitor o semaforo . Si tratta di meccanismi di mutex a basso livello (mutua esclusione) che consentono a un thread di acquisire il controllo di una variabile o di un blocco di codice esclusivamente, forzando tutti gli altri thread ad attendere se tentano anche di acquisire lo stesso mutex. Una volta che il thread proprietario rilascia il mutex, un altro thread può acquisire a turno il mutex.

Addendum (luglio 2016)

La sincronizzazione si verifica su un object . Ciò significa che chiamare un metodo sincronizzato di una class bloccherà l’object della chiamata. I metodi sincronizzati statici bloccheranno l’object Class stesso.

Allo stesso modo, l’inserimento di un blocco sincronizzato richiede il blocco di this object del metodo.

Ciò significa che un metodo (o un blocco) sincronizzato può essere eseguito in più thread contemporaneamente se si bloccano su oggetti diversi , ma solo un thread può eseguire un metodo (o un blocco) sincronizzato alla volta per ogni dato singolo object.

volatile:

volatile è una parola chiave. volatile forza tutti i thread per ottenere l’ultimo valore della variabile dalla memoria principale anziché dalla cache. Non è richiesto alcun blocco per accedere alle variabili volatili. Tutti i thread possono accedere al valore variabile volatile allo stesso tempo.

L’uso di variabili volatile riduce il rischio di errori di coerenza della memoria, poiché ogni scrittura su una variabile volatile stabilisce una relazione prima-evento con letture successive della stessa variabile.

Ciò significa che le modifiche a una variabile volatile sono sempre visibili ad altri thread . Inoltre, significa anche che quando un thread legge una variabile volatile , non vede solo l’ultima modifica al volatile, ma anche gli effetti collaterali del codice che ha portato alla modifica .

Quando utilizzare: un thread modifica i dati e altri thread devono leggere l’ultimo valore dei dati. Gli altri thread prenderanno qualche azione ma non aggiorneranno i dati .

AtomicXXX:

AtomicXXX classi AtomicXXX supportano la programmazione thread-safe su singole variabili. Queste classi AtomicXXX (come AtomicInteger ) risolvono gli errori di inconsistenza di memoria / gli effetti collaterali della modifica delle variabili volatili a cui è stato effettuato l’accesso in più thread.

Quando utilizzare: più thread possono leggere e modificare i dati.

sincronizzato:

synchronized è una parola chiave usata per proteggere un metodo o un blocco di codice. Rendendo il metodo come sincronizzato ha due effetti:

  1. Innanzitutto, non è ansible che due invocazioni di metodi synchronized sullo stesso object si interfaccino. Quando un thread sta eseguendo un metodo synchronized per un object, tutti gli altri thread che invocano metodi synchronized per lo stesso blocco object (sospendi l’esecuzione) finché il primo thread non viene eseguito con l’object.

  2. In secondo luogo, quando un metodo synchronized chiude, stabilisce automaticamente una relazione prima-accade con qualsiasi successiva chiamata di un metodo synchronized per lo stesso object. Ciò garantisce che le modifiche allo stato dell’object siano visibili a tutti i thread.

Quando utilizzare: più thread possono leggere e modificare i dati. La tua logica aziendale non solo aggiorna i dati, ma esegue anche operazioni atomiche

AtomicXXX è equivalente a volatile + synchronized anche se l’implementazione è diversa. AmtomicXXX estende volatile variabili volatile + compareAndSet ma non usa la sincronizzazione.

Domande relative a SE:

Differenza tra volatile e sincronizzato in Java

Volatile booleano vs AtomicoBooleano

Buoni articoli da leggere: (il contenuto di cui sopra è tratto da queste pagine di documentazione)

https://docs.oracle.com/javase/tutorial/essential/concurrency/sync.html

https://docs.oracle.com/javase/tutorial/essential/concurrency/atomic.html

https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/atomic/package-summary.html

So che due thread non possono entrare nel blocco Synchronize allo stesso tempo

Due thread non possono immettere due volte un blocco sincronizzato sullo stesso object. Ciò significa che due thread possono entrare nello stesso blocco su oggetti diversi. Questa confusione può portare a un codice come questo.

 private Integer i = 0; synchronized(i) { i++; } 

Questo non si comporterà come ci si aspetta dato che potrebbe bloccarsi su un object diverso ogni volta.

se questo è vero di come questo atomic.incrementAndGet () funziona senza Synchronize ?? ed è thread-sicuro ??

sì. Non utilizza il blocco per ottenere la sicurezza del filo.

Se vuoi sapere come funzionano in modo più dettagliato, puoi leggere il codice per loro.

E qual è la differenza tra lettura interna e scrittura su Variabile Volatile / Variabile Atomica ??

La class atomica usa campi volatili . Non c’è differenza nel campo. La differenza sono le operazioni eseguite. Le classi atomiche utilizzano le operazioni CompareAndSwap o CAS.

ho letto in qualche articolo che thread ha una copia locale delle variabili che cos’è ??

Posso solo supporre che si riferisca al fatto che ogni CPU ha una propria visione cache della memoria che può essere diversa da ogni altra CPU. Per garantire che la tua CPU abbia una visione coerente dei dati, devi usare le tecniche di sicurezza dei thread.

Questo è solo un problema quando la memoria è condivisa, almeno un thread lo aggiorna.

Una sincronizzazione volatile + è una soluzione infallibile per un’operazione (statement) completamente atomica che include più istruzioni per la CPU.

Per esempio, per esempio: volatile int i = 2; i ++, che non è altro che i = i + 1; che rende i come valore 3 nella memoria dopo l’esecuzione di questa istruzione. Ciò include la lettura del valore esistente dalla memoria per i (che è 2), il caricamento nel registro dell’accumulatore della CPU e l’esecuzione del calcolo incrementando il valore esistente con uno (2 + 1 = 3 nell’accumulatore) e quindi riscrivendo tale valore incrementato torna alla memoria. Queste operazioni non sono abbastanza atomiche anche se il valore è di i è volatile. essere volatile garantisce solo che una lettura / scrittura SINGLE dalla memoria è atomica e non con MULTIPLE. Quindi, abbiamo bisogno di essere sincronizzati anche attorno a i ++ per mantenere la dichiarazione atomica infallibile. Ricorda il fatto che una dichiarazione include più affermazioni.

Spero che la spiegazione sia abbastanza chiara.

Il modificatore volatile Java è un esempio di un meccanismo speciale per garantire che la comunicazione avvenga tra i thread. Quando un thread scrive su una variabile volatile e un altro thread vede quella scrittura, il primo thread indica il secondo su tutti i contenuti della memoria fino a quando non ha eseguito la scrittura su quella variabile volatile.

Le operazioni atomiche vengono eseguite in una singola unità di attività senza interferenze da altre operazioni. Le operazioni atomiche sono necessarie nell’ambiente multi-thread per evitare l’incoerenza dei dati.