Sincronizzazione del campo non finale


91

Viene visualizzato un avviso ogni volta che eseguo la sincronizzazione su un campo di classe 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  
     {  
     }  
   }  
 } 

quindi 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 sopra sia il modo corretto per sincronizzare su un campo di classe non finale. Come posso sincronizzare un campo non finale?

Risposte:


127

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

Detto questo, non c'è niente di sbagliato nella sincronizzazione su un campo non finale di per sé. È sufficiente tenere presente che se il riferimento all'oggetto cambia, la stessa sezione di codice potrebbe essere eseguita in parallelo . Ad esempio, se un thread esegue il codice nel blocco sincronizzato e qualcuno lo chiama setO(...), un altro thread può eseguire lo stesso blocco sincronizzato sulla stessa istanza contemporaneamente.

Sincronizza sull'oggetto a cui hai bisogno dell'accesso esclusivo (o, meglio ancora, un oggetto dedicato a custodirlo)


1
Sto dicendo che, se sincronizzi su un campo non finale, dovresti essere consapevole del fatto che lo snippet di codice viene eseguito con accesso esclusivo all'oggetto a ocui si fa riferimento nel momento in cui è stato raggiunto il blocco sincronizzato. Se l'oggetto che ofa riferimento a modifiche, un altro thread può arrivare ed eseguire il blocco di codice sincronizzato.
aioobe

42
Non sono d'accordo con la tua regola empirica: preferisco sincronizzare su un oggetto il cui unico scopo è proteggere un altro stato. Se non stai mai facendo nulla con un oggetto diverso dal bloccarlo, sai per certo che nessun altro codice può bloccarlo. Se si blocca un oggetto "reale" di cui poi si chiamano i metodi, anche quell'oggetto può sincronizzarsi su se stesso, il che rende più difficile ragionare sul blocco.
Jon Skeet

9
Come ho detto nella mia risposta, penso che dovrei averlo giustificato con molta attenzione per me, perché vorresti fare una cosa del genere. E non consiglierei nemmeno la sincronizzazione this: consiglierei di creare una variabile finale nella classe esclusivamente ai fini del blocco , che impedisce a chiunque altro di bloccare lo stesso oggetto.
Jon Skeet

1
Questo è un altro buon punto e sono d'accordo; il bloccaggio su una variabile non finale necessita sicuramente di un'attenta giustificazione.
aioobe

Non sono sicuro dei problemi di visibilità della memoria relativi alla modifica di un oggetto utilizzato per la sincronizzazione. Penso che potresti avere grossi problemi a cambiare un oggetto e poi fare affidamento sul codice che vede la modifica correttamente in modo che "la stessa sezione di codice possa essere eseguita in parallelo". Non sono sicuro di quali garanzie siano estese dal modello di memoria alla visibilità della memoria dei campi utilizzati per il blocco, in contrasto con le variabili a cui si accede all'interno del blocco di sincronizzazione. La mia regola pratica sarebbe, se sincronizzi qualcosa, dovrebbe essere definitivo.
Mike Q

47

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

Supponendo che i blocchi sincronizzati abbiano lo scopo di garantire che solo un thread acceda ad alcuni dati condivisi alla volta, considera:

  • Il thread 1 entra nel blocco sincronizzato. Sì, ha accesso esclusivo ai dati condivisi ...
  • Il thread 2 chiama setO ()
  • Il thread 3 (o ancora 2 ...) entra nel blocco sincronizzato. Eek! Pensa di avere accesso esclusivo ai dati condivisi, ma il thread 1 lo sta ancora cercando ...

Perché si vuole che questo accada? Forse ci sono alcune situazioni molto specializzate in cui ha senso ... ma dovresti presentarmi con un caso d'uso specifico (insieme a modi per mitigare il tipo di scenario che ho fornito sopra) prima di essere soddisfatto esso.


2
@ aioobe: Ma il thread 1 potrebbe ancora eseguire del codice che modifica l'elenco (e fa spesso riferimento a o) - e durante la sua esecuzione inizia a mutare un elenco diverso. Come sarebbe una buona idea? Penso che siamo fondamentalmente in disaccordo sul fatto che sia o meno una buona idea bloccare gli oggetti che tocchi in altri modi. Preferirei essere in grado di ragionare sul mio codice senza alcuna conoscenza di cosa fa l'altro codice in termini di blocco.
Jon Skeet

2
@Felype: Sembra che dovresti porre una domanda più dettagliata come domanda separata, ma sì, spesso creerei oggetti separati proprio come lucchetti.
Jon Skeet

3
@VitBernatik: No. Se il thread X inizia a modificare la configurazione, il thread Y cambia il valore della variabile da sincronizzare, quindi il thread Z inizia a modificare la configurazione, quindi sia X che Z modificheranno la configurazione allo stesso tempo, il che non è corretto .
Jon Skeet

1
In breve, è più sicuro dichiarare sempre tali oggetti di blocco definitivi, giusto?
Sant'Antario

2
@LinkTheProgrammer: "Un metodo sincronizzato sincronizza ogni singolo oggetto nell'istanza" - no, non lo fa. Semplicemente non è vero e dovresti rivedere la tua comprensione della sincronizzazione.
Jon Skeet

12

Sono d'accordo con uno dei commenti di John: devi sempre utilizzare un lock dummy finale durante l'accesso a una variabile non finale per evitare incongruenze in caso di modifiche al riferimento della variabile. Quindi in ogni caso e come prima regola pratica:

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

Motivo n. 1: tieni premuto il lucchetto e cambi il riferimento della variabile da solo. Un altro thread in attesa al di fuori del blocco sincronizzato potrà entrare nel blocco protetto.

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

Ma quando si utilizza un lock dummy finale, c'è un altro problema : potresti ottenere dati errati, perché il tuo oggetto non finale verrà sincronizzato con la RAM solo quando si chiama synchronize (object). Quindi, come seconda regola pratica:

Regola n. 2: quando si blocca un oggetto non finale è sempre necessario eseguire entrambe le operazioni: utilizzare un blocco fittizio finale e il blocco dell'oggetto non finale per motivi di sincronizzazione RAM. (L'unica alternativa sarà dichiarare tutti i campi dell'oggetto come volatili!)

Questi blocchi sono anche chiamati "blocchi nidificati". Nota che devi chiamarli sempre nello stesso ordine, altrimenti otterrai un dead lock :

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 i due lucchetti direttamente sulla stessa riga, perché stanno sempre insieme. In questo modo, potresti persino eseguire 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 interromperà se chiami in un altro thread qualcosa del genere:

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

Esiste solo una soluzione alternativa a tali blocchi annidati durante la gestione dei campi non finali:

Regola # 2 - Alternativa: dichiara tutti i campi dell'oggetto come volatili. (Non parlerò qui degli svantaggi di farlo, ad esempio impedire qualsiasi archiviazione nelle cache di livello x anche per le letture, ecc.)

Quindi quindi aioobe ha ragione: usa java.util.concurrent. Oppure inizia a capire tutto sulla sincronizzazione e fallo da solo con blocchi annidati. ;)

Per maggiori dettagli sul motivo per cui la sincronizzazione sui campi non finali si interrompe, dai un'occhiata al mio caso di test: https://stackoverflow.com/a/21460055/2012947

E per maggiori dettagli sul motivo per cui è necessario sincronizzarsi a causa della RAM e delle cache, dai un'occhiata qui: https://stackoverflow.com/a/21409975/2012947


1
Penso che devi avvolgere il setter di ocon un (LOCK) sincronizzato per stabilire una relazione "accade prima" tra l'impostazione e l'oggetto di lettura o. Sto discutendo questo in una domanda simile mia: stackoverflow.com/questions/32852464/...
Petrakeas

Uso dataObject per sincronizzare l'accesso ai membri di dataObject. Come è sbagliato? Se dataObject inizia a puntare in un punto diverso, voglio che sia sincronizzato sui nuovi dati per impedire ai thread simultanei di modificarlo. Qualche problema con quello?
Harmen

2

Non vedo davvero la risposta corretta qui, cioè, va perfettamente bene farlo.

Non sono nemmeno sicuro del perché sia ​​un avvertimento, non c'è niente di sbagliato in esso. La JVM si assicura che tu riceva un oggetto valido (o nullo) quando leggi un valore e puoi sincronizzarlo su qualsiasi oggetto.

Se prevedi di cambiare effettivamente il blocco mentre è in uso (invece di cambiarlo ad esempio da un metodo init, prima di iniziare a usarlo), devi creare la variabile che intendi modificare volatile. Quindi tutto ciò che devi fare è sincronizzare sia il vecchio che il nuovo oggetto e puoi tranquillamente modificare il valore

public volatile Object lock;

...

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

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


Questo potrebbe non funzionare. Dire o iniziato come riferimento a O1, quindi il thread T1 blocca o (= O1) e O2 e imposta o su O2. Allo stesso tempo Thread T2 blocca O1 e attende che T1 lo sblocchi. Quando riceve il blocco O1, imposta o su O3. In questo scenario, tra il rilascio di O1 da parte di T1 e il blocco O1 di T2, O1 non è più valido per il blocco tramite o. A questo punto un altro thread può utilizzare o (= O2) per il blocco e procedere ininterrottamente in gara con T2.
GPS

2

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'oggetto sta cambiando. Ho chiesto separatamente e secondo il signor Erickson non è thread-safe - vedi: L' ingresso in blocco sincronizzato è atomico? . Quindi prendi come esempio come NON farlo - con i link perché;)

Guarda 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();
    }

}

1
Questo potrebbe essere a posto - ma non so se ci sia alcuna garanzia nel modello di memoria che il valore che si sincronizza sulla è la più recente scritto uno - non credo che ci sia alcuna garanzia di atomicamente "leggere e sincronizzare". Personalmente cerco di evitare la sincronizzazione su monitor che hanno comunque altri usi, per semplicità. (Avendo un campo separato, il codice diventa chiaramente corretto invece di dover ragionare attentamente.)
Jon Skeet

@ Jon. Grazie per la risposta! Ho sentito la tua preoccupazione. Sono d'accordo per questo caso il blocco esterno eviterebbe la questione della "atomicità sincronizzata". Quindi sarebbe preferibile. Sebbene potrebbero esserci casi in cui si desidera introdurre in runtime più configurazioni e condividere configurazioni diverse per diversi gruppi di thread (sebbene non sia il mio caso). E allora questa soluzione potrebbe diventare interessante. Ho pubblicato la domanda stackoverflow.com/questions/29217266/… di atomicità sincronizzata () - quindi vedremo se può essere utilizzata (ed è qualcuno che risponde)
Vit Bernatik

2

AtomicReference si adatta alle tue esigenze.

Dalla documentazione java sul pacchetto atomico :

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

boolean compareAndSet(expectedValue, updateValue);

Codice d'esempio:

String initialReference = "value 1";

AtomicReference<String> someRef =
    new AtomicReference<String>(initialReference);

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

Nell'esempio sopra, sostituisci Stringcon il tuoObject

Domanda SE correlata:

Quando utilizzare AtomicReference in Java?


1

Se onon cambia mai per tutta la durata di un'istanza di X, la seconda versione ha uno stile migliore indipendentemente dal fatto che sia coinvolta la sincronizzazione.

Ora, se c'è qualcosa di sbagliato nella prima versione è impossibile rispondere senza sapere cos'altro sta succedendo in quella classe. Tenderei a concordare con il compilatore sul fatto che sembra soggetto a errori (non ripeterò ciò che hanno detto gli altri).


1

Aggiungendo solo i miei due centesimi: ho ricevuto questo avviso quando ho utilizzato un componente istanziato tramite il designer, quindi il suo campo non può essere davvero definitivo, perché il costruttore non può accettare 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.

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.