Differenza tra volatile e sincronizzato in Java

Mi chiedo quale sia la differenza tra dichiarare una variabile come volatile e accedere sempre alla variabile in un blocco synchronized(this) 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: facciamo sempre solo una semplice lettura o scrittura, quindi a differenza di un blocco sincronizzato non potremo mai aggrapparci a nessun blocco;
  • poiché l’accesso a una variabile volatile non mantiene mai un blocco, non è adatto per i casi in cui si desidera leggere-aggiornare-scrivere come un’operazione atomica (a meno che non si sia pronti a “perdere un aggiornamento”);

Cosa intendono con read-update-write ? Non è una scrittura anche un aggiornamento o significa semplicemente che l’ aggiornamento è una scrittura che dipende dalla lettura?

Soprattutto, quando è più appropriato dichiarare variabili volatile piuttosto che accedervi tramite un blocco synchronized ? È una buona idea usare volatile per variabili che dipendono dall’input? Ad esempio, c’è una variabile chiamata render che viene letta attraverso il ciclo di rendering e impostata da un evento keypress?

È importante capire che ci sono due aspetti per inficiare la sicurezza.

  1. controllo dell’esecuzione, e
  2. visibilità della memoria

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

L’utilizzo synchronized impedisce a qualsiasi altro thread di ottenere il monitor (o il blocco) per lo stesso object , impedendo in tal modo l’ esecuzione simultanea di tutti i blocchi di codice protetti dalla sincronizzazione sullo stesso object . 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 che successivamente acquisisce lo stesso blocco prima che acquisisse il blocco. In termini pratici, sull’hardware corrente, questo in genere causa lo svuotamento delle cache della CPU quando un monitor viene acquisito e scrive nella memoria principale quando viene rilasciato, entrambe sono (relativamente) costose.

L’uso di volatile , d’altra parte, impone tutti gli accessi (letti o scritti) alla variabile volatile per verificarsi nella memoria principale, mantenendo efficacemente la variabile volatile dalle cache della CPU. Questo può essere utile per alcune azioni in cui è semplicemente richiesto che la visibilità della variabile sia corretta e l’ordine degli accessi non è importante. L’uso di volatile cambia anche il trattamento di long e double per richiedere l’accesso ad essi per essere atomici; su alcuni (vecchi) hardware questo potrebbe richiedere dei blocchi, sebbene non su hardware moderno a 64 bit. Sotto il nuovo modello di memoria (JSR-133) per Java 5+, la semantica del volatile è stata rafforzata per essere quasi 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.

Sotto il nuovo modello di memoria, è ancora vero che le variabili volatili non possono essere riordinate l’una con l’altra. La differenza è che ora non è più così facile riordinare i normali accessi di campo attorno a loro. La scrittura su un campo volatile ha lo stesso effetto memoria di una versione di monitoraggio e la lettura da un campo volatile ha lo stesso effetto di memoria acquisito da un monitor. In effetti, poiché il nuovo modello di memoria pone vincoli più rigidi sul riordino degli accessi al campo volatile con altri accessi di campo, volatili o meno, tutto ciò che era visibile al thread A quando scrive nel campo volatile f diventa visibile al thread B quando legge f .

– Domande frequenti su JSR 133 (Java Memory Model)

Quindi, ora entrambe le forms di barriera della memoria (sotto l’attuale JMM) causano una barriera di riordino delle istruzioni che impedisce al compilatore o al tempo di esecuzione di riordinare le istruzioni attraverso la barriera. Nel vecchio JMM, volatile non ha impedito il riordino. Questo può essere importante, perché oltre alle barriere della memoria l’unica limitazione imposta è che, per ogni thread particolare , l’effetto netto del codice è lo stesso che sarebbe se le istruzioni fossero eseguite esattamente nell’ordine in cui compaiono nel fonte.

Un uso di volatile è che un object condiviso ma immutabile viene ricreato al volo, con molti altri thread che prendono un riferimento all’object in un punto particolare nel loro ciclo di esecuzione. Uno ha bisogno che gli altri thread inizino a utilizzare l’object ricreato una volta che è stato pubblicato, ma non ha bisogno del sovraccarico aggiuntivo della sincronizzazione completa e della sua contesa e del flushing 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 inserirli contemporaneamente. Tra le molte permutazioni di ciò che potrebbe accadere, una è che thread-1 esegue il test per il contatore == 1000 e lo trova vero e viene quindi sospeso. Quindi thread-2 esegue lo stesso test e lo vede anche vero ed è sospeso. Quindi thread-1 riprende e imposta counter a 0. Quindi thread-2 riprende e imposta nuovamente counter su 0 perché ha perso l’aggiornamento da thread-1. Ciò può anche accadere anche se il cambio di thread non si verifica come ho descritto, ma semplicemente perché erano presenti due diverse copie del contatore memorizzate nella cache in due diversi core della CPU ei thread venivano eseguiti su un core separato. Del resto, un thread potrebbe avere un contatore ad un valore e l’altro potrebbe avere un contatore con un valore completamente diverso solo a causa del caching.

Ciò che è importante in questo esempio è che il contatore delle variabili è stato letto dalla memoria principale nella cache, aggiornato nella cache e solo riscritto nella memoria principale in un punto indeterminato in seguito quando si è verificata una barriera di memoria o quando la memoria cache era necessaria per qualcos’altro. Rendere il contatore volatile è insufficiente 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 istruzioni di read+increment+write atomica read+increment+write , 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 il mio esempio in cui un riferimento a un object 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 copy-on-write, a condizione che l’array sia stato letto solo prendendo prima una copia locale del riferimento ad esso.

volatile è un modificatore di campo , mentre sincronizzato modifica i blocchi di codice e i metodi . Quindi possiamo specificare tre varianti di una 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 in i1 nel thread corrente. I thread possono avere copie locali di variabili e i dati non devono essere uguali ai dati contenuti in altri thread. In particolare, un altro thread potrebbe aver aggiornato i1 nella sua thread, ma il valore nel thread corrente potrebbe essere diverso da quel valore aggiornato. Infatti Java ha l’idea di una memoria “principale”, e questa è la memoria che contiene il valore “corretto” corrente 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, è ansible che la memoria “principale” abbia un valore 1 per i1 , che per thread1 abbia un valore 2 per i1 e che per thread2 sia impostato un valore 3 per i1 se thread1 e thread2 hanno entrambi aggiornato i1 ma il valore aggiornato non è ancora stato propagato alla memoria “principale” o ad altri thread.

D’altra parte, geti2() accede efficacemente al valore di i2 dalla memoria “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 dati sincronizzati su tutti i thread, in modo che ogni volta che si accede o si aggiorna la variabile in qualsiasi thread, tutti gli altri thread vedano immediatamente lo stesso valore. Generalmente le variabili volatili hanno un sovraccarico di accesso e di aggiornamento più elevato rispetto alle variabili “semplici”. Generalmente i thread possono avere una propria copia dei dati per una migliore efficienza.

Esistono due differenze tra volitivo e sincronizzato.

Innanzitutto sincronizzato ottiene e rilascia blocchi sui monitor che possono forzare solo un thread alla volta per eseguire un blocco di codice. Questo è l’aspetto abbastanza noto alla sincronizzazione. Ma sincronizzato sincronizza anche la memoria. Infatti sincronizzato sincronizza l’intera memoria di thread con la memoria “principale”. Quindi eseguire geti3() fa quanto segue:

  1. Il thread acquisisce il blocco sul monitor per questo object.
  2. La memoria del thread svuota 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 si imposta il valore di ritorno sul valore corrente di i3, che potrebbe essere stato appena ripristinato dalla memoria “principale”).
  4. (Eventuali modifiche alle variabili normalmente vengono scritte nella memoria “principale”, ma per geti3 () non abbiamo modifiche).
  5. Il thread rilascia il blocco sul monitor per questo object.

Quindi, dove volatile sincronizza solo il valore di una variabile tra la memoria del thread e la memoria “principale”, sincronizza sincronizzato il valore di tutte le variabili tra la memoria del thread e la memoria “principale” e blocca e rilascia un monitor per l’avvio. Chiaramente sincronizzato è probabile che abbia un overhead più volatile.

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

synchronized è un modificatore di restrizione dell’accesso a livello di metodo / livello di blocco. Si assicurerà che un thread sia proprietario del blocco per la sezione critica. Solo il thread, che possiede un lock, può entrare nel blocco synchronized . Se altri thread stanno tentando di accedere a questa sezione critica, devono attendere fino a quando il proprietario corrente non rilascia il blocco.

volatile è un modificatore di accesso variabile che forza tutti i thread per ottenere l’ultimo valore della variabile dalla memoria principale. Non è richiesto alcun blocco per accedere alle variabili volatile . Tutti i thread possono accedere al valore variabile volatile allo stesso tempo.

Un buon esempio per utilizzare la variabile volatile: variabile Date .

Supponiamo che tu abbia reso variabile Data variabile. 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 di data reale (effettivo). Non hai bisogno di thread diversi che mostrano tempi diversi per la stessa variabile. Tutti i thread dovrebbero mostrare il giusto valore di Data.

inserisci la descrizione dell'immagine qui

Dai un’occhiata a questo articolo per una migliore comprensione del concetto volatile .

Lawrence Dol cleary ha spiegato la tua read-write-update query .

Per quanto riguarda le altre tue domande

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

Devi usare volatile se pensi che tutti i thread debbano avere il valore reale 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.

  1. volatile parola chiave volatile in java è un modificatore di campo, mentre la synchronized modifica i blocchi di codice ei metodi.

  2. synchronized ottiene e rilascia il blocco sulla parola chiave java volatile del monitor non richiede quello.

  3. I thread in Java possono essere bloccati per l’attesa di qualsiasi monitor in caso di synchronized , questo non è il caso della parola chiave volatile in Java.

  4. synchronized metodo synchronized influisce sulle prestazioni più della parola chiave volatile in Java.

  5. Poiché la parola chiave volatile in Java sincronizza solo il valore di una variabile tra la memoria di thread e la memoria “principale” mentre synchronized parola chiave sincronizzata sincronizza il valore di tutte le variabili tra la memoria di thread e la memoria “principale” e rilascia un monitor per l’avvio. Per questo motivo, è probabile che la parola chiave synchronized in Java abbia un overhead più volatile .

  6. Non puoi sincronizzare su oggetti nulli ma la tua variabile volatile in java potrebbe essere nullo.

  7. Da Java 5 La scrittura in un campo volatile ha lo stesso effetto memoria di una versione di monitoraggio e la lettura da un campo volatile ha lo stesso effetto di memoria di un monitor acquisito

Mi piace la spiegazione di jenkov

Visibilità di oggetti condivisi

Se due o più thread condividono un object, senza l’uso appropriato di dichiarazioni volatili o di sincronizzazione , gli aggiornamenti all’object condiviso creato da un thread potrebbero non essere visibili ad altri thread.

Immagina che l’object condiviso sia inizialmente memorizzato nella memoria principale. Un thread in esecuzione sulla CPU uno legge quindi l’object condiviso nella cache della CPU. Qui apporta una modifica all’object condiviso. Finché la cache della CPU non è stata ripristinata nella memoria principale, la versione modificata dell’object condiviso non è visibile ai thread in esecuzione su altre CPU. In questo modo ogni thread può finire con la propria copia dell’object condiviso, ogni copia seduta in una cache della CPU diversa.

Il seguente diagramma illustra la situazione disegnata. Un thread in esecuzione sulla CPU di sinistra copia l’object condiviso nella sua cache della CPU e modifica la sua variabile count in 2. Questa modifica non è visibile ad altri thread in esecuzione sulla CPU corretta, perché l’aggiornamento per contare non è stato ripristinato allo stato principale memoria ancora.

inserisci la descrizione dell'immagine qui

Per risolvere questo problema è ansible utilizzare la parola chiave volatile di Java . La parola chiave volatile può essere sicura che una determinata variabile venga letta direttamente dalla memoria principale, e sempre riscritta nella memoria principale una volta aggiornata.

Condizioni di gara

Se due o più thread condividono un object e più di un thread aggiorna le variabili in quell’object condiviso, possono verificarsi condizioni di competizione.

Immagina se il thread A legge il conteggio delle variabili di un object condiviso nella sua cache della CPU. Immaginate anche, che il thread B faccia lo stesso, ma in una cache della CPU diversa. Ora il thread A aggiunge uno a contare, e il thread B fa lo stesso. Ora var1 è stato incrementato due volte, una volta in ogni cache della CPU.

Se questi incrementi fossero stati eseguiti in modo sequenziale, il conteggio delle variabili sarebbe stato incrementato due volte e il valore originale + 2 riscritto nella memoria principale.

Tuttavia, i due incrementi sono stati eseguiti contemporaneamente senza una sincronizzazione adeguata. Indipendentemente dal thread A e B che scrive la versione aggiornata del conteggio alla memoria principale, il valore aggiornato sarà solo 1 superiore rispetto al valore originale, nonostante i due incrementi.

Questo diagramma illustra l’occorrenza del problema con le condizioni di gara come descritto sopra:

inserisci la descrizione dell'immagine qui

Per risolvere questo problema è ansible utilizzare un blocco sincronizzato Java . Un blocco sincronizzato garantisce che solo un thread può entrare in una data sezione critica del codice in un dato momento. I blocchi sincronizzati garantiscono inoltre che tutte le variabili accessibili all’interno del blocco sincronizzato vengano lette dalla memoria principale, e quando il thread esce dal blocco sincronizzato, tutte le variabili aggiornate saranno nuovamente scaricate nella memoria principale, indipendentemente dal fatto che la variabile sia dichiarata volatile o non.