Qual è il danno potenziale se fosse possibile invocare wait()
al di fuori di un blocco sincronizzato, conservandone la semantica - sospendere il thread del chiamante?
Illustriamo quali problemi incontreremmo se wait()
potessimo essere chiamati al di fuori di un blocco sincronizzato con un esempio concreto .
Supponiamo che dovessimo implementare una coda di blocco (lo so, ce n'è già una nell'API :)
Un primo tentativo (senza sincronizzazione) potrebbe guardare qualcosa lungo le linee sottostanti
class BlockingQueue {
Queue<String> buffer = new LinkedList<String>();
public void give(String data) {
buffer.add(data);
notify(); // Since someone may be waiting in take!
}
public String take() throws InterruptedException {
while (buffer.isEmpty()) // don't use "if" due to spurious wakeups.
wait();
return buffer.remove();
}
}
Questo è ciò che potrebbe potenzialmente accadere:
Un thread consumer chiama take()
e vede che il buffer.isEmpty()
.
Prima che il thread del consumatore continui a chiamare wait()
, arriva un thread del produttore che invoca un full give()
, ovverobuffer.add(data); notify();
Il filo consumatore ora chiamare wait()
(e perdere il notify()
che è stato appena chiamato).
Se sfortunato, il thread del produttore non produrrà di più give()
a causa del fatto che il thread del consumatore non si sveglia mai e abbiamo un dead-lock.
Una volta compreso il problema, la soluzione è ovvia: utilizzare synchronized
per assicurarsi che notify
non venga mai chiamato tra isEmpty
ewait
.
Senza entrare nei dettagli: questo problema di sincronizzazione è universale. Come sottolinea Michael Borgwardt, attendere / notifica riguarda la comunicazione tra thread, quindi si finirà sempre con una condizione di gara simile a quella sopra descritta. Questo è il motivo per cui viene applicata la regola "aspetta solo dentro sincronizzato".
Un paragrafo del link pubblicato da @Willie lo riassume abbastanza bene:
È necessaria la garanzia assoluta che il cameriere e il notificatore concordino sullo stato del predicato. Il cameriere controlla lo stato del predicato ad un certo punto leggermente PRIMA di andare a dormire, ma dipende per correttezza dal fatto che il predicato è vero QUANDO va a dormire. C'è un periodo di vulnerabilità tra questi due eventi, che può interrompere il programma.
Il predicato che il produttore e il consumatore devono concordare è nell'esempio sopra buffer.isEmpty()
. E l'accordo viene risolto assicurando che l'attesa e la notifica vengano eseguite in synchronized
blocchi.
Questo post è stato riscritto come articolo qui: Java: Perché aspettare deve essere chiamato in un blocco sincronizzato