Por que wait () sempre deve estar no bloco sincronizado


257

Todos sabemos que, para invocar Object.wait(), essa chamada deve ser colocada no bloco sincronizado, caso contrário, uma IllegalMonitorStateExceptioné lançada. Mas qual é a razão para fazer essa restrição? Eu sei que wait()libera o monitor, mas por que precisamos adquiri-lo explicitamente, sincronizando um bloco específico e liberá-lo chamando wait()?

Qual é o dano potencial se fosse possível invocar wait()fora de um bloco sincronizado, mantendo sua semântica - suspendendo o encadeamento do chamador?

Respostas:


232

A wait()só faz sentido quando há também uma notify(), por isso é sempre sobre a comunicação entre os threads, e isso precisa de sincronização para funcionar corretamente. Alguém poderia argumentar que isso deveria estar implícito, mas isso não ajudaria realmente, pelo seguinte motivo:

Semanticamente, você nunca apenas wait(). Você precisa de alguma condição para ser saturado e, se não estiver, aguarde até que esteja. Então, o que você realmente faz é

if(!condition){
    wait();
}

Mas a condição está sendo definida por um encadeamento separado, portanto, para que este funcione corretamente, você precisa de sincronização.

Mais algumas coisas erradas, onde apenas porque seu thread parou de esperar não significa que a condição que você está procurando é verdadeira:

  • Você pode obter despertares espúrios (o que significa que um segmento pode acordar da espera sem nunca ter recebido uma notificação) ou

  • A condição pode ser definida, mas um terceiro encadeamento torna a condição falsa novamente quando o encadeamento em espera é ativado (e recupera o monitor).

Para lidar com esses casos, o que você realmente precisa é sempre de uma variação disso:

synchronized(lock){
    while(!condition){
        lock.wait();
    }
}

Melhor ainda, não mexa nas primitivas de sincronização e trabalhe com as abstrações oferecidas nos java.util.concurrentpacotes.


3
Há uma discussão detalhada aqui também, dizendo essencialmente a mesma coisa. coding.derkeiler.com/Archive/Java/comp.lang.java.programmer/…

1
btw, se você não deve ignorar a sinalização interrompida, o loop também deve verificar Thread.interrupted().
bestsss

2
Ainda posso fazer algo como: while (! Condition) {sincronizado (isso) {wait ();}} o que significa que ainda há uma corrida entre verificar a condição e esperar, mesmo que wait () seja chamado corretamente em um bloco sincronizado. Existe alguma outra razão por trás dessa restrição, talvez devido à maneira como ela é implementada em Java?
shrini1000

9
Outro cenário desagradável: a condição é falsa, estamos prestes a entrar em wait () e outro segmento altera a condição e chama notify (). Como ainda não estamos em wait (), perderemos esta notificação (). Em outras palavras, teste e aguarde, além de alterar e notificar, devem ser atômicos .

1
@ Nullpointer: se é um tipo que pode ser escrito atomicamente (como o booleano implícito usando-o diretamente em uma cláusula if) e não há interdependência com outros dados compartilhados, você pode declarar que é volátil. Mas você precisa disso ou da sincronização para garantir que a atualização seja visível para outros threads imediatamente.
Michael Borgwardt

283

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:

  1. Um segmento de consumidor chama take()e vê que o buffer.isEmpty().

  2. 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();

  3. O segmento do consumidor agora chamará wait()(e perderá o notify()que acabou de ser chamado).

  4. 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 synchronizedpara garantir que notifynunca seja chamado entre isEmptye 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 synchronizedblocos.


Esta postagem foi reescrita como um artigo aqui: Java: Por que a espera deve ser chamada em um bloco sincronizado


Além disso, também para garantir que as alterações feitas na condição sejam vistas imediatamente após a espera () terminar, eu acho. Caso contrário, também haverá um impasse desde que o notify () já foi chamado.
Surya Wijaya Madjid

Interessante, mas observe que apenas a chamada sincronizada na verdade nem sempre solucionará esses problemas devido à natureza "não confiável" de wait () e notify (). Leia mais aqui: stackoverflow.com/questions/21439355/… . A razão pela qual a sincronização é necessária está na arquitetura do hardware (veja minha resposta abaixo).
Marcus

mas se adicionar return buffer.remove();enquanto bloco mas depois wait();, funciona?
BobJiang

@ BobJiang, não, a discussão pode ser acordada por outros motivos que não alguém que está ligando. Em outras palavras, o buffer pode estar vazio mesmo após waitretornos.
precisa saber é

Eu tenho apenas Thread.currentThread().wait();na mainfunção cercada por try-catch for InterruptedException. Sem synchronizedbloco, isso me dá a mesma exceção IllegalMonitorStateException. O que faz chegar agora ao estado ilegal? Mas funciona dentro de um synchronizedbloco.
Shashwat

12

@Rollerball está certo. O wait()é chamado, para que o encadeamento possa aguardar a ocorrência de alguma condição quando essa wait()chamada ocorrer, o encadeamento é forçado a desistir de seu bloqueio.
Para desistir de algo, você precisa possuí-lo primeiro. O thread precisa possuir o bloqueio primeiro. Daí a necessidade de chamá-lo dentro de um synchronizedmétodo / bloco.

Sim, concordo com todas as respostas acima em relação a possíveis danos / inconsistências se você não verificou a condição no synchronizedmétodo / bloco. No entanto, como @ shrini1000 apontou, apenas chamar wait()dentro de um bloco sincronizado não impedirá que essa inconsistência aconteça.

Aqui está uma boa leitura ..


5
@Popeye Explique 'corretamente' corretamente. O seu comentário não serve para ninguém.
Marquês de Lorne

4

O problema que pode causar se você não sincronizar antes wait()é o seguinte:

  1. Se o 1º thread entrar makeChangeOnX()e verificar a condição while, e estiver true( x.metCondition()retorna false, significa que x.conditioné false), ele entrará nele. Logo antes do wait()método, outro thread vai para setConditionToTrue()e define o x.conditionpara truee notifyAll().
  2. Então, somente depois disso, o 1º thread entrará em seu wait()método (não afetado pelo notifyAll()que aconteceu alguns momentos antes). Nesse caso, o 1º thread permanecerá aguardando a execução de outro thread setConditionToTrue(), mas isso pode não acontecer novamente.

Mas se você colocar synchronizedantes dos métodos que alteram o estado do objeto, isso não acontecerá.

class A {

    private Object X;

    makeChangeOnX(){
        while (! x.getCondition()){
            wait();
            }
        // Do the change
    }

    setConditionToTrue(){
        x.condition = true; 
        notifyAll();

    }
    setConditionToFalse(){
        x.condition = false;
        notifyAll();
    }
    bool getCondition(){
        return x.condition;
    }
}

2

Todos sabemos que os métodos wait (), notify () e notifyAll () são usados ​​para comunicações entre threads. Para se livrar do sinal perdido e problemas espúrios de ativação, o thread em espera sempre espera em algumas condições. por exemplo-

boolean wasNotified = false;
while(!wasNotified) {
    wait();
}

Em seguida, notificar a variável wasNotified dos conjuntos de encadeamentos para true e notificar.

Cada encadeamento possui seu cache local, para que todas as alterações sejam escritas primeiro e depois promovidas para a memória principal gradualmente.

Se esses métodos não fossem invocados no bloco sincronizado, a variável wasNotified não seria liberada na memória principal e estaria presente no cache local do encadeamento, de modo que o encadeamento em espera continuará aguardando o sinal, embora tenha sido redefinido pela notificação do encadeamento.

Para corrigir esses tipos de problemas, esses métodos são sempre chamados dentro do bloco sincronizado, o que garante que, quando o bloco sincronizado for iniciado, tudo será lido na memória principal e liberado na memória principal antes de sair do bloco sincronizado.

synchronized(monitor) {
    boolean wasNotified = false;
    while(!wasNotified) {
        wait();
    }
}

Obrigado, espero que esclareça.


1

Isso basicamente tem a ver com a arquitetura do hardware (ou seja, RAM e caches ).

Se você não usar synchronizedjunto com wait()ou notify(), outro encadeamento poderá inserir o mesmo bloco em vez de aguardar a entrada do monitor. Além disso, ao acessar, por exemplo, um array sem um bloco sincronizado, outro thread pode não ver as alterações nele ... na verdade, outro thread não verá nenhuma alteração quando já tiver uma cópia do array no cache de nível x ( aka caches de 1º / 2º / 3º nível) do segmento que processa o núcleo da CPU.

Mas os blocos sincronizados são apenas um lado da medalha: se você realmente acessar um objeto dentro de um contexto sincronizado a partir de um contexto não sincronizado, o objeto ainda não será sincronizado, mesmo dentro de um bloco sincronizado, porque ele possui uma cópia do objeto em seu cache. Eu escrevi sobre esses problemas aqui: https://stackoverflow.com/a/21462631 e Quando um bloqueio contém um objeto não final, a referência do objeto ainda pode ser alterada por outro thread?

Além disso, estou convencido de que os caches de nível x são responsáveis ​​pela maioria dos erros de tempo de execução não reproduzíveis. Isso ocorre porque os desenvolvedores geralmente não aprendem coisas de baixo nível, como o funcionamento da CPU ou como a hierarquia de memória afeta a execução de aplicativos: http://en.wikipedia.org/wiki/Memory_hierarchy

Continua sendo um enigma por que as classes de programação não começam primeiro com a hierarquia de memória e a arquitetura da CPU. "Olá mundo" não vai ajudar aqui. ;)


1
Acabei de descobrir um site que explica de forma perfeita e aprofundada: javamex.com/tutorials/…
Marcus

Hmm .. não tenho certeza se eu sigo. Se o cache foi o único motivo para colocar a espera e a notificação dentro de sincronizadas, por que a sincronização não é colocada na implementação da espera / notificação?
aioobe 27/01

Boa pergunta, já que esperar / notificar poderia muito bem ser métodos sincronizados ... talvez os ex-desenvolvedores de Java da Sun saibam a resposta? Dê uma olhada no link acima, ou talvez isso também ajude você: docs.oracle.com/javase/specs/jls/se7/html/jls-17.html
Marcus

Um motivo pode ser: Nos primeiros dias de Java, não havia erros de compilação ao não chamar a sincronização antes de executar essas operações de multithreading. Em vez disso, havia apenas erros de tempo de execução (por exemplo, coderanch.com/t/239491/java-programmer-SCJP/certification/… ). Talvez eles realmente pensassem na @SUN que, quando os programadores estão recebendo esses erros, eles estão sendo contatados, o que pode ter lhes dado a oportunidade de vender mais servidores. Quando isso mudou? Talvez Java 5.0 ou 6.0, mas na verdade eu não me lembro de ser honesto ...
Marcus

TBH Vejo alguns problemas com sua resposta 1) Sua segunda frase não faz sentido: não importa em qual objeto um segmento esteja bloqueado. Independentemente do objeto em que dois threads sejam sincronizados, todas as alterações são tornadas visíveis. 2) Você diz que outro tópico "não" verá nenhuma alteração. Isso deve ser "pode ​​não" . 3) Não sei por que você está exibindo caches de primeiro / segundo / terceiro nível ... O que importa aqui é o que o Java Memory Model diz e que é especificado no JLS. Embora a arquitetura de hardware possa ajudar a entender por que o JLS diz o que faz, é estritamente irrelevante nesse contexto.
aioobe

0

diretamente deste tutorial do java oracle:

Quando um encadeamento chama d.wait, ele deve possuir o bloqueio intrínseco para d - caso contrário, um erro será gerado. A chamada de espera dentro de um método sincronizado é uma maneira simples de adquirir o bloqueio intrínseco.


Da pergunta que o autor fez, não parece que o autor da pergunta tenha um entendimento claro do que citei no tutorial. Além disso, minha resposta explica "Por que".
Rollerball

0

Quando você chama notify () de um objeto t, o java notifica um método t.wait () específico. Mas, como o java pesquisa e notifica um método de espera específico.

java apenas analisa o bloco de código sincronizado que foi bloqueado pelo objeto t. java não pode pesquisar o código inteiro para notificar um t.wait () específico.


0

conforme documentos:

O encadeamento atual deve possuir o monitor desse objeto. O encadeamento libera a propriedade deste monitor.

wait()O método simplesmente significa que libera a trava no objeto. Portanto, o objeto será bloqueado apenas dentro do bloco / método sincronizado. Se o encadeamento estiver fora do bloco de sincronização significa que não está bloqueado, se não estiver bloqueado, o que você lançaria no objeto?


0

Espera de encadeamento no objeto de monitoramento (objeto usado pelo bloco de sincronização). Pode haver n número de objetos de monitoramento em toda a jornada de um único encadeamento. Se o Thread aguardar fora do bloco de sincronização, não haverá nenhum objeto de monitoramento e também outro thread notificará para acessar o objeto de monitoramento, como o thread fora do bloco de sincronização saberia que foi notificado. Esse também é um dos motivos pelos quais wait (), notify () e notifyAll () estão na classe de objeto em vez da classe de encadeamento.

Basicamente, o objeto de monitoramento é um recurso comum aqui para todos os encadeamentos, e os objetos de monitoramento podem estar disponíveis apenas no bloco de sincronização.

class A {
   int a = 0;
  //something......
  public void add() {
   synchronization(this) {
      //this is your monitoring object and thread has to wait to gain lock on **this**
       }
  }
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.