Chiaramente, notify
riattiva (qualsiasi) un thread nel set di attesa, notifyAll
riattiva tutti i thread nel set di attesa. La seguente discussione dovrebbe chiarire ogni dubbio. notifyAll
dovrebbe essere usato la maggior parte del tempo. Se non si è sicuri di quale utilizzare, utilizzare notifyAll
. Si prega di consultare la spiegazione che segue.
Leggi attentamente e capisci. Vi prego di inviarmi una e-mail se avete domande.
Guarda produttore / consumatore (il presupposto è una classe ProducerConsumer con due metodi). È ROTTO (perché usa notify
) - sì PU MAY funzionare - anche la maggior parte delle volte, ma può anche causare deadlock - vedremo perché:
public synchronized void put(Object o) {
while (buf.size()==MAX_SIZE) {
wait(); // called if the buffer is full (try/catch removed for brevity)
}
buf.add(o);
notify(); // called in case there are any getters or putters waiting
}
public synchronized Object get() {
// Y: this is where C2 tries to acquire the lock (i.e. at the beginning of the method)
while (buf.size()==0) {
wait(); // called if the buffer is empty (try/catch removed for brevity)
// X: this is where C1 tries to re-acquire the lock (see below)
}
Object o = buf.remove(0);
notify(); // called if there are any getters or putters waiting
return o;
}
IN PRIMO LUOGO,
Perché abbiamo bisogno di un ciclo while che circonda l'attesa?
Abbiamo bisogno di un while
ciclo nel caso in cui otteniamo questa situazione:
Il consumatore 1 (C1) inserisce il blocco sincronizzato e il buffer è vuoto, quindi C1 viene messo nel set di attesa (tramite la wait
chiamata). Il consumatore 2 (C2) sta per immettere il metodo sincronizzato (al punto Y sopra), ma il produttore P1 inserisce un oggetto nel buffer e successivamente chiama notify
. L'unico thread in attesa è C1, quindi viene svegliato e ora tenta di riacquistare il blocco dell'oggetto nel punto X (sopra).
Ora C1 e C2 stanno tentando di acquisire il blocco della sincronizzazione. Uno di questi (non deterministico) viene scelto ed entra nel metodo, l'altro viene bloccato (non in attesa - ma bloccato, nel tentativo di acquisire il blocco sul metodo). Diciamo che C2 ottiene prima il blocco. C1 sta ancora bloccando (cercando di acquisire il blocco su X). C2 completa il metodo e rilascia il blocco. Ora, C1 acquisisce il blocco. Indovina un po ', per fortuna abbiamo un while
loop, perché C1 esegue il controllo del loop (guard) e gli viene impedito di rimuovere un elemento inesistente dal buffer (C2 lo ha già!). Se non avessimo un while
, otterremmo un IndexArrayOutOfBoundsException
come C1 tenta di rimuovere il primo elemento dal buffer!
ADESSO,
Ok, ora perché abbiamo bisogno di notifica All?
Nell'esempio produttore / consumatore sopra sembra che possiamo cavarcela notify
. Sembra così, perché possiamo dimostrare che le guardie sui circuiti di attesa per produttore e consumatore si escludono a vicenda. Cioè, sembra che non possiamo avere un thread in attesa nel put
metodo così comeget
metodo, perché, affinché ciò sia vero, allora dovrebbe essere vero quanto segue:
buf.size() == 0 AND buf.size() == MAX_SIZE
(supponiamo che MAX_SIZE non sia 0)
TUTTAVIA, questo non è abbastanza buono, ABBIAMO BISOGNO di usare notifyAll
. Vediamo perché ...
Supponiamo di avere un buffer di dimensione 1 (per rendere l'esempio facile da seguire). I seguenti passaggi ci portano a un punto morto. Si noti che QUALSIASI momento un thread viene svegliato con notifica, può essere selezionato in modo non deterministico dalla JVM, ovvero qualsiasi thread in attesa può essere svegliato. Si noti inoltre che quando più thread bloccano l'accesso a un metodo (ovvero tentando di acquisire un blocco), l'ordine di acquisizione può essere non deterministico. Ricorda anche che un thread può essere solo in uno dei metodi alla volta: i metodi sincronizzati consentono a un solo thread di eseguire (cioè mantenere il blocco di) qualsiasi metodo (sincronizzato) nella classe. Se si verifica la seguente sequenza di eventi - risultati deadlock:
PASSAGGIO 1:
- P1 inserisce 1 carattere nel buffer
PASSAGGIO 2:
- P2 tentativi put
- controlla il ciclo di attesa - già un carattere - attende
PASSAGGIO 3:
- Tentativi P3put
- verifica il ciclo di attesa - già un carattere - attende
PASSAGGIO 4:
- C1 tenta di ottenere 1 carattere
- C2 tenta di ottenere 1 carattere - blocchi all'entrata nel get
metodo
- C3 tenta di ottenere 1 carattere - blocchi all'accesso al get
metodo
PASSO 5:
- C1 sta eseguendo il get
metodo - ottiene il carattere, chiama notify
, esce dal metodo
- Il notify
P2 si sveglia
- MA, C2 entra nel metodo prima che P2 possa (P2 deve riacquisire il blocco), quindi P2 blocca quando si accede al put
metodo
- C2 controlla il ciclo di attesa, non più caratteri nel buffer, quindi attende
- C3 inserisce il metodo dopo C2, ma prima di P2, controlla il ciclo di attesa, non più caratteri nel buffer, quindi attende
PASSAGGIO 6:
- ADESSO: c'è P3, C2 e C3 in attesa!
- Infine P2 acquisisce il blocco, inserisce un carattere nel buffer, chiama la notifica, esce dal metodo
PASSAGGIO 7:
- La notifica di P2 attiva P3 (ricordare che è possibile riattivare qualsiasi thread)
- P3 verifica la condizione del ciclo di attesa, nel buffer è già presente un carattere, quindi attende.
- NON PIÙ FILETTI PER CHIAMARE NOTIFICARE E TRE FILETTI PERMANENTAMENTE SOSPESO!
SOLUZIONE: sostituire notify
con notifyAll
nel codice produttore / consumatore (sopra).