Thread - synchronized e notify

8 respostas
E

Eu estou com uma dúvida e gostaria que me ajudassem…

Vou propor um problema:

public synchronized produtor(int valor)
{
   while(quantidade_elementos_buffer == buffer.lebgth)
   {
      wait();
   }

   buffer[posicao] = valor;
   quantidade_elementos_buffer++;
   notifyAll();
}

Uma thread que deseja produzir primeiro tem que adquirir o bloqueio do objeto. Faz de conta de a thread adquiriu o bloqueio e entrou no método produtor(). A thread verifica no loop while se o buffer (um vetor com n elementos) está cheio ou não. Se estiver cheio, ele espera e libera o bloqueio. Vamos considerar que ele coloca um elemento na última posição disponível no buffer.

A thread atinge a instrução notifyAll(). Depois de executada, uma thread consumidora entra no estado executável e espera ser alocada pelo Sistema Operacional para executar.

Dúvida: E se o SO decidir que a thread produtora já executou por tempo suficiente exatamente depois de executar notifyAll(), mas antes de atingir o fim do método? Nesse caso, essa thread não vai liberar o bloqueio, mas a thread consumidora pode executar. A thread consumidora tenta obter o bloqueio e não consegue. O seu estado vai para bloqueado novamente. A thread produtora novamente começa a executar, entra no while e verifica que o buffer está cheio. Nesse caso, ela entra no estado de espera e temos um deadlock. O programa trava.

Alguém pode me explicar como programa sai desse cenário que descrevi? Tem algo implícito sendo considerado que ainda não sei.

PS: não coloquei o método consumidor(), mas para entrar nele, a thread consumidora tenta obter o mesmo bloqueio de objeto que o método produtor().

8 Respostas

S

ECO2004:
Eu estou com uma dúvida e gostaria que me ajudassem…

Vou propor um problema:

public synchronized produtor(int valor)
{
   while(quantidade_elementos_buffer == buffer.lebgth)
   {
      wait();
   }

   buffer[posicao] = valor;
   quantidade_elementos_buffer++;
   notifyAll();
}

Uma thread que deseja produzir primeiro tem que adquirir o bloqueio do objeto. Faz de conta de a thread adquiriu o bloqueio e entrou no método produtor(). A thread verifica no loop while se o buffer (um vetor com n elementos) está cheio ou não. Se estiver cheio, ele espera e libera o bloqueio. Vamos considerar que ele coloca um elemento na última posição disponível no buffer.

A thread atinge a instrução notifyAll(). Depois de executada, uma thread consumidora entra no estado executável e espera ser alocada pelo Sistema Operacional para executar.

Dúvida: E se o SO decidir que a thread produtora já executou por tempo suficiente exatamente depois de executar notifyAll(), mas antes de atingir o fim do método? Nesse caso, essa thread não vai liberar o bloqueio, mas a thread consumidora pode executar. A thread consumidora tenta obter o bloqueio e não consegue. O seu estado vai para bloqueado novamente. A thread produtora novamente começa a executar, entra no while e verifica que o buffer está cheio. Nesse caso, ela entra no estado de espera e temos um deadlock. O programa trava.

Alguém pode me explicar como programa sai desse cenário que descrevi? Tem algo implícito sendo considerado que ainda não sei.

PS: não coloquei o método consumidor(), mas para entrar nele, a thread consumidora tenta obter o mesmo bloqueio de objeto que o método produtor().

hum… o wait e o notify não podem estar no mesmo método. isso dá deadlock porque se o método está em wait ele nunca chega em notifiy. O syncronized não usa o mesmo lock que wait e notifyAll. Syncronized é na classe, wait/notifify é na thread.
Depois a thread não é executada no OS, é na jvm. A destinção é importante porque o controle é da JVM, mesmo que a implementação use alguma ferramenta do OS (a antiga jvm da IBM para palm tinha threads embora o OS do palm não seja multiprocesso).

O “fim do método” é a ultima instrução dele. Não existe nada entre notifyall() e a chave final. Logo, não ha como a jvm parar no meio ( a menos que vc inclua uma nova invocação a outro método). Além disso vc colocou o meotodo como syncronized que significa “não me interrompe”. Logo a jvm não irá tirar a prioridade dessa thread enquanto estiver dentro do método.

A JVM não garante muitas em threads. O conceito é bem assíncrono mesmo. E é bastante complicado explicar aqui por causa da linha do tempo envolvida. Mas ha uns conceitos meio que mal entendidos ai , eu acho.

E

Eu dei só um exemplo de método…acabei me esquecendo do tipo de retorno.

Vou esclarecer a dúvida: Vamos supor que uma thread esteja executando no método produtor(). Essa thread insere um elemento na última posição de um vetor. Depois disso, ela continua executando o método até chegar em notifyAll(). Depois de terminar de executar esse método, mas antes de sair totalmente do método sincronizado, o Sistema Operacional decide que essa thread já executou todo o seu quantum e decide alocar outra thread para a CPU. Como o método notifyAll já foi executado pela thread produtora, uma thread consumidora (não mostrada) deixa de estar bloqueada e entra no estado de executável. Contudo, a thread produtora ainda não tinha liberado o bloqueio de monitor, pois não tinha saído totalmente do método. Quando o SO alocar a thread consumidora para executar, ela tentará adquirir o bloqueio e não vai conseguir. Essa thread consumidora vai entrar no estado bloqueado novamente. Nesse instante, o Sistema Operacional decide alocar mais uma vez a thread produtora. Essa thread ainda tem o bloqueio de monitor, assim ela o libera ao finalmente sair do método sincronizado e o tenta adquirir novamente. Ela consegue. Ela verifica no loop while se todas as posições disponíveis para produzir estão cheios. Como anteriormente ela tinha preenchido o último espaço livre, ela acaba entrando no estado de espera. Temos aí um caso de deadlock.

Tem alguma liberação implícita aí que não detectei. Por exemplo, que a thread produtora executa notifyAll() e, obrigatoriamente, impede que o SO troque de contexto até que ela saia totalmente do método sincronizado.

Entenderam?

R

quando a thread chama o wait dentro do loop ela entra em estado de espera mas antes de efetivamente “esperar” ela libera o lock que tem no objeto. Isso é um dos motivos de você ter que verificar a condição de espera em um loop ao invés de apenas um if porque como ela liberou o lock quando alguma outra thread notificar uma mudança (notify ou notifyall) e a thead que está dormindo (chamou o wait) acordar e recuperar o lock do objeto não existe garantia de que o estado do mesmo seja o mesmo já que o lock foi liberado antes da thread dormir (chamar o wait)

R

Esse esquema de liberar o lock no wait é a “liberação implícita” que você disse. Se isso não ocorresse, o sistema ficaria travado, como você disse.

Eu copiei o trecho do javadoc do método wait que diz isso:

R

é, se as duas threads utilizam o mesmo lock, então o consumidor não vai conseguir entrar no método para consumir mesmo se ele estiver em “estado wait” (chamou wait porque não tinha nada para consumir em um momento anterior) porque para poder retornar a execução do método que consome após chamar um wait a thread que estiver executando o consumidor deverá obter novamente o lock do objeto e não vai conseguir isso porque o lock está com a thread que executa o produtor, então a thread que consome continuará no wait até que o produtor libere o lock. Mesmo que o produtor consiga adquirir o lock novamente depois de produzir e antes que o consumidor possa executar (acho que esse é o caso que vc ficou na dúvida), ele entrará no while e vai verificar que o buffer está cheio, chamando o wait. Neste momento o lock do objeto será liberado e a thread que consome e estava esperando uma oportunidade para obter o lock do objeto devido à chamada ao notifyall feita num momento anterior conseguirá esse lock e poderá executar sua lógica.

E

Obrigado, pessoal!

W

Não entendi perfeitamente o que você quer (o texto ficou meio estranho). Mas acho que entendi o contexto...

Primeiro, deve-se levar em consideração o que você está sincronizando. O método produtor (que, em seu código, parece um construtor e não um método) está sincronizando a classe onde ele está inserido. Se o método consumidor não sincronizar a mesma classe, você poderá estar criando uma situação de deadlock.

Para fazer isso de maneira adequada, deve criar/definir um monitor que definirá quem acessa o recurso compartilhado de forma a evitar o deadlock.

Depois de algum tempo, passei a não utilizar métodos síncronos e sempre eleger um monitor e utilizar blocos síncronos
private Object mutex = new Object(); //Isto é um atributo da tua classe

//Isto deve ficar dentro do teu método
synchronized (mutex) {
   //seu código aqui
}
O bloco síncrono impede que o objeto seja acessado enquanto outro esteja dentro de outro bloco sincronizando o mesmo objeto. Ao utilizar um método síncrono, é equivalente a:
public void myMethod() {
   synchronized(this) {
      //seu código aqui.
   }
}
W

ECO2004:
Eu dei só um exemplo de método…acabei me esquecendo do tipo de retorno.

Vou esclarecer a dúvida: Vamos supor que uma thread esteja executando no método produtor(). Essa thread insere um elemento na última posição de um vetor. Depois disso, ela continua executando o método até chegar em notifyAll(). Depois de terminar de executar esse método, mas antes de sair totalmente do método sincronizado, o Sistema Operacional decide que essa thread já executou todo o seu quantum e decide alocar outra thread para a CPU. Como o método notifyAll já foi executado pela thread produtora, uma thread consumidora (não mostrada) deixa de estar bloqueada e entra no estado de executável. Contudo, a thread produtora ainda não tinha liberado o bloqueio de monitor, pois não tinha saído totalmente do método. Quando o SO alocar a thread consumidora para executar, ela tentará adquirir o bloqueio e não vai conseguir. Essa thread consumidora vai entrar no estado bloqueado novamente. Nesse instante, o Sistema Operacional decide alocar mais uma vez a thread produtora. Essa thread ainda tem o bloqueio de monitor, assim ela o libera ao finalmente sair do método sincronizado e o tenta adquirir novamente. Ela consegue. Ela verifica no loop while se todas as posições disponíveis para produzir estão cheios. Como anteriormente ela tinha preenchido o último espaço livre, ela acaba entrando no estado de espera. Temos aí um caso de deadlock.

Tem alguma liberação implícita aí que não detectei. Por exemplo, que a thread produtora executa notifyAll() e, obrigatoriamente, impede que o SO troque de contexto até que ela saia totalmente do método sincronizado.

Entenderam?

Agora ficou um pouco mais claro. Porém, na minha opinião, o sergiotaborda já respondeu

Criado 5 de fevereiro de 2013
Ultima resposta 5 de fev. de 2013
Respostas 8
Participantes 4