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){
}
}
}
}
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) {
}
}
}
}
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) {
synchronized (LOCK3) {
synchronized (LOCK2) {
}
}
}
}
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
ocui si fa riferimento nel momento in cui è stato raggiunto il blocco sincronizzato. Se l'oggetto cheofa riferimento a modifiche, un altro thread può arrivare ed eseguire il blocco di codice sincronizzato.