JavaRush /Java Blog /Random-IT /Gestire la volatilità
lexmirnov
Livello 29
Москва

Gestire la volatilità

Pubblicato nel gruppo Random-IT

Linee guida per l'utilizzo delle variabili volatili

Di Brian Goetz, 19 giugno 2007 Originale: Gestione della volatilità Le variabili volatili in Java possono essere chiamate "luce sincronizzata"; Richiedono meno codice da utilizzare rispetto ai blocchi sincronizzati, spesso funzionano più velocemente, ma possono fare solo una frazione di ciò che fanno i blocchi sincronizzati. Questo articolo presenta diversi modelli per utilizzare volatile in modo efficace e alcuni avvertimenti su dove non utilizzarlo. I blocchi hanno due caratteristiche principali: mutua esclusione (mutex) e visibilità. L'esclusione reciproca significa che un blocco può essere mantenuto solo da un thread alla volta e questa proprietà può essere utilizzata per implementare protocolli di controllo dell'accesso per le risorse condivise in modo che solo un thread alla volta le utilizzi. La visibilità è un problema più sottile, il suo scopo è garantire che le modifiche apportate alle risorse pubbliche prima del rilascio del blocco siano visibili al thread successivo che assume il controllo di quel blocco. Se la sincronizzazione non garantisse la visibilità, i thread potrebbero ricevere valori obsoleti o errati per le variabili pubbliche, il che porterebbe a una serie di seri problemi.
Variabili volatili
Le variabili volatili hanno le proprietà di visibilità di quelle sincronizzate, ma mancano della loro atomicità. Ciò significa che i thread utilizzeranno automaticamente i valori più attuali delle variabili volatili. Possono essere utilizzati per la thread safety , ma in un insieme di casi molto limitato: quelli che non introducono relazioni tra più variabili o tra valori attuali e futuri di una variabile. Pertanto, volatile da solo non è sufficiente per implementare un contatore, un mutex o qualsiasi classe le cui parti immutabili siano associate a più variabili (ad esempio, "start <=end"). Puoi scegliere i blocchi volatili per uno dei due motivi principali: semplicità o scalabilità. Alcuni costrutti linguistici sono più facili da scrivere come codice di programma, e successivamente da leggere e comprendere, quando utilizzano variabili volatili anziché lock. Inoltre, a differenza dei lock, non possono bloccare un thread e sono quindi meno soggetti a problemi di scalabilità. Nelle situazioni in cui sono presenti molte più letture che scritture, le variabili volatili possono fornire vantaggi in termini di prestazioni rispetto ai blocchi.
Condizioni per il corretto utilizzo del volatile
Puoi sostituire i blocchi con quelli volatili in un numero limitato di circostanze. Per essere thread-safe, entrambi i criteri devono essere soddisfatti:
  1. Ciò che viene scritto in una variabile è indipendente dal suo valore corrente.
  2. La variabile non partecipa agli invarianti con altre variabili.
In poche parole, queste condizioni significano che i valori validi che possono essere scritti su una variabile volatile sono indipendenti da qualsiasi altro stato del programma, compreso lo stato corrente della variabile. La prima condizione esclude l'uso di variabili volatili come contatori thread-safe. Sebbene l'incremento (x++) sembri una singola operazione, in realtà è un'intera sequenza di operazioni di lettura-modifica-scrittura che devono essere eseguite atomicamente, cosa che volatile non fornisce. Un'operazione valida richiederebbe che il valore di x rimanga lo stesso durante tutta l'operazione, cosa che non può essere ottenuta utilizzando volatile. (Tuttavia, se è possibile garantire che il valore venga scritto da un solo thread, la prima condizione può essere omessa.) Nella maggior parte delle situazioni, la prima o la seconda condizione verranno violate, rendendo le variabili volatili un approccio meno comunemente utilizzato per ottenere la sicurezza del thread rispetto a quelle sincronizzate. Il Listato 1 mostra una classe non thread-safe con un intervallo di numeri. Contiene un invariante: il limite inferiore è sempre inferiore o uguale a quello superiore. @NotThreadSafe public class NumberRange { private int lower, upper; public int getLower() { return lower; } public int getUpper() { return upper; } public void setLower(int value) { if (value > upper) throw new IllegalArgumentException(...); lower = value; } public void setUpper(int value) { if (value < lower) throw new IllegalArgumentException(...); upper = value; } } Poiché le variabili di stato dell'intervallo sono limitate in questo modo, non sarà sufficiente rendere volatili i campi inferiore e superiore per garantire che la classe sia thread-safe; sarà comunque necessaria la sincronizzazione. Altrimenti prima o poi sarai sfortunato e due thread che eseguono setLower() e setUpper() con valori inappropriati possono portare l'intervallo a uno stato incoerente. Ad esempio, se il valore iniziale è (0, 5), il thread A chiama setLower(4) e allo stesso tempo il thread B chiama setUpper(3), queste operazioni interlacciate genereranno un errore, sebbene entrambe supereranno il controllo questo dovrebbe proteggere l'invariante. Di conseguenza, l'intervallo sarà (4, 3) - valori errati. Dobbiamo rendere setLower() e setUpper() atomici rispetto ad altre operazioni su range - e rendere volatili i campi non lo farà.
Considerazioni sulle prestazioni
Il primo motivo per utilizzare volatile è la semplicità. In alcune situazioni, utilizzare tale variabile è semplicemente più semplice che utilizzare il blocco ad essa associato. Il secondo motivo sono le prestazioni, a volte il volatile funzionerà più velocemente dei blocchi. È estremamente difficile fare affermazioni precise e onnicomprensive come "X è sempre più veloce di Y", soprattutto quando si tratta delle operazioni interne della Java Virtual Machine. (Ad esempio, la JVM potrebbe rilasciare completamente il blocco in alcune situazioni, rendendo difficile discutere in modo astratto i costi della volatilità rispetto alla sincronizzazione). Tuttavia, sulla maggior parte delle architetture di processore moderne, il costo della lettura delle variabili volatili non è molto diverso dal costo della lettura delle variabili regolari. Il costo della scrittura di volatili è significativamente più alto rispetto alla scrittura di variabili regolari a causa del recinto di memoria richiesto per la visibilità, ma è generalmente più economico dell'impostazione dei blocchi.
Modelli per il corretto utilizzo del volatile
Molti esperti di concorrenza tendono a evitare del tutto l'utilizzo di variabili volatili perché sono più difficili da utilizzare correttamente rispetto ai lock. Esistono tuttavia alcuni schemi ben definiti che, se seguiti attentamente, possono essere utilizzati con sicurezza in un’ampia varietà di situazioni. Rispetta sempre le limitazioni dei volatili: usa solo volatili che sono indipendenti da qualsiasi altra cosa nel programma e questo dovrebbe impedirti di entrare in un territorio pericoloso con questi schemi.
Modello n. 1: flag di stato
Forse l'uso canonico delle variabili mutabili è costituito da semplici flag di stato booleani che indicano che si è verificato un importante evento una tantum del ciclo di vita, come il completamento dell'inizializzazione o una richiesta di arresto. Molte applicazioni includono un costrutto di controllo del tipo: "finché non siamo pronti per lo spegnimento, continua a eseguire" come mostrato nel Listato 2: È volatile boolean shutdownRequested; ... public void shutdown() { shutdownRequested = true; } public void doWork() { while (!shutdownRequested) { // do stuff } } probabile che il metodo shutdown() venga chiamato da qualche parte all'esterno del ciclo - su un altro thread - pertanto è necessaria la sincronizzazione per garantire la corretta visibilità della variabile shutdownRequested. (Può essere chiamato da un ascoltatore JMX, un ascoltatore di azioni in un thread di eventi della GUI, tramite RMI, tramite un servizio web, ecc.). Tuttavia, un ciclo con blocchi sincronizzati sarà molto più complicato di un ciclo con un flag di stato volatile come nel Listato 2. Poiché volatile semplifica la scrittura del codice e il flag di stato non dipende da nessun altro stato del programma, questo è un esempio di buon uso del volatile. La caratteristica di tali flag di stato è che di solito c'è solo una transizione di stato; il flag shutdownRequested passa da falso a vero, quindi il programma si spegne. Questo modello può essere esteso ai flag di stato che possono cambiare avanti e indietro, ma solo se il ciclo di transizione (da falso a vero a falso) avviene senza intervento esterno. Altrimenti è necessario qualche tipo di meccanismo di transizione atomica, come le variabili atomiche.
Modello n. 2: pubblicazione sicura una tantum
Gli errori di visibilità possibili quando non c'è sincronizzazione possono diventare un problema ancora più difficile quando si scrivono riferimenti a oggetti invece di valori primitivi. Senza sincronizzazione, puoi vedere il valore corrente per un riferimento a un oggetto che è stato scritto da un altro thread e vedere ancora i valori di stato obsoleti per quell'oggetto. (Questa minaccia è alla radice del problema con il famigerato blocco del doppio controllo, in cui un riferimento a un oggetto viene letto senza sincronizzazione e si rischia di vedere il riferimento effettivo ma di ottenere attraverso di esso un oggetto parzialmente costruito.) Un modo per pubblicare in modo sicuro un object è fare riferimento a un oggetto volatile. Il Listato 3 mostra un esempio in cui, durante l'avvio, un thread in background carica alcuni dati dal database. Altro codice che potrebbe tentare di utilizzare questi dati verifica se è stato pubblicato prima di provare a utilizzarlo. public class BackgroundFloobleLoader { public volatile Flooble theFlooble; public void initInBackground() { // делаем много всякого theFlooble = new Flooble(); // единственная запись в theFlooble } } public class SomeOtherClass { public void doWork() { while (true) { // чё-то там делаем... // используем theFolooble, но только если она готова if (floobleLoader.theFlooble != null) doSomething(floobleLoader.theFlooble); } } } Se il riferimento a theFlooble non fosse volatile, il codice in doWork() rischierebbe di vedere un Flooble parzialmente costruito quando si tenta di fare riferimento a theFlooble. Il requisito fondamentale per questo modello è che l'oggetto pubblicato deve essere thread-safe o effettivamente immutabile (effettivamente immutabile significa che il suo stato non cambia mai dopo la pubblicazione). Un collegamento volatile può garantire che un oggetto sia visibile nella sua forma pubblicata, ma se lo stato dell'oggetto cambia dopo la pubblicazione, è necessaria un'ulteriore sincronizzazione.
Modello n. 3: osservazioni indipendenti
Un altro semplice esempio di utilizzo sicuro di volatile è quando le osservazioni vengono periodicamente “pubblicate” per l’uso all’interno di un programma. Ad esempio, c'è un sensore ambientale che rileva la temperatura attuale. Il thread in background può leggere questo sensore ogni pochi secondi e aggiornare una variabile volatile contenente la temperatura corrente. Altri thread possono quindi leggere questa variabile, sapendo che il valore in essa contenuto è sempre aggiornato. Un altro utilizzo di questo modello è la raccolta di statistiche sul programma. Il Listato 4 mostra come il meccanismo di autenticazione può ricordare il nome dell'ultimo utente che ha effettuato l'accesso. Il riferimento lastUser verrà riutilizzato per pubblicare il valore affinché venga utilizzato dal resto del programma. public class UserManager { public volatile String lastUser; public boolean authenticate(String user, String password) { boolean valid = passwordIsValid(user, password); if (valid) { User u = new User(); activeUsers.add(u); lastUser = user; } return valid; } } Questo modello si espande sul precedente; il valore viene pubblicato per essere utilizzato altrove nel programma, ma la pubblicazione non è un evento una tantum, ma una serie di eventi indipendenti. Questo modello richiede che il valore pubblicato sia effettivamente immutabile, ovvero che il suo stato non cambi dopo la pubblicazione. Il codice che utilizza il valore deve essere consapevole che può cambiare in qualsiasi momento.
Modello n. 4: modello "fagiolo volatile".
Il modello "bean volatile" è applicabile nei framework che utilizzano JavaBeans come "strutture glorificate". Il modello “volatile bean” utilizza un JavaBean come contenitore per un gruppo di proprietà indipendenti con getter e/o setter. La logica alla base del modello "bean volatile" è che molti framework forniscono contenitori per titolari di dati mutabili (come HttpSession), ma gli oggetti inseriti in questi contenitori devono essere thread-safe. Nel modello bean volatile, tutti gli elementi dati JavaBean sono volatili e getter e setter dovrebbero essere banali: non dovrebbero contenere alcuna logica diversa da quella di ottenere o impostare la proprietà corrispondente. Inoltre, per i membri dati che sono riferimenti a oggetti, detti oggetti devono essere effettivamente immutabili. (Ciò non consente campi di riferimento di array, poiché quando un riferimento di array viene dichiarato volatile, solo quel riferimento, e non gli elementi stessi, ha la proprietà volatile.) Come con qualsiasi variabile volatile, non possono esserci invarianti o restrizioni associate alle proprietà dei JavaBean . Un esempio di JavaBean scritto utilizzando il modello "volatile bean" è mostrato nel Listato 5: @ThreadSafe public class Person { private volatile String firstName; private volatile String lastName; private volatile int age; public String getFirstName() { return firstName; } public String getLastName() { return lastName; } public int getAge() { return age; } public void setFirstName(String firstName) { this.firstName = firstName; } public void setLastName(String lastName) { this.lastName = lastName; } public void setAge(int age) { this.age = age; } }
Modelli volatili più complessi
I modelli nella sezione precedente coprono la maggior parte dei casi comuni in cui l'utilizzo di volatile è ragionevole e ovvio. Questa sezione esamina un modello più complesso in cui volatile può fornire un vantaggio in termini di prestazioni o scalabilità. I modelli volatili più avanzati possono essere estremamente fragili. È fondamentale che le tue ipotesi siano attentamente documentate e che questi modelli siano fortemente incapsulati, perché anche le modifiche più piccole possono rompere il tuo codice! Inoltre, dato che il motivo principale per casi d'uso volatili più complessi sono le prestazioni, assicurati di avere effettivamente una chiara necessità del miglioramento delle prestazioni previsto prima di utilizzarli. Questi modelli sono compromessi che sacrificano la leggibilità o la facilità di manutenibilità per possibili miglioramenti delle prestazioni: se non hai bisogno del miglioramento delle prestazioni (o non puoi dimostrare di averne bisogno con un programma di misurazione rigoroso), allora probabilmente è un cattivo affare perché quello stai rinunciando a qualcosa di prezioso e ottieni qualcosa di meno in cambio.
Modello n. 5: blocco di lettura-scrittura economico
A questo punto dovresti essere ben consapevole che volatile è troppo debole per implementare un contatore. Poiché ++x è essenzialmente una riduzione di tre operazioni (lettura, aggiunta, memorizzazione), se le cose vanno male, perderai il valore aggiornato se più thread tentano di incrementare il contatore volatile contemporaneamente. Tuttavia, se il numero di letture rispetto alle modifiche è notevolmente superiore, è possibile combinare il blocco intrinseco e le variabili volatili per ridurre il sovraccarico complessivo del percorso del codice. Il Listato 6 mostra un contatore thread-safe che utilizza sincronizzato per garantire che l'operazione di incremento sia atomica e utilizza volatile per garantire che il risultato corrente sia visibile. Se gli aggiornamenti non sono frequenti, questo approccio può migliorare le prestazioni poiché i costi di lettura sono limitati alle letture volatili, che generalmente sono più economiche rispetto all'acquisizione di un blocco senza conflitti. @ThreadSafe public class CheesyCounter { // Employs the cheap read-write lock trick // All mutative operations MUST be done with the 'this' lock held @GuardedBy("this") private volatile int value; public int getValue() { return value; } public synchronized int increment() { return value++; } } Il motivo per cui questo metodo è chiamato "blocco di lettura-scrittura economico" è perché si utilizzano meccanismi di temporizzazione diversi per le letture e le scritture. Poiché le operazioni di scrittura in questo caso violano la prima condizione dell'utilizzo di volatile, non è possibile utilizzare volatile per implementare un contatore in modo sicuro: è necessario utilizzare un blocco. Tuttavia, puoi utilizzare volatile per rendere visibile il valore corrente durante la lettura, quindi utilizzare un blocco per tutte le operazioni di modifica e volatile per le operazioni di sola lettura. Se un blocco consente solo a un thread alla volta di accedere a un valore, le letture volatili ne consentono più di uno, quindi quando usi volatile per proteggere la lettura, ottieni un livello di scambio più elevato rispetto a quando utilizzi un blocco su tutto il codice: e legge e registra. Tuttavia, bisogna essere consapevoli della fragilità di questo modello: con due meccanismi di sincronizzazione concorrenti, può diventare molto complesso se si va oltre l’applicazione più elementare di questo modello.
Riepilogo
Le variabili volatili sono una forma di sincronizzazione più semplice ma più debole rispetto al blocco, che in alcuni casi fornisce prestazioni o scalabilità migliori rispetto al blocco intrinseco. Se soddisfi le condizioni per un utilizzo sicuro di volatile - una variabile è veramente indipendente sia dalle altre variabili che dai suoi valori precedenti - a volte puoi semplificare il codice sostituendo sincronizzato con volatile. Tuttavia, il codice che utilizza volatile è spesso più fragile del codice che utilizza il blocco. I modelli qui suggeriti coprono i casi più comuni in cui la volatilità è un’alternativa ragionevole alla sincronizzazione. Seguendo questi schemi – e facendo attenzione a non spingerli oltre i propri limiti – è possibile utilizzare in sicurezza i volatili nei casi in cui forniscono benefici.
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION