Sincronizzazione del campo non finale

Un avviso viene visualizzato ogni volta che si sincronizza su un campo di class non finale. Ecco il codice:

public class X { private Object o; public void setO(Object o) { this.o = o; } public void x() { synchronized (o) // synchronization on a non-final field { } } } 

così ho cambiato la codifica nel modo seguente ..

  public class X { private final Object o; public X() { o = new Object(); } public void x() { synchronized (o) { } } } 

Non sono sicuro che il codice precedente sia il modo corretto per sincronizzare su un campo di class non finale. Come posso sincronizzare un campo non finale?

Prima di tutto, ti incoraggio a sforzarti davvero per affrontare i problemi di concorrenza su un livello più alto di astrazione, ovvero risolverlo usando classi da java.util.concurrent come ExecutorServices, Callables, Futures ecc.

Detto questo, non c’è niente di sbagliato nel sincronizzare su un campo non finale di per sé. Hai solo bisogno di tenere a mente che se il riferimento all’object cambia, la stessa sezione di codice può essere eseguita in parallelo . Ad esempio, se un thread esegue il codice nel blocco sincronizzato e qualcuno chiama setO(...) , un altro thread può eseguire contemporaneamente lo stesso blocco sincronizzato sulla stessa istanza .

Sincronizza sull’object a cui devi accedere in modo esclusivo (o, meglio ancora, un object “custodendolo”).

Non è davvero una buona idea, perché i blocchi sincronizzati non sono più sincronizzati in modo coerente.

Supponendo che i blocchi sincronizzati siano pensati per garantire che solo un thread acceda ad alcuni dati condivisi alla volta, considera:

  • La filettatura 1 entra nel blocco sincronizzato. Sì, ha accesso esclusivo ai dati condivisi …
  • Thread 2 chiama setO ()
  • Thread 3 (o ancora 2 …) entra nel blocco sincronizzato. Eek! Pensa che abbia accesso esclusivo ai dati condivisi, ma il thread 1 ne sta ancora furtling …

Perché vorresti che succedesse? Forse ci sono alcune situazioni molto specializzate in cui ha senso … ma tu dovresti presentarmi un caso d’uso specifico (insieme a modi per mitigare il tipo di scenario che ho dato sopra) prima che sarei felice con esso.

Sono d’accordo con uno dei commenti di John: Devi sempre usare un manichino di blocco finale mentre accedi a una variabile non finale per evitare incoerenze nel caso delle modifiche di riferimento della variabile. Quindi, in tutti i casi e come prima regola empirica:

Regola n. 1: se un campo non è definitivo, usa sempre un manichino di blocco finale (privato).

Motivo n. 1: tieni premuto il lucchetto e modifica il riferimento della variabile da solo. Un altro thread in attesa al di fuori del blocco sincronizzato sarà in grado di accedere al blocco protetto.

Motivo n. 2: tieni premuto il lucchetto e un altro thread cambia il riferimento della variabile. Il risultato è lo stesso: un altro thread può entrare nel blocco protetto.

Ma quando si utilizza un manichino di blocco finale, c’è un altro problema : si potrebbero ottenere dati errati, poiché il proprio object non finale verrà sincronizzato con la RAM solo quando si chiama synchronize (object). Quindi, come seconda regola empirica:

Regola n. 2: quando si blocca un object non finale è sempre necessario eseguire entrambe le operazioni: utilizzando un manichino di blocco finale e il blocco dell’object non finalizzato per motivi di sincronizzazione RAM. (L’unica alternativa sarà dichiarare volatile tutti i campi dell’object!)

Queste serrature sono anche chiamate “serrature annidate”. Si noti che è necessario chiamarli sempre nello stesso ordine, altrimenti si otterrà un blocco morto :

 public class X { private final LOCK; private Object o; public void setO(Object o){ this.o = o; } public void x() { synchronized (LOCK) { synchronized(o){ //do something with o... } } } } 

Come puoi vedere, scrivo le due serrature direttamente sulla stessa linea, perché appartengono sempre insieme. In questo modo, potresti persino fare 10 blocchi di nidificazione:

 synchronized (LOCK1) { synchronized (LOCK2) { synchronized (LOCK3) { synchronized (LOCK4) { //entering the locked space } } } } 

Nota che questo codice non si interromperà se acquisisci un blocco interno come synchronized (LOCK3) da un altro thread. Ma si romperà se chiami in un altro thread qualcosa del genere:

 synchronized (LOCK4) { synchronized (LOCK1) { //dead lock! synchronized (LOCK3) { synchronized (LOCK2) { //will never enter here... } } } } 

C’è solo un modo per aggirare questi blocchi nidificati mentre si gestiscono i campi non finali:

Regola n. 2 – Alternativa: dichiara tutti i campi dell’object come volatili. (Non parlerò qui degli svantaggi di fare ciò, ad esempio impedendo l’archiviazione nelle cache a livello x anche per le letture, ecc.)

Quindi, aioobe ha perfettamente ragione: usa java.util.concurrent. O inizia a capire tutto sulla sincronizzazione e fallo da solo con i blocchi annidati. 😉

Per ulteriori dettagli sul perché la sincronizzazione nei campi non finali si interrompa, dai uno sguardo al mio caso di test: https://stackoverflow.com/a/21460055/2012947

E per maggiori dettagli sul motivo per cui è necessario sincronizzare il tutto a causa della RAM e delle cache date un’occhiata qui: https://stackoverflow.com/a/21409975/2012947

Se o non cambia mai per la durata di un’istanza di X , la seconda versione è migliore, indipendentemente dal fatto che la sincronizzazione sia coinvolta.

Ora, se c’è qualcosa di sbagliato nella prima versione è imansible rispondere senza sapere cos’altro sta succedendo in quella class. Tenderei ad essere d’accordo con il compilatore che sembra incline agli errori (non ripeterò quello che hanno detto gli altri).

Aggiungendo solo i miei due centesimi: ho ricevuto questo avvertimento quando ho usato il componente che viene istanziato tramite il designer, quindi il campo non può essere davvero definitivo, perché il costruttore non può prendere parametri. In altre parole, avevo un campo quasi finale senza la parola chiave finale.

Penso che sia per questo che è solo un avvertimento: probabilmente stai facendo qualcosa di sbagliato, ma potrebbe anche essere giusto.

Non sto davvero vedendo la risposta corretta qui, cioè, è perfettamente giusto farlo.

Non sono nemmeno sicuro del perché sia ​​un avvertimento, non c’è niente di sbagliato in questo. La JVM si assicura di ottenere qualche object valido indietro (o null) quando leggi un valore e puoi sincronizzarlo su qualsiasi object.

Se si prevede di modificare effettivamente il blocco mentre è in uso (a differenza, ad esempio, cambiandolo da un metodo init, prima di iniziare a utilizzarlo), è necessario creare la variabile che si intende modificare volatile . Quindi tutto ciò che devi fare è sincronizzare sia il vecchio che il nuovo object, e puoi tranquillamente cambiare il valore

 public volatile Object lock; 

 synchronized (lock) { synchronized (newObject) { lock = newObject; } } 

Là. Non è complicato, scrivere codice con blocchi (mutex) è abbastanza semplice. Scrivere codice senza di loro (blocco codice libero) è ciò che è difficile.

EDIT: Quindi questa soluzione (come suggerito da Jon Skeet) potrebbe avere un problema con l’atomicità dell’implementazione di “synchronized (object) {}” mentre il riferimento all’object sta cambiando. Ho chiesto separatamente e secondo Mr. Erickson non è thread-safe – vedi: sta entrando in blocco sincronizzato atomico? . Quindi prendi come esempio come NON farlo – con i collegamenti perché;)

Vedere il codice come funzionerebbe se synchronized () fosse atomico:

 public class Main { static class Config{ char a='0'; char b='0'; public void log(){ synchronized(this){ System.out.println(""+a+","+b); } } } static Config cfg = new Config(); static class Doer extends Thread { char id; Doer(char id) { this.id = id; } public void mySleep(long ms){ try{Thread.sleep(ms);}catch(Exception ex){ex.printStackTrace();} } public void run() { System.out.println("Doer "+id+" beg"); if(id == 'X'){ synchronized (cfg){ cfg.a=id; mySleep(1000); // do not forget to put synchronize(cfg) over setting new cfg - otherwise following will happend // here it would be modifying different cfg (cos Y will change it). // Another problem would be that new cfg would be in parallel modified by Z cos synchronized is applied on new object cfg.b=id; } } if(id == 'Y'){ mySleep(333); synchronized(cfg) // comment this and you will see inconsistency in log - if you keep it I think all is ok { cfg = new Config(); // introduce new configuration // be aware - don't expect here to be synchronized on new cfg! // Z might already get a lock } } if(id == 'Z'){ mySleep(666); synchronized (cfg){ cfg.a=id; mySleep(100); cfg.b=id; } } System.out.println("Doer "+id+" end"); cfg.log(); } } public static void main(String[] args) throws InterruptedException { Doer X = new Doer('X'); Doer Y = new Doer('Y'); Doer Z = new Doer('Z'); X.start(); Y.start(); Z.start(); } } 

AtomicReference si adatta alle tue esigenze.

Dalla documentazione java sul pacchetto atomico :

Un piccolo toolkit di classi che supportano la programmazione thread-safe su singole variabili. In sostanza, le classi in questo pacchetto estendono la nozione di valori volatili, campi ed elementi di matrice a quelli che forniscono anche un’operazione di aggiornamento condizionale atomico del modulo:

 boolean compareAndSet(expectedValue, updateValue); 

Codice d’esempio:

 String initialReference = "value 1"; AtomicReference someRef = new AtomicReference(initialReference); String newReference = "value 2"; boolean exchanged = someRef.compareAndSet(initialReference, newReference); System.out.println("exchanged: " + exchanged); 

Nell’esempio sopra, sostituisci String con il tuo Object

Questione SE correlata:

Quando utilizzare AtomicReference in Java?