Qual è la differenza tra atomico / volatile / sincronizzato?


297

Come funzionano internamente atomico / volatile / sincronizzato?

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 volatilenel 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 contemporaneamente in un blocco sincronizzato ... ho ragione? Se questo è vero, come atomic.incrementAndGet()funziona senza synchronized? Ed è thread-safe?

E qual è la differenza tra lettura interna e scrittura su variabili volatili / variabili atomiche? Ho letto in alcuni articoli che il thread ha una copia locale delle variabili - che cos'è?


5
Questo fa molte domande, con un codice che non si compila nemmeno. Forse dovresti leggere un buon libro, come Java Concurrency in Practice.
JB Nizet,

4
@JBNizet hai ragione !!! ho quel libro, non ha il concetto Atomic in breve e non ne sto prendendo alcuni concetti. della maledizione è un mio errore, non dell'autore.
Hardik,

4
Non devi davvero preoccuparti di come viene implementato (e varia con il sistema operativo). Quello che devi capire è il contratto: il valore viene incrementato atomicamente e tutti gli altri thread sono garantiti per vedere il nuovo valore.
JB Nizet,

Risposte:


392

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 volatilealla stoppedvariabile e funziona bene - se qualsiasi altro thread modifica la stoppedvariabile 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 AtomicIntegerclasse 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 volatileassicura 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é volatilenon è predefinito.

volatile senza sincronizzazione (2)

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

Lo stesso problema di cui sopra, ma ancora peggio perché inon lo è private. Le condizioni di gara sono ancora presenti. Perché è un problema? Se, diciamo, due thread eseguono questo codice contemporaneamente, l'output potrebbe essere + 5o + 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 Integercreato tramite autoboxing ...) Completamente imperfetto. Puoi anche scrivere:

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

Non è possibile inserire due thread nello stesso synchronizedblocco con lo stesso blocco . In questo caso (e similmente nel tuo codice) l'oggetto lock cambia ad ogni esecuzione, quindi synchronizedefficacemente 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 iin modo tempsincrono (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;
  }
}

10
L'unica cosa che aggiungerei è che JVM copia i valori delle variabili nei registri per operare su di essi. Ciò significa che i thread in esecuzione su una singola CPU / core possono ancora vedere valori diversi per una variabile non volatile.
David Harkness,

@thomasz: compare compareAndSet (attuale, attuale + 1) sincronizzato ?? se no di quello che succede quando due thread eseguono questo metodo contemporaneamente ??
hardik,

@Hardik: compareAndSetè solo un involucro sottile attorno all'operazione CAS. Vado alcuni dettagli nella mia risposta.
Tomasz Nurkiewicz,

1
@thomsasz: ok, passo attraverso questa domanda di collegamento e mi risponde jon skeet, dice "il thread non può leggere una variabile volatile senza controllare se nessun altro thread ha eseguito una scrittura". ma cosa succede se un thread si trova tra l'operazione di scrittura e il secondo thread lo sta leggendo !! ho sbagliato ?? non è la condizione di gara sul funzionamento atomico ??
hardik,

3
@Hardik: crea un'altra domanda per ottenere più risposte a ciò che stai chiedendo, qui siamo solo io e te e i commenti non sono appropriati per porre domande. Non dimenticare di pubblicare un link a una nuova domanda qui in modo che io possa dare seguito.
Tomasz Nurkiewicz,

61

Dichiarare una variabile come volatile significa che la modifica del suo valore influisce immediatamente sulla memoria effettiva per la variabile. Il compilatore non può ottimizzare i riferimenti fatti alla variabile. Ciò garantisce che quando un thread modifica la variabile, tutti gli altri thread vedono 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 vengano completati all'interno del thread in cui vengono eseguiti e non vengano 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 queste fasi saranno completate come se fossero un'unica operazione indivisibile / ininterrotta.

La sincronizzazione di tutti gli accessi a una variabile consente a un solo 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, è del tutto possibile sincronizzare solo alcuni accessi a una variabile e consentire ad altri accessi di non essere sincronizzati (ad esempio, sincronizzare tutte le scritture su una variabile ma nessuna delle letture da essa).

Atomicità, sincronizzazione e volatilità sono attributi indipendenti, ma in genere vengono utilizzati in combinazione per applicare una cooperazione thread appropriata per l'accesso alle variabili.

Addendum (aprile 2016)

L'accesso sincronizzato a una variabile viene generalmente implementato utilizzando un monitor o un semaforo . Si tratta di meccanismi di mutex (mutua esclusione) di basso livello che consentono a un thread di acquisire il controllo esclusivo di una variabile o di un blocco di codice, costringendo 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 il mutex a sua volta.

Addendum (luglio 2016)

La sincronizzazione si verifica su un oggetto . Ciò significa che chiamare un metodo sincronizzato di una classe bloccherà l' thisoggetto della chiamata. I metodi sincronizzati statici bloccheranno l' Classoggetto stesso.

Allo stesso modo, l'inserimento di un blocco sincronizzato richiede il blocco thisdell'oggetto del metodo.

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


25

volatile:

volatileè una parola chiave. volatileimpone a tutti i thread di 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 contemporaneamente al valore variabile volatile.

L'uso delle volatilevariabili riduce il rischio di errori di coerenza della memoria, poiché qualsiasi scrittura su una variabile volatile stabilisce una relazione accidentale prima delle successive letture della stessa variabile.

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

Quando utilizzare: un thread modifica i dati e gli altri thread devono leggere il valore più recente dei dati. Altri thread eseguiranno alcune azioni ma non aggiorneranno i dati .

AtomicXXX:

AtomicXXXle classi supportano la programmazione thread-free senza blocco su singole variabili. Queste AtomicXXXclassi (come AtomicInteger) risolvono errori di incoerenza della memoria / effetti collaterali della modifica di variabili volatili, a cui è stato effettuato l'accesso in più thread.

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

sincronizzato:

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

  1. In primo luogo, non è possibile synchronizedinterlacciare due invocazioni di metodi sullo stesso oggetto. Quando un thread esegue un synchronizedmetodo per un oggetto, tutti gli altri thread che invocano synchronizedmetodi per lo stesso blocco di oggetti (sospensione dell'esecuzione) fino a quando il primo thread non viene eseguito con l'oggetto.

  2. In secondo luogo, quando un synchronizedmetodo esce, stabilisce automaticamente una relazione accidentale prima di qualsiasi invocazione successiva di un synchronizedmetodo per lo stesso oggetto. Ciò garantisce che le modifiche allo stato dell'oggetto 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 volatile + synchronizedanche se l'implementazione è diversa. AmtomicXXXestende volatilevariabili + compareAndSetmetodi ma non usa la sincronizzazione.

Domande SE correlate:

Differenza tra volatile e sincronizzato in Java

Volatile booleano vs AtomicBoolean

Buoni articoli da leggere: (Il contenuto sopra è tratto da queste pagine della 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


2
Questa è la prima risposta che menziona effettivamente la semantica che accade prima delle parole chiave / caratteristiche descritte, che sono importanti per capire come influenzano effettivamente l'esecuzione del codice. Le risposte più votate mancano questo aspetto.
jhyot,

5

So che due thread non possono entrare contemporaneamente nel blocco Synchronize

Due thread non possono inserire due volte un blocco sincronizzato sullo stesso oggetto. Ciò significa che due thread possono inserire lo 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 previsto in quanto potrebbe bloccare ogni volta un oggetto diverso.

se questo è vero di How this atomic.incrementAndGet () funziona senza Synchronize ?? ed è thread-safe ??

sì. Non utilizza il blocco per ottenere la sicurezza della filettatura.

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 classe atomica utilizza campi volatili . Non c'è differenza nel campo. La differenza sono le operazioni eseguite. Le classi Atomic usano operazioni CompareAndSwap o CAS.

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

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

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


@Aniket Thakur ne sei sicuro? L'intero è immutabile. Quindi i ++ probabilmente decomprimerà automaticamente il valore int, lo incrementerà e quindi creerà un nuovo numero intero, che non è la stessa istanza di prima. Prova a rendere i final e otterrai errori del compilatore quando chiami i ++.
fuemf5,

2

Sincronizzato Vs Atomic Vs Volatile:

  • Volatile e Atomico viene applicato solo su variabile, mentre Sincronizzato si applica sul metodo.
  • Volatili assicurano la visibilità non l'atomicità / la coerenza dell'oggetto, mentre altri garantiscono la visibilità e l'atomicità.
  • Archivio variabile volatile nella RAM ed è più veloce nell'accesso ma non possiamo raggiungere la sicurezza dei thread o la sincronizzazione senza parole chiave sincronizzate.
  • Sincronizzato implementato come blocco sincronizzato o metodo sincronizzato mentre entrambi non lo sono. Siamo in grado di eseguire il thread sicuro su più righe di codice con l'aiuto di parole chiave sincronizzate mentre con entrambi non possiamo ottenere lo stesso.
  • Sincronizzato può bloccare lo stesso oggetto di classe o oggetto di classe diversa mentre entrambi non possono.

Per favore, correggimi se ho perso qualcosa.


1

Una sincronizzazione volatile + è una soluzione a prova di errore per un'operazione (istruzione) completamente atomica che include più istruzioni per la CPU.

Dire ad esempio: volatile int i = 2; i ++, che non è altro che i = i + 1; che rende i come valore 3 in 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 quel valore incrementato torna alla memoria. Queste operazioni non sono abbastanza atomiche sebbene il valore sia di i sia volatile. essendo volatile garantisce solo che UN SOLO Lettura / Scrittura dalla memoria è atomico e non con MULTIPLO. Quindi, dobbiamo aver sincronizzato anche intorno a i ++ per mantenerlo come una dichiarazione atomica a prova di errore. Ricorda che un'istruzione include più istruzioni.

Spero che la spiegazione sia abbastanza chiara.


1

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 dice al secondo tutto il contenuto 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 in un ambiente multi-thread per evitare incoerenze nei dati.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.