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
o
cui si fa riferimento nel momento in cui è stato raggiunto il blocco sincronizzato. Se l'oggetto cheo
fa riferimento a modifiche, un altro thread può arrivare ed eseguire il blocco di codice sincronizzato.