Differenza tra volatile e sincronizzato in Java


233

Mi chiedo la differenza tra dichiarare una variabile come volatilee accedere sempre alla variabile in un synchronized(this)blocco in Java?

Secondo questo articolo http://www.javamex.com/tutorials/synchronization_volatile.shtml c'è molto da dire e ci sono molte differenze ma anche alcune somiglianze.

Sono particolarmente interessato a questa informazione:

...

  • l'accesso a una variabile volatile non ha mai il potenziale per bloccare: stiamo solo facendo una semplice lettura o scrittura, quindi a differenza di un blocco sincronizzato non terremo mai un blocco;
  • poiché l'accesso a una variabile volatile non ha mai un blocco, non è adatto ai casi in cui vogliamo leggere-aggiornare-scrivere come un'operazione atomica (a meno che non siamo pronti a "perdere un aggiornamento");

Cosa significano per lettura-aggiornamento-scrittura ? Una scrittura non è anche un aggiornamento o significa semplicemente che l' aggiornamento è una scrittura che dipende dalla lettura?

Soprattutto, quando è più appropriato dichiarare le variabili volatilepiuttosto che accedervi attraverso un synchronizedblocco? È una buona idea usare volatileper le variabili che dipendono dall'input? Ad esempio, esiste una variabile chiamata renderche viene letta attraverso il ciclo di rendering e impostata da un evento keypress?

Risposte:


383

È importante capire che ci sono due aspetti della sicurezza del thread.

  1. controllo dell'esecuzione e
  2. visibilità della memoria

Il primo ha a che fare con il controllo dell'esecuzione del codice (incluso l'ordine in cui vengono eseguite le istruzioni) e se può essere eseguito contemporaneamente, e il secondo con quando gli effetti in memoria di ciò che è stato fatto sono visibili ad altri thread. Poiché ogni CPU ha diversi livelli di cache tra essa e la memoria principale, i thread in esecuzione su CPU o core diversi possono vedere la "memoria" in modo diverso in un dato momento poiché i thread possono ottenere e lavorare su copie private della memoria principale.

L'utilizzo synchronizedimpedisce a qualsiasi altro thread di ottenere il monitor (o il blocco) per lo stesso oggetto , impedendo così l' esecuzione simultanea di tutti i blocchi di codice protetti dalla sincronizzazione sullo stesso oggetto . La sincronizzazione crea anche una barriera di memoria "succede prima", causando un vincolo di visibilità della memoria tale che qualsiasi cosa fatta fino al punto in cui un thread rilascia un blocco appare ad un altro thread successivamente acquisendo lo stesso blocco che si è verificato prima che acquisisse il blocco. In termini pratici, sull'hardware corrente, questo in genere provoca lo svuotamento delle cache della CPU quando viene acquisito un monitor e scrive nella memoria principale quando viene rilasciato, entrambi i quali sono (relativamente) costosi.

L'uso volatile, d'altro canto, impone a tutti gli accessi (lettura o scrittura) alla variabile volatile alla memoria principale, mantenendo efficacemente la variabile volatile fuori dalla cache della CPU. Ciò può essere utile per alcune azioni in cui è richiesto semplicemente che la visibilità della variabile sia corretta e l'ordine degli accessi non è importante. L'uso volatilemodifica anche il trattamento di longe doubleper richiedere che gli accessi ad essi siano atomici; su alcuni hardware (meno recenti) ciò potrebbe richiedere blocchi, sebbene non su hardware a 64 bit moderno. Con il nuovo modello di memoria (JSR-133) per Java 5+, la semantica del volatile è stata rafforzata per essere quasi altrettanto forte quanto sincronizzata rispetto alla visibilità della memoria e all'ordinamento delle istruzioni (vedere http://www.cs.umd.edu /users/pugh/java/memoryModel/jsr-133-faq.html#volatile). Ai fini della visibilità, ogni accesso a un campo volatile agisce come una mezza sincronizzazione.

Con il nuovo modello di memoria, è ancora vero che le variabili volatili non possono essere riordinate tra loro. La differenza è che ora non è più così facile riordinare i normali accessi ai campi che li circondano. La scrittura su un campo volatile ha lo stesso effetto memoria di una versione monitor e la lettura da un campo volatile ha lo stesso effetto memoria acquisito da un monitor. In effetti, poiché il nuovo modello di memoria pone vincoli più severi sul riordino degli accessi ai campi volatili con altri accessi ai campi, volatili o meno, tutto ciò che era visibile al thread Aquando si scrive sul campo volatile fdiventa visibile al thread Bquando legge f.

- Domande frequenti su JSR 133 (modello di memoria Java)

Quindi, ora entrambe le forme di barriera di memoria (sotto l'attuale JMM) causano una barriera di riordino delle istruzioni che impedisce al compilatore o al runtime di riordinare le istruzioni attraverso la barriera. Nel vecchio JMM, la volatilità non impediva il riordino. Questo può essere importante perché, a parte le barriere della memoria, l'unica limitazione imposta è che, per ogni particolare thread , l'effetto netto del codice è lo stesso che sarebbe se le istruzioni fossero eseguite esattamente nell'ordine in cui appaiono nella fonte.

Un uso di volatile è che un oggetto condiviso ma immutabile viene ricreato al volo, con molti altri thread che prendono un riferimento all'oggetto in un punto particolare del loro ciclo di esecuzione. È necessario che gli altri thread inizino a utilizzare l'oggetto ricreato una volta pubblicato, ma non è necessario l'overhead aggiuntivo della sincronizzazione completa e il relativo contesa e lo svuotamento della cache.

// Declaration
public class SharedLocation {
    static public SomeObject someObject=new SomeObject(); // default object
    }

// Publishing code
// Note: do not simply use SharedLocation.someObject.xxx(), since although
//       someObject will be internally consistent for xxx(), a subsequent 
//       call to yyy() might be inconsistent with xxx() if the object was 
//       replaced in between calls.
SharedLocation.someObject=new SomeObject(...); // new object is published

// Using code
private String getError() {
    SomeObject myCopy=SharedLocation.someObject; // gets current copy
    ...
    int cod=myCopy.getErrorCode();
    String txt=myCopy.getErrorText();
    return (cod+" - "+txt);
    }
// And so on, with myCopy always in a consistent state within and across calls
// Eventually we will return to the code that gets the current SomeObject.

Parlando alla tua domanda di lettura-aggiornamento-scrittura, in particolare. Considera il seguente codice non sicuro:

public void updateCounter() {
    if(counter==1000) { counter=0; }
    else              { counter++; }
    }

Ora, con il metodo updateCounter () non sincronizzato, due thread possono inserirlo contemporaneamente. Tra le molte permutazioni di ciò che potrebbe accadere, una è che thread-1 esegue il test per counter == 1000 e lo trova vero e viene quindi sospeso. Quindi thread-2 fa lo stesso test e lo vede anche vero ed è sospeso. Quindi thread-1 riprende e imposta il contatore su 0. Quindi thread-2 riprende e imposta nuovamente il contatore su 0 perché non ha eseguito l'aggiornamento da thread-1. Questo può accadere anche se la commutazione del thread non avviene come ho descritto, ma semplicemente perché erano presenti due diverse copie memorizzate nella cache del contatore in due diversi core della CPU e i thread funzionavano ciascuno su un core separato. Del resto, un thread potrebbe avere un contatore su un valore e l'altro potrebbe avere un contatore su un valore completamente diverso solo a causa della memorizzazione nella cache.

La cosa importante in questo esempio è che il contatore delle variabili è stato letto dalla memoria principale nella cache, aggiornato nella cache e riscritto nella memoria principale solo in un momento indeterminato in seguito quando si è verificata una barriera di memoria o quando la memoria cache era necessaria per qualcos'altro. La creazione del contatore volatilenon è sufficiente per la sicurezza del thread di questo codice, perché il test per il massimo e le assegnazioni sono operazioni discrete, incluso l'incremento che è un insieme di read+increment+writeistruzioni non atomiche della macchina, qualcosa del tipo:

MOV EAX,counter
INC EAX
MOV counter,EAX

Le variabili volatili sono utili solo quando tutte le operazioni eseguite su di esse sono "atomiche", come nel mio esempio in cui un riferimento a un oggetto completamente formato viene solo letto o scritto (e, in effetti, in genere è scritto solo da un singolo punto). Un altro esempio potrebbe essere un riferimento di array volatile che supporta un elenco di copia su scrittura, a condizione che l'array sia stato letto solo eseguendo prima una copia locale del riferimento.


5
Grazie mille! L'esempio con il contatore è semplice da capire. Tuttavia, quando le cose diventano reali, è un po 'diverso.
Albus Silente,

"In termini pratici, sull'hardware attuale, questo di solito provoca lo svuotamento delle cache della CPU quando viene acquisito un monitor e scrive nella memoria principale quando viene rilasciato, entrambi costosi (relativamente parlando)." . Quando dici cache della CPU, è lo stesso degli stack Java locali per ogni thread? o un thread ha la sua versione locale di Heap? Scusati se sono sciocco qui.
NishM,

1
@nishm Non è lo stesso, ma includerebbe le cache locali dei thread coinvolti. .
Lawrence Dol,

1
@ MarianPaździoch: un incremento o decremento NON è una lettura o una scrittura, è una lettura e una scrittura; è una lettura in un registro, quindi un incremento del registro, quindi una riscrittura in memoria. Le letture e le scritture sono individualmente atomiche, ma non lo sono più operazioni di questo tipo.
Lawrence Dol,

2
Quindi, secondo le FAQ, non solo le azioni eseguite dopo l'acquisizione di un blocco sono rese visibili dopo lo sblocco, ma tutte le azioni fatte da quel thread sono rese visibili. Anche azioni compiute prima dell'acquisizione del blocco.
Lii,

97

volatile è un modificatore di campo , mentre sincronizzato modifica blocchi e metodi di codice . Quindi possiamo specificare tre varianti di un semplice accessor usando queste due parole chiave:

    int i1;
    int geti1() {return i1;}

    volatile int i2;
    int geti2() {return i2;}

    int i3;
    synchronized int geti3() {return i3;}

geti1()accede al valore attualmente memorizzato nel i1thread corrente. I thread possono avere copie locali delle variabili e i dati non devono essere gli stessi dei dati contenuti in altri thread. In particolare, un altro thread potrebbe essere stato aggiornato i1nel thread, ma il valore nel thread corrente potrebbe essere diverso da quello valore aggiornato. In effetti Java ha l'idea di una memoria "principale", e questa è la memoria che contiene l'attuale valore "corretto" per le variabili. I thread possono avere una propria copia di dati per le variabili e la copia del thread può essere diversa dalla memoria "principale". Quindi, in effetti, è possibile che la memoria "principale" abbia un valore di 1 per i1, per thread1 abbia un valore di 2 per i1e per thread2di avere un valore di 3 per i1se Thread1 e thread2 avere sia i1 aggiornato ma quelli valore aggiornato non è ancora stato propagato alla memoria "principale" o altri fili.

D'altra parte, geti2()accede efficacemente al valore della i2memoria "principale". Una variabile volatile non può avere una copia locale di una variabile diversa dal valore attualmente presente nella memoria "principale". In effetti, una variabile dichiarata volatile deve avere i suoi dati sincronizzati su tutti i thread, in modo che ogni volta che accedi o aggiorni la variabile in qualsiasi thread, tutti gli altri thread vedono immediatamente lo stesso valore. Le variabili generalmente volatili hanno un accesso maggiore e aggiornano le spese generali rispetto alle variabili "semplici". Generalmente ai thread è consentito avere una propria copia di dati per una migliore efficienza.

Esistono due differenze tra volitile e sincronizzato.

In primo luogo sincronizzato ottiene e rilascia i blocchi sui monitor che possono forzare un solo thread alla volta per eseguire un blocco di codice. Questo è l'aspetto abbastanza noto da sincronizzare. Ma sincronizzato sincronizza anche la memoria. Infatti sincronizzato sincronizza l'intera memoria del thread con la memoria "principale". Quindi l'esecuzione geti3()fa quanto segue:

  1. Il thread acquisisce il blocco sul monitor per questo oggetto.
  2. La memoria del thread scarica tutte le sue variabili, cioè ha tutte le sue variabili effettivamente lette dalla memoria "principale".
  3. Il blocco di codice viene eseguito (in questo caso impostando il valore di ritorno sul valore corrente di i3, che potrebbe essere stato appena resettato dalla memoria "principale").
  4. (Qualsiasi modifica alle variabili ora verrebbe normalmente scritta nella memoria "principale", ma per geti3 () non abbiamo modifiche.)
  5. Il thread rilascia il blocco sul monitor per questo oggetto.

Quindi, dove volatile sincronizza solo il valore di una variabile tra la memoria del thread e la memoria "principale", sincronizzato sincronizza il valore di tutte le variabili tra la memoria del thread e la memoria "principale" e si blocca e rilascia un monitor per l'avvio. È probabile che una chiara sincronizzazione abbia un sovraccarico maggiore che volatile.

http://javaexp.blogspot.com/2007/12/difference-between-volatile-and.html


35
-1, Volatile non acquisisce un blocco, utilizza l'architettura della CPU sottostante per garantire la visibilità su tutti i thread dopo la scrittura.
Michael Barker,

Vale la pena notare che potrebbero esserci alcuni casi in cui un lucchetto può essere utilizzato per garantire l'atomicità delle scritture. Ad esempio, scrivere a lungo su una piattaforma a 32 bit che non supporta i diritti di larghezza estesa. Intel lo evita utilizzando i registri SSE2 (128 bit di larghezza) per gestire i long volatili. Tuttavia, considerare un volatile come un lucchetto probabilmente porterà a cattivi bug nel tuo codice.
Michael Barker,

2
L'importante semantica condivisa da blocchi e variabili volatili è che entrambi forniscono bordi Happens-Before (Java 1.5 e versioni successive). L'inserimento di un blocco sincronizzato, la rimozione di un blocco e la lettura da un volatile sono tutti considerati come un "acquisizione" e il rilascio di un blocco, l'uscita da un blocco sincronizzato e la scrittura di un volatile sono tutte forme di un "rilascio".
Michael Barker,

20

synchronizedè il modificatore della restrizione dell'accesso a livello di metodo / livello di blocco. Si assicurerà che un thread possieda il blocco per la sezione critica. Solo il thread, che possiede un blocco, può inserire il synchronizedblocco. Se altri thread stanno tentando di accedere a questa sezione critica, devono attendere che l'attuale proprietario rilasci il blocco.

volatileè un modificatore di accesso variabile che forza tutti i thread a ottenere l'ultimo valore della variabile dalla memoria principale. Non è richiesto alcun blocco per accedere alle volatilevariabili. Tutti i thread possono accedere contemporaneamente al valore variabile volatile.

Un buon esempio per usare la variabile volatile: Datevariabile.

Supponiamo che tu abbia reso Variabile data volatile. Tutti i thread, che accedono a questa variabile, ottengono sempre i dati più recenti dalla memoria principale in modo che tutti i thread mostrino il valore della data (reale) reale. Non hai bisogno di thread diversi che mostrano tempi diversi per la stessa variabile. Tutti i thread devono mostrare il valore Data corretto.

inserisci qui la descrizione dell'immagine

Dai un'occhiata a questo articolo per una migliore comprensione del volatileconcetto.

Lawrence Dol ha spiegato chiaramente il tuo read-write-update query.

Per quanto riguarda le altre tue domande

Quando è più appropriato dichiarare le variabili volatili che accedervi tramite sincronizzato?

Devi usarlo volatilese pensi che tutti i thread debbano ottenere il valore effettivo della variabile in tempo reale come nell'esempio che ho spiegato per la variabile Date.

È una buona idea usare volatile per variabili che dipendono dall'input?

La risposta sarà la stessa della prima query.

Fare riferimento a questo articolo per una migliore comprensione.


Quindi la lettura può avvenire contemporaneamente e tutto il thread leggerà l'ultimo valore perché la CPU non memorizza nella cache della memoria principale la cache del thread della CPU, ma per quanto riguarda la scrittura? La scrittura non deve essere corretta contemporaneamente? Seconda domanda: se un blocco è sincronizzato, ma la variabile non è volatile, il valore di una variabile in un blocco sincronizzato può ancora essere modificato da un altro thread in un altro blocco di codice, giusto?
the_prole

11

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.


1
"Anche se può sembrare un'operazione, in realtà è 3: leggere il valore corrente di x dalla memoria, aggiungerne 1 e salvarlo in memoria." - Esatto, perché i valori della memoria devono passare attraverso i circuiti della CPU per essere aggiunti / modificati. Anche se questo si trasforma in un'unica INCoperazione di assemblaggio , le operazioni della CPU sottostante sono ancora 3 volte e richiedono il blocco per la sicurezza del thread. Buon punto. INC/DECTuttavia , i comandi possono essere contrassegnati atomicamente in assembly e comunque essere 1 operazione atomica.
Zombi

@Zombies quindi quando creo un blocco sincronizzato per x ++, lo trasforma in un INC / DEC atomico contrassegnato o usa un blocco normale?
David Refaeli,

Non lo so! Quello che so è che INC / DEC non sono atomici perché per una CPU, deve caricare il valore e leggerlo e anche scriverlo (in memoria), proprio come qualsiasi altra operazione aritmetica.
Zombi
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.