Qual é o dano potencial se fosse possível invocar wait()
fora de um bloco sincronizado, mantendo sua semântica - suspendendo o encadeamento do chamador?
Vamos ilustrar quais problemas encontraríamos se wait()
pudessem ser chamados fora de um bloco sincronizado com um exemplo concreto .
Suponhamos que implementássemos uma fila de bloqueio (eu sei, já existe uma na API :)
Uma primeira tentativa (sem sincronização) pode parecer algo ao longo das linhas abaixo
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();
}
}
Isto é o que potencialmente poderia acontecer:
Um segmento de consumidor chama take()
e vê que o buffer.isEmpty()
.
Antes que o encadeamento do consumidor continue a chamar wait()
, um encadeamento produtor aparece e chama um conjunto completo give()
, ou seja,buffer.add(data); notify();
O segmento do consumidor agora chamará wait()
(e perderá o notify()
que acabou de ser chamado).
Se tiver azar, o encadeamento do produtor não produzirá mais give()
como resultado do encadeamento do consumidor nunca ser ativado e teremos um bloqueio.
Depois de entender o problema, a solução é óbvia: use synchronized
para garantir que notify
nunca seja chamado entre isEmpty
e wait
.
Sem entrar em detalhes: esse problema de sincronização é universal. Como Michael Borgwardt aponta, esperar / notificar tem tudo a ver com comunicação entre threads, portanto você sempre terá uma condição de corrida semelhante à descrita acima. É por isso que a regra "esperar apenas dentro da sincronização" é imposta.
Um parágrafo do link postado por @Willie resume muito bem:
Você precisa de uma garantia absoluta de que o garçom e o notificador concordam com o estado do predicado. O garçom verifica o estado do predicado em algum momento antes de dormir, mas depende da correção do fato de o predicado ser verdadeiro QUANDO vai dormir. Há um período de vulnerabilidade entre esses dois eventos, que podem interromper o programa.
O predicado com o qual o produtor e o consumidor precisam concordar está no exemplo acima buffer.isEmpty()
. E o contrato é resolvido garantindo que a espera e a notificação sejam executadas em synchronized
blocos.
Esta postagem foi reescrita como um artigo aqui: Java: Por que a espera deve ser chamada em um bloco sincronizado