Claramente, notify
ativa (qualquer) um segmento no conjunto de espera, notifyAll
ativa todos os segmentos no conjunto de espera. A discussão a seguir deve esclarecer qualquer dúvida. notifyAll
deve ser usado na maioria das vezes. Se você não tiver certeza de qual usar, use notifyAll
. Consulte a explicação a seguir.
Leia com muito cuidado e entenda. Por favor, envie-me um e-mail se você tiver alguma dúvida.
Observe o produtor / consumidor (suposição é uma classe ProducerConsumer com dois métodos). ESTÁ QUEBRADO (porque usa notify
) - sim, PODE funcionar - mesmo na maioria das vezes, mas também pode causar um impasse - veremos o porquê:
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;
}
PRIMEIRAMENTE,
Por que precisamos de um loop while em torno da espera?
Precisamos de um while
loop caso tenhamos essa situação:
O consumidor 1 (C1) entra no bloco sincronizado e o buffer está vazio, então C1 é colocado no conjunto de espera (por meio da wait
chamada). O consumidor 2 (C2) está prestes a entrar no método sincronizado (no ponto Y acima), mas o Produtor P1 coloca um objeto no buffer e, posteriormente, chama notify
. O único encadeamento em espera é C1, portanto ele é ativado e agora tenta readquirir o bloqueio de objeto no ponto X (acima).
Agora C1 e C2 estão tentando adquirir o bloqueio de sincronização. Um deles (não-deterministicamente) é escolhido e entra no método, o outro é bloqueado (sem esperar - mas bloqueado, tentando obter o bloqueio no método). Digamos que C2 consiga o bloqueio primeiro. C1 ainda está bloqueando (tentando adquirir o bloqueio em X). C2 conclui o método e libera o bloqueio. Agora, C1 adquire o bloqueio. Adivinhe, por sorte, temos um while
loop, porque C1 executa a verificação de loop (guarda) e é impedido de remover um elemento inexistente do buffer (C2 já o conseguiu!). Se não tivéssemos um while
, teríamos um IndexArrayOutOfBoundsException
como C1 tenta remover o primeiro elemento do buffer!
AGORA,
Ok, agora, por que precisamos notificar tudo?
No exemplo produtor / consumidor acima, parece que podemos nos safar notify
. Parece assim, porque podemos provar que os guardas nos ciclos de espera para produtores e consumidores são mutuamente exclusivos. Ou seja, parece que não podemos ter um encadeamento aguardando o put
método, assim como o get
método, porque, para que isso seja verdade, o seguinte teria que ser verdadeiro:
buf.size() == 0 AND buf.size() == MAX_SIZE
(suponha que MAX_SIZE não seja 0)
No entanto, isso não é bom o suficiente, precisamos usar notifyAll
. Vamos ver porque ...
Suponha que tenhamos um buffer de tamanho 1 (para facilitar o exemplo). As etapas a seguir nos levam a um impasse. Observe que QUALQUER TEMPO em que um encadeamento é acordado com notificação, ele pode ser selecionado de maneira não determinística pela JVM - ou seja, qualquer encadeamento em espera pode ser despertado. Observe também que, quando vários threads estão bloqueando a entrada em um método (ou seja, tentando adquirir um bloqueio), a ordem de aquisição pode ser não determinística. Lembre-se também de que um encadeamento pode estar apenas em um dos métodos a qualquer momento - os métodos sincronizados permitem que apenas um encadeamento esteja executando (ou seja, mantendo o bloqueio de) qualquer método (sincronizado) na classe. Se ocorrer a seguinte sequência de eventos - resultados de conflito:
PASSO 1:
- P1 coloca 1 caractere no buffer
PASSO 2:
- Tentativas P2 put
- verifica o loop de espera - já é um caractere - aguarda
PASSO 3:
- P3 tenta put
- verifica o loop de espera - já é um caractere - aguarda
PASSO 4:
- C1 tenta obter 1 caractere
- C2 tenta obter 1 caractere - blocos na entrada do get
método
- C3 tenta obter 1 caractere - blocos na entrada do get
método
ETAPA 5:
- C1 está executando o get
método - obtém o método char, chama notify
, sai
- O notify
acorda P2
- MAS, C2 entra no método antes que o P2 possa (o P2 deve readquirir o bloqueio), portanto, o P2 bloqueia a entrada no put
método
- C2 verifica o loop de espera, não há mais caracteres no buffer, então aguarda
- C3 entra no método após C2, mas antes de P2, verifica o loop de espera, não há mais caracteres no buffer, então aguarda
PASSO 6:
- AGORA: P3, C2 e C3 estão esperando!
- Finalmente P2 adquire o bloqueio, coloca um caractere no buffer, chama notifica, sai do método
PASSO 7:
- A notificação do P2 ativa o P3 (lembre-se de que qualquer thread pode ser ativado)
- O P3 verifica a condição do loop de espera, já existe um caractere no buffer, então aguarda.
- NÃO MAIS LINHAS PARA CHAMAR NOTIFICAR E TRÊS LINHAS SUSPENSAS PERMANENTEMENTE!
SOLUÇÃO: Substitua notify
por notifyAll
no código do produtor / consumidor (acima).