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 synchronizedper assicurarsi che notifynon venga mai chiamato tra isEmptyewait .
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 synchronizedblocchi.
Questo post è stato riscritto come articolo qui: Java: Perché aspettare deve essere chiamato in un blocco sincronizzato