Java: notify () vs. notifyAll () novamente

377

Se alguém pesquisar no Google por "diferença entre notify()e notifyAll()", muitas explicações aparecerão (deixando de lado os parágrafos do javadoc). Tudo se resume ao número de threads em espera sendo ativados: um dentro notify()e tudo dentro notifyAll().

No entanto (se eu entender a diferença entre esses métodos corretamente), somente um encadeamento será sempre selecionado para aquisição posterior do monitor; no primeiro caso, o selecionado pela VM, no segundo caso, o selecionado pelo planejador de encadeamentos do sistema. Os procedimentos de seleção exata para os dois (no caso geral) não são conhecidos pelo programador.

Qual é a diferença útil entre notify () e notifyAll () então? Estou esquecendo de algo?

Sergey Mikhanov
fonte
6
As bibliotecas úteis a serem usadas para simultaneidade estão nas bibliotecas de simultaneidade. Sugiro que estas sejam uma escolha melhor em quase todos os casos. A data Concurency biblioteca pré Java 5.0 (em que se foram eles foram adicionados como padrão em 2004)
Peter Lawrey
4
Eu discordo de Peter. A biblioteca de simultaneidade é implementada em Java e há muito código Java executado toda vez que você chama lock (), unlock () etc.) Você pode dar um tiro no pé usando a biblioteca de simultaneidade em vez de boa e velha synchronized, exceto por certas , casos de uso bastante raros.
Alexander Ryzhov
2
O principal mal-entendido parece ser o seguinte: ... apenas um encadeamento é sempre selecionado para aquisição posterior do monitor; no primeiro caso, o selecionado pela VM, no segundo caso, o selecionado pelo planejador de encadeamentos do sistema. A implicação é que os são essencialmente os mesmos. Embora o comportamento descrito esteja correto, o que está faltando é que, no notifyAll()caso, os outros threads após o primeiro permanecem ativos e adquirem o monitor, um por um. No notifycaso, nenhum dos outros threads é ativado. Então, funcionalmente, eles são muito diferentes!
BeeOnRope
1) Se muitos threads estiverem aguardando um objeto, e notify () é chamado apenas uma vez nesse objeto. Exceto um dos threads em espera, os threads restantes aguardam para sempre? 2) Se notify () for usado, apenas um dos muitos threads em espera começará a ser executado. Se notifyall () for usado, todos os threads em espera serão notificados, mas apenas um deles começará a ser executado; então, qual é o uso de notifyall () aqui?
Chetan Gowda
@ChetanGowda Notificando todos os threads vs Notificar exatamente apenas um thread arbitrário realmente tem uma diferença significativa até que essa diferença aparentemente sutil, mas importante nos atinja. Quando você notifica () apenas um thread, todos os outros threads estarão em estado de espera até receber uma notificação explícita. /sinal. Notificando tudo, todos os threads serão executados e concluídos em alguma ordem, um após o outro, sem qualquer notificação adicional - aqui devemos dizer que os threads estão blockede não waiting. Quando blockedseu exec é suspenso temporariamente até que outro thread esteja dentro do syncbloco.
user104309

Respostas:

248

No entanto (se eu entender bem a diferença entre esses métodos), apenas um encadeamento será sempre selecionado para aquisição adicional do monitor.

Isso não está correto. o.notifyAll()ativa todos os threads bloqueados nas o.wait()chamadas. Os threads só podem retornar o.wait()um a um, mas cada um terá a sua vez.


Simplificando, isso depende do motivo pelo qual seus threads estão aguardando para serem notificados. Deseja dizer a um dos threads em espera que algo aconteceu ou deseja contar a todos ao mesmo tempo?

Em alguns casos, todos os threads em espera podem executar ações úteis quando a espera terminar. Um exemplo seria um conjunto de threads aguardando a conclusão de uma determinada tarefa; Após a conclusão da tarefa, todos os threads em espera podem continuar com seus negócios. Nesse caso, você usaria o notifyAll () para ativar todos os threads em espera ao mesmo tempo.

Outro caso, por exemplo, bloqueio mutuamente exclusivo, apenas um dos threads em espera pode fazer algo útil após ser notificado (nesse caso, adquira o bloqueio). Nesse caso, você prefere usar o notify () . Implementado adequadamente, você também pode usar notifyAll () nessa situação, mas acionaria desnecessariamente threads que não podem fazer nada.


Em muitos casos, o código para aguardar uma condição será gravado como um loop:

synchronized(o) {
    while (! IsConditionTrue()) {
        o.wait();
    }
    DoSomethingThatOnlyMakesSenseWhenConditionIsTrue_and_MaybeMakeConditionFalseAgain();
}

Dessa forma, se uma o.notifyAll()chamada ativar mais de um encadeamento em espera, e o primeiro a retornar das o.wait()marcas deixar a condição no estado falso, os outros encadeamentos que foram despertados voltarão à espera.

Liedman
fonte
29
se você notificar apenas um segmento, mas vários estão aguardando um objeto, como a VM determina qual deles notificar?
amphibient
6
Não posso ter certeza sobre as especificações Java, mas geralmente você deve evitar fazer suposições sobre esses detalhes. Eu acho que você pode assumir que a VM fará isso de uma maneira sã e principalmente justa.
Liedman
15
Liedman está severamente errado, a especificação Java afirma explicitamente que não é garantido que o notify () seja justo. ou seja, cada chamada para notificar pode ativar o mesmo encadeamento novamente (a fila de encadeamentos no monitor NÃO É JUSTA ou FIFO). No entanto, o agendador é garantido para ser justo. É por isso que, na maioria dos casos, em que você tem mais de 2 threads, deve preferir notifyAll.
Yann TM
45
@YannTM Sou a favor de críticas construtivas, mas acho que seu tom é um pouco injusto. Eu disse explicitamente "não posso dizer com certeza" e "eu acho". Calma, você já escreveu algo há sete anos que não era 100% correto?
Liedman
10
O problema é que esta é a resposta aceita, não é uma questão de orgulho pessoal. Se você sabe que estava errado agora, edite sua resposta para dizê-lo e aponte para, por exemplo, xagyg pedagógica e resposta correta abaixo.
Yann TM
330

Claramente, notifyativa (qualquer) um segmento no conjunto de espera, notifyAllativa todos os segmentos no conjunto de espera. A discussão a seguir deve esclarecer qualquer dúvida. notifyAlldeve 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 whileloop 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 waitchamada). 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 whileloop, 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 IndexArrayOutOfBoundsExceptioncomo 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 putmétodo, assim como o getmé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 getmétodo
- C3 tenta obter 1 caractere - blocos na entrada do getmétodo

ETAPA 5:
- C1 está executando o getmétodo - obtém o método char, chama notify, sai
- O notifyacorda 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 putmé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 notifypor notifyAllno código do produtor / consumidor (acima).

xagyg
fonte
11
finnw - P3 deve verificar novamente a condição porque as notifycausas P3 (o thread selecionado neste exemplo) prosseguem a partir do ponto em que estavam aguardando (ou seja, dentro do whileloop). Existem outros exemplos que não causam conflito, no entanto, nesse caso, o uso de notifynão garante código livre de conflito. O uso de notifyAllfaz.
xagyg
4
@marcus Muito perto. Com notifyAll, cada encadeamento readquirirá o bloqueio (um de cada vez), mas observe que após um encadeamento readquirir o bloqueio e executar o método (e depois sair) ... o próximo encadeamento retoma o bloqueio, verifica o "while" e voltará a "aguardar" (dependendo da condição). Portanto, notificar ativa um segmento - como você declara corretamente. notifyAll ativa todos os threads e cada thread recupera o bloqueio, um de cada vez - verifica a condição do "while" e executa o método ou "aguarda" novamente.
xagyg
11
@xagyg, você está falando de um cenário em que cada produtor tem apenas um único caractere para armazenar? Nesse caso, seu exemplo está correto, mas não é muito interessante. Com as etapas adicionais que sugiro, você pode bloquear o mesmo sistema, mas com uma quantidade ilimitada de entrada - e é assim que esses padrões geralmente são usados ​​na realidade.
quer
3
@codeObserver Você perguntou: "A chamada notifyAll () levaria a vários threads em espera verificando a condição while () ao mesmo tempo ... e, portanto, existe a possibilidade de que, antes que o tempo seja resolvido, 2 threads já estejam fora dele causando outOfBound exceção? " Não, isso não é possível, pois, embora vários encadeamentos sejam ativados, eles não podem verificar a condição while ao mesmo tempo. Eles são obrigados a obter novamente o bloqueio (imediatamente após a espera) antes de poderem entrar novamente na seção de código e verificar novamente o tempo. Portanto, um de cada vez.
Xagyg 01/12/12
4
@xagyg nice example. Isso está fora do tópico da pergunta original; apenas por uma questão de discussão. O impasse é um problema de design (corrija-me se estiver errado). Porque você tem um bloqueio compartilhado por ambos, put e get. E a JVM não é inteligente o suficiente para chamar put após um get liberar a trava e vice-verso. O bloqueio morto acontece porque put acorda outra put, que volta a esperar () por causa de while (). Fazer duas classes (e dois bloqueios) funcionaria? . Então coloque {synchonized (get)}, se {(synchonized (put)} Em outras palavras, get vai acordar só colocar, e colocar vai acordar obter apenas.
Jay
43

Diferenças úteis:

  • Use notify () se todos os seus threads em espera forem intercambiáveis ​​(a ordem em que eles despertam não importa) ou se você tiver apenas um thread em espera. Um exemplo comum é um pool de encadeamentos usado para executar trabalhos de uma fila - quando um trabalho é adicionado, um dos encadeamentos é notificado para ativar, executar o próximo trabalho e voltar a dormir.

  • Use notifyAll () para outros casos em que os threads em espera podem ter finalidades diferentes e devem poder ser executados simultaneamente. Um exemplo é uma operação de manutenção em um recurso compartilhado, em que vários encadeamentos aguardam a conclusão da operação antes de acessar o recurso.

David Crow
fonte
19

Eu acho que depende de como os recursos são produzidos e consumidos. Se 5 objetos de trabalho estiverem disponíveis ao mesmo tempo e você tiver 5 objetos de consumidor, faria sentido ativar todos os encadeamentos usando notifyAll () para que cada um possa processar 1 objeto de trabalho.

Se você tiver apenas um objeto de trabalho disponível, qual é o sentido de ativar todos os objetos de consumidor para concorrer a esse objeto? A primeira verificação do trabalho disponível será obtida e todos os outros threads verificarão e descobrirão que eles não têm nada para fazer.

Encontrei uma ótima explicação aqui . Em resumo:

O método notify () geralmente é usado para pools de recursos , onde há um número arbitrário de "consumidores" ou "trabalhadores" que consomem recursos, mas quando um recurso é adicionado ao pool, apenas um dos consumidores ou trabalhadores em espera pode lidar com isso. O método notifyAll () é realmente usado na maioria dos outros casos. Estritamente, é necessário notificar os garçons sobre uma condição que permita que vários garçons continuem. Mas isso geralmente é difícil de saber. Portanto, como regra geral, se você não possui uma lógica específica para usar o notify (), provavelmente deve usar o notifyAll () , porque geralmente é difícil saber exatamente quais threads estarão aguardando em um objeto específico e por quê.

andyuk
fonte
11

Observe que, com os utilitários de simultaneidade, você também pode escolher entre signal()e signalAll()como esses métodos são chamados lá. Portanto, a questão permanece válida mesmo com java.util.concurrent.

Doug Lea menciona um ponto interessante em seu famoso livro : se um notify()e Thread.interrupt()acontecer ao mesmo tempo, a notificação pode realmente se perder. Se isso puder acontecer e tiver implicações dramáticas, notifyAll()é uma escolha mais segura, mesmo que você pague o preço da sobrecarga (ativando muitos threads na maioria das vezes).

cinzento
fonte
10

Pequeno resumo:

Sempre prefira notifyAll () em vez de notify (), a menos que você tenha um aplicativo massivamente paralelo em que um grande número de threads faça a mesma coisa.

Explicação:

notify () [...] ativa um único thread. Como o notify () não permite que você especifique o encadeamento acordado, ele é útil apenas em aplicativos massivamente paralelos - ou seja, programas com um grande número de encadeamentos, todos executando tarefas semelhantes. Em um aplicativo como esse, você não se importa com o encadeamento acordado.

fonte: https://docs.oracle.com/javase/tutorial/essential/concurrency/guardmeth.html

Compare notify () com notifyAll () na situação descrita acima: um aplicativo massivamente paralelo em que os threads estão fazendo a mesma coisa. Se você chamar notifyAll () nesse caso, notifyAll () induzirá a ativação (ou seja, agendamento) de um grande número de threads, muitos deles desnecessariamente (já que apenas um thread pode realmente prosseguir, a saber, o thread ao qual será concedido o monitore o objeto wait () , notify () ou notifyAll () foi chamado), desperdiçando, portanto, recursos de computação.

Portanto, se você não tiver um aplicativo em que um grande número de threads faça a mesma coisa simultaneamente, prefira notifyAll () em vez de notify () . Por quê? Porque, como outros usuários já responderam neste fórum, notifique ()

ativa um único thread que está aguardando no monitor desse objeto. [...] A escolha é arbitrária e ocorre a critério da implementação.

origem: API Java SE8 ( https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html#notify-- )

Imagine que você tem um aplicativo de consumidor produtor onde os consumidores estão prontos (ou seja, aguardando ) para consumir, os produtores estão prontos (ou seja, aguardando ) para produzir e a fila de itens (a serem produzidos / consumidos) está vazia. Nesse caso, notify () pode acordar apenas consumidores e nunca produtores, porque a escolha de quem é acordado é arbitrária . O ciclo de consumo do produtor não faria nenhum progresso, embora produtores e consumidores estejam prontos para produzir e consumir, respectivamente. Em vez disso, um consumidor é acordado (ou seja, deixando o status de espera () ), não retira um item da fila porque está vazio e notifica () outro consumidor para prosseguir.

Por outro lado, notifyAll () desperta produtores e consumidores. A escolha de quem está agendado depende do agendador. Obviamente, dependendo da implementação do agendador, o agendador também pode agendar apenas consumidores (por exemplo, se você atribuir uma prioridade muito alta aos encadeamentos do consumidor). No entanto, a suposição aqui é que o perigo do planejador agendar apenas consumidores é menor do que o perigo da JVM despertar apenas os consumidores porque qualquer planejador razoavelmente implementado não toma apenas decisões arbitrárias . Em vez disso, a maioria das implementações do planejador faz pelo menos algum esforço para evitar a fome.

ansd
fonte
9

Aqui está um exemplo. Executá-lo. Em seguida, altere um dos notifyAll () para notify () e veja o que acontece.

Classe ProducerConsumerExample

public class ProducerConsumerExample {

    private static boolean Even = true;
    private static boolean Odd = false;

    public static void main(String[] args) {
        Dropbox dropbox = new Dropbox();
        (new Thread(new Consumer(Even, dropbox))).start();
        (new Thread(new Consumer(Odd, dropbox))).start();
        (new Thread(new Producer(dropbox))).start();
    }
}

Classe Dropbox

public class Dropbox {

    private int number;
    private boolean empty = true;
    private boolean evenNumber = false;

    public synchronized int take(final boolean even) {
        while (empty || evenNumber != even) {
            try {
                System.out.format("%s is waiting ... %n", even ? "Even" : "Odd");
                wait();
            } catch (InterruptedException e) { }
        }
        System.out.format("%s took %d.%n", even ? "Even" : "Odd", number);
        empty = true;
        notifyAll();

        return number;
    }

    public synchronized void put(int number) {
        while (!empty) {
            try {
                System.out.println("Producer is waiting ...");
                wait();
            } catch (InterruptedException e) { }
        }
        this.number = number;
        evenNumber = number % 2 == 0;
        System.out.format("Producer put %d.%n", number);
        empty = false;
        notifyAll();
    }
}

Classe de consumidor

import java.util.Random;

public class Consumer implements Runnable {

    private final Dropbox dropbox;
    private final boolean even;

    public Consumer(boolean even, Dropbox dropbox) {
        this.even = even;
        this.dropbox = dropbox;
    }

    public void run() {
        Random random = new Random();
        while (true) {
            dropbox.take(even);
            try {
                Thread.sleep(random.nextInt(100));
            } catch (InterruptedException e) { }
        }
    }
}

Classe de produtores

import java.util.Random;

public class Producer implements Runnable {

    private Dropbox dropbox;

    public Producer(Dropbox dropbox) {
        this.dropbox = dropbox;
    }

    public void run() {
        Random random = new Random();
        while (true) {
            int number = random.nextInt(10);
            try {
                Thread.sleep(random.nextInt(100));
                dropbox.put(number);
            } catch (InterruptedException e) { }
        }
    }
}
Erik
fonte
8

De Joshua Bloch, o próprio Java Guru na edição Java Efetiva 2ª:

"Item 69: Prefira utilitários de concorrência para aguardar e notificar".

Steve McLeod
fonte
16
O porquê é mais importante que a fonte.
Pacerier 10/08/14
2
@ Pacerier Bem dito. Eu estaria mais interessado em descobrir os motivos também. Um possível motivo pode ser que a espera e a notificação na classe de objeto sejam baseadas em uma variável de condição implícita. Portanto, no exemplo padrão de produtor e consumidor ..... tanto o produtor quanto o consumidor aguardarão na mesma condição que pode levar a um impasse, conforme explicado por xagyg em sua resposta. Assim, uma abordagem melhor é usar 2 variáveis de condição, como explicado no docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/...
rahul
6

Espero que isso esclareça algumas dúvidas.

notify () : O método notify () ativa um thread aguardando o bloqueio (o primeiro thread que chamou wait () nesse bloqueio).

notifyAll () : O método notifyAll () ativa todos os threads que aguardam o bloqueio; a JVM seleciona um dos encadeamentos da lista de encadeamentos que aguardam o bloqueio e ativa esse encadeamento.

No caso de um único encadeamento aguardando um bloqueio, não há diferença significativa entre notify () e notifyAll (). No entanto, quando houver mais de um encadeamento aguardando pelo bloqueio, tanto no notify () quanto no notiteAll (), o encadeamento exato acordado estará sob o controle da JVM e você não poderá controlar programaticamente a ativação de um encadeamento específico.

À primeira vista, parece que é uma boa ideia ligar para notify () para ativar um thread; pode parecer desnecessário ativar todos os threads. No entanto, o problema com notify () é que o encadeamento acordado pode não ser o adequado para ser acordado (o encadeamento pode estar esperando por alguma outra condição ou a condição ainda não está satisfeita para esse encadeamento etc.). Nesse caso , o notify () pode ser perdido e nenhum outro encadeamento será ativado, levando a um tipo de conflito (a notificação é perdida e todos os outros encadeamentos aguardam a notificação - para sempre).

Para evitar esse problema , é sempre melhor chamar notifyAll () quando houver mais de um encadeamento aguardando um bloqueio (ou mais de uma condição na qual a espera é feita). O método notifyAll () ativa todos os threads, portanto, não é muito eficiente. no entanto, essa perda de desempenho é insignificante em aplicativos do mundo real.

pardeep131085
fonte
6

Existem três estados para um encadeamento.

  1. WAIT - O thread não está usando nenhum ciclo da CPU
  2. BLOQUEADO - O encadeamento está bloqueado ao tentar adquirir um monitor. Ainda pode estar usando os ciclos da CPU
  3. EXECUTANDO - O encadeamento está em execução.

Agora, quando um notify () é chamado, a JVM escolhe um encadeamento e os move para o estado BLOCKED e, portanto, para o estado RUNNING, pois não há concorrência para o objeto de monitor.

Quando um notifyAll () é chamado, a JVM seleciona todos os encadeamentos e os move para o estado BLOCKED. Todos esses encadeamentos obterão o bloqueio do objeto com prioridade. O fio que é capaz de adquirir o monitor primeiro poderá ir primeiro ao estado RUNNING e assim por diante.

S_R
fonte
Apenas uma explicação incrível.
Royatirek 02/12/19
5

Estou muito surpreso que ninguém tenha mencionado o infame problema de "despertar perdido" (pesquise no google).

Basicamente:

  1. se você tiver vários threads aguardando na mesma condição e,
  2. vários threads que podem fazer a transição do estado A para o estado B e,
  3. vários threads que podem fazer a transição do estado B para o estado A (geralmente os mesmos threads que no 1.) e,
  4. A transição do estado A para B deve notificar os threads em 1.

ENTÃO, você deve usar o notifyAll, a menos que tenha garantias comprováveis ​​de que desperdícios perdidos são impossíveis.

Um exemplo comum é uma fila FIFO simultânea em que: vários enfileiradores (1. e 3. acima) podem fazer a transição da fila de vazios para não enfraquecidos. Os enfileiradores múltiplos (2. acima) podem aguardar a condição "a fila não está vazia" vazia -> não vazio deve notificar dequeuers

Você pode escrever facilmente uma intercalação de operações na qual, a partir de uma fila vazia, 2 enfileiradores e 2 enfileiradores interagem e 1 enfileirador permanecerá adormecido.

Este é um problema sem dúvida comparável ao problema de deadlock.

NickV
fonte
Minhas desculpas, xagyg explica em detalhes. O nome do problema é "despertar perdido"
NickV 15/09/12
@Abhay Bansal: Eu acho que você está perdendo o fato de que condition.wait () libera a trava e ela é readquirida pelo thread que acorda.
NickV
4

notify()vai acordar um thread enquanto notifyAll()vai acordar tudo. Até onde eu sei, não há meio termo. Mas se você não tiver certeza do que notify()fará com seus threads, use notifyAll(). Funciona sempre como um encanto.

Spoike
fonte
4

Todas as respostas acima estão corretas, pelo que sei, então vou lhe contar outra coisa. Para o código de produção, você realmente deve usar as classes em java.util.concurrent. Há muito pouco que eles não podem fazer por você, na área de concorrência em java.


fonte
4

notify()permite escrever código mais eficiente do que notifyAll().

Considere o seguinte pedaço de código que é executado a partir de vários threads paralelos:

synchronized(this) {
    while(busy) // a loop is necessary here
        wait();
    busy = true;
}
...
synchronized(this) {
    busy = false;
    notifyAll();
}

Pode ser mais eficiente usando notify():

synchronized(this) {
    if(busy)   // replaced the loop with a condition which is evaluated only once
        wait();
    busy = true;
}
...
synchronized(this) {
    busy = false;
    notify();
}

No caso, se você tiver um grande número de threads, ou se a condição do loop de espera for dispendiosa para avaliar, notify()será significativamente mais rápida que notifyAll(). Por exemplo, se você tiver 1000 threads, 999 threads serão despertados e avaliados após o primeiro notifyAll(), 998, 997 e assim por diante. Pelo contrário, com a notify()solução, apenas um segmento será despertado.

Use notifyAll()quando precisar escolher qual thread fará o trabalho a seguir:

synchronized(this) {
    while(idx != last+1)  // wait until it's my turn
        wait();
}
...
synchronized(this) {
    last = idx;
    notifyAll();
}

Finalmente, é importante entender que, no caso de notifyAll(), o código dentro dos synchronizedblocos que foram despertados será executado sequencialmente, não todos de uma vez. Digamos que há três threads aguardando no exemplo acima e o quarto thread chama notifyAll(). Todos os três encadeamentos serão despertados, mas apenas um iniciará a execução e verificará a condição do whileloop. Se a condição for true, ela será chamada wait()novamente e somente então o segundo encadeamento começará a executar e verificará sua whilecondição de loop, e assim por diante.

Alexander Ryzhov
fonte
4

Aqui está uma explicação mais simples:

Você está correto ao usar o notify () ou notifyAll (), o resultado imediato é que exatamente um outro encadeamento adquirirá o monitor e começará a executar. (Supondo que alguns encadeamentos foram de fato bloqueados em espera () por esse objeto, outros encadeamentos não relacionados não estão absorvendo todos os núcleos disponíveis etc.) O impacto ocorre mais tarde.

Suponha que os segmentos A, B e C estivessem aguardando esse objeto e o segmento A obtenha o monitor. A diferença está no que acontece quando A libera o monitor. Se você usou o notify (), B e C ainda estão bloqueados em wait (): eles não estão esperando no monitor, eles estão esperando para serem notificados. Quando A libera o monitor, B e C ainda estão sentados, aguardando uma notificação ().

Se você usou notifyAll (), B e C avançaram além do estado "aguardar notificação" e aguardam a aquisição do monitor. Quando A libera o monitor, B ou C o adquirirá (assumindo que nenhum outro encadeamento esteja competindo por esse monitor) e começará a executar.

Steve
fonte
Explicação muito clara. O resultado desse comportamento de notify () pode levar a "Sinal Perdido" / "Notificação Perdida", levando a um impasse / nenhuma situação de progresso do estado do aplicativo.P-Producer, C-Consumer P1, P2 e C2 estão aguardando C1. As chamadas C1 notificam () e destinam-se a um produtor, mas C2 pode ser acordado e, portanto, P1 e P2 perderam a notificação e estarão aguardando uma "notificação" explícita (uma chamada de notificação ()).
user104309
4

Esta resposta é uma reescrita gráfica e simplificação da excelente resposta por xagyg , incluindo comentários de eran .

Por que usar o notifyAll, mesmo quando cada produto é destinado a um único consumidor?

Considere produtores e consumidores, simplificados da seguinte forma.

Produtor:

while (!empty) {
   wait() // on full
}
put()
notify()

Consumidor:

while (empty) {
   wait() // on empty
}
take()
notify()

Suponha que 2 produtores e 2 consumidores compartilhem um buffer de tamanho 1. A figura a seguir mostra um cenário que leva a um impasse , que seria evitado se todos os threads usados notificassem .

Cada notificação é rotulada com o segmento que está sendo ativado.

impasse devido à notificação

Marco
fonte
3

Gostaria de mencionar o que é explicado no Java Concurrency in Practice:

Primeiro ponto, seja o Notify ou o NotifyAll?

It will be NotifyAll, and reason is that it will save from signall hijacking.

Se dois encadeamentos A e B estiverem aguardando predicados de condições diferentes da mesma fila de condições e a notificação for chamada, será de responsabilidade da JVM a qual encadeamento a JVM notificará.

Agora, se a notificação foi destinada ao encadeamento A e ao encadeamento notificado da JVM B, o encadeamento B será ativado e verá que essa notificação não é útil, portanto, aguardará novamente. E o segmento A nunca chegará a saber sobre esse sinal perdido e alguém seqüestrou sua notificação.

Portanto, chamar notifyAll resolverá esse problema, mas novamente terá impacto no desempenho, pois notificará todos os threads e todos os threads competirão pelo mesmo bloqueio e envolverá a troca de contexto e, portanto, a carga na CPU. Mas devemos nos preocupar com o desempenho apenas se estiver se comportando corretamente, se o comportamento em si não estiver correto, o desempenho não será útil.

Esse problema pode ser resolvido com o uso do objeto Condition do bloqueio de bloqueio explícito, fornecido no jdk 5, pois fornece uma espera diferente para cada predicado de condição. Aqui ele se comportará corretamente e não haverá problema de desempenho, pois chamará o sinal e garantirá que apenas um encadeamento esteja aguardando essa condição

AKS
fonte
3

notify()- Seleciona um encadeamento aleatório do conjunto de espera do objeto e o coloca no BLOCKEDestado. O restante dos encadeamentos no conjunto de espera do objeto ainda está no WAITINGestado.

notifyAll()- Move todos os threads do conjunto de espera do objeto para o BLOCKEDestado. Após o uso notifyAll(), não há segmentos restantes no conjunto de espera do objeto compartilhado porque todos eles estão agora no BLOCKEDestado e não noWAITING estado.

BLOCKED- bloqueado para aquisição de bloqueio. WAITING- aguardando notificação (ou bloqueado para conclusão da associação).

fireboy91
fonte
3

Retirado do blog sobre Java eficaz:

The notifyAll method should generally be used in preference to notify. 

If notify is used, great care must be taken to ensure liveness.

Então, o que eu entendo é (do blog mencionado anteriormente, comente por "Yann TM" sobre resposta aceita e documentos Java ):

  • notify (): A JVM desperta um dos threads em espera nesse objeto. A seleção de threads é feita arbitrariamente, sem justiça. Portanto, o mesmo segmento pode ser despertado repetidamente. Portanto, o estado do sistema muda, mas nenhum progresso real é feito. Assim, criando um livelock .
  • notifyAll (): A JVM desperta todos os threads e, em seguida, todos os threads correm para o bloqueio neste objeto. Agora, o agendador da CPU seleciona um thread que adquire bloqueio nesse objeto. Esse processo de seleção seria muito melhor que a seleção da JVM. Assim, garantindo vivacidade.
rajya vardhan
fonte
2

Veja o código postado por @xagyg.

Suponha que dois encadeamentos diferentes aguardem duas condições diferentes:
O primeiro encadeamento está aguardando buf.size() != MAX_SIZEe o segundo encadeamento está aguardandobuf.size() != 0 .

Suponha que em algum momento buf.size() não seja igual a 0 . Chamadas JVM em notify()vez denotifyAll() e o primeiro encadeamento é notificado (não o segundo).

O primeiro encadeamento é acordado, verifica o buf.size()que pode retornar MAX_SIZEe volta à espera. O segundo segmento não é ativado, continua aguardando e não chama get().

alxlevin
fonte
1

notify() acorda o primeiro segmento que chamou wait() o mesmo objeto.

notifyAll()ativa todos os threads que chamaram wait()o mesmo objeto.

O thread de maior prioridade será executado primeiro.

Kshitij Banerjee
fonte
13
No caso de notify()não ser exatamente " o primeiro tópico ".
Bhesh Gurung
6
você não pode prever qual será escolhido pela VM. Só Deus sabe.
Sergii Shevchyk
Não há garantia de que será a primeira (sem fairness)
Ivan Voroshilin
Ele só ativará o primeiro encadeamento se o sistema operacional garantir isso e é provável que não. Realmente adia para o sistema operacional (e seu agendador) para determinar qual thread ativar.
Paul Stelian
1

notificar notificará apenas um encadeamento que está no estado de espera, enquanto notificar todos notificará todos os encadeamentos no estado de espera agora todos os encadeamentos notificados e todos os encadeamentos bloqueados são elegíveis para o bloqueio, dos quais apenas um receberá o bloqueio e todos os outros (incluindo aqueles que estão no estado de espera anteriormente) estarão no estado bloqueado.

Saurabh
fonte
1

Para resumir as excelentes explicações detalhadas acima, e da maneira mais simples que posso pensar, isso se deve às limitações do monitor interno da JVM, que 1) é adquirido em toda a unidade de sincronização (bloco ou objeto) e 2) não discrimina a condição específica que está sendo aguardada / notificada em / sobre.

Isso significa que, se vários encadeamentos estiverem aguardando em condições diferentes e a notificação () for usada, o encadeamento selecionado pode não ser aquele que faria progresso na condição recém-cumprida - causando esse encadeamento (e outros encadeamentos ainda aguardando que poderiam para cumprir a condição, etc.) não ser capaz de progredir e, eventualmente, morrer de fome ou interromper o programa.

Por outro lado, notifyAll () permite que todos os threads em espera adquiram novamente o bloqueio e verifiquem suas respectivas condições, permitindo, assim, o progresso.

Portanto, o notify () pode ser usado com segurança somente se for garantido qualquer thread em espera para permitir o progresso, caso seja selecionado, o que geralmente é satisfeito quando todos os threads no mesmo monitor verificam apenas uma e a mesma condição - uma situação bastante rara caso em aplicações do mundo real.

KingCode
fonte
0

Quando você chama wait () do "objeto" (esperando que o bloqueio do objeto seja adquirido), internamente, isso liberará o bloqueio nesse objeto e ajudará os outros threads a bloquearem esse "objeto"; nesse cenário, haverá mais de um encadeamento aguardando o "recurso / objeto" (considerando que os outros encadeamentos também emitiram a espera no mesmo objeto acima e, no decorrer do caminho, haverá um encadeamento que preenche o recurso / objeto e chama o notify / notifyAll).

Aqui, quando você emitir a notificação do mesmo objeto (do mesmo / outro lado do processo / código), isso liberará um encadeamento único bloqueado e em espera (nem todos os encadeamentos em espera - esse encadeamento liberado será escolhido pelo JVM Thread O agendador e todo o processo de obtenção de bloqueios no objeto são iguais aos regulares).

Se você tiver apenas um segmento que estará compartilhando / trabalhando neste objeto, não há problema em usar o método notify () sozinho na sua implementação de espera-notificação.

se você estiver em uma situação em que mais de um encadeamento lê e grava recursos / objetos com base na sua lógica de negócios, você deve ir para notifyAll ()

Agora eu estou olhando exatamente como a jvm está identificando e quebrando o thread em espera quando emitimos o notify () em um objeto ...

Deepak sai
fonte
0

Embora existam respostas sólidas acima, estou surpreso com o número de confusões e mal-entendidos que li. Provavelmente, isso prova a idéia de que se deve usar o java.util.concurrent, tanto quanto possível, em vez de tentar escrever seu próprio código simultâneo quebrado. De volta à pergunta: para resumir, a melhor prática hoje é EVITAR notify () em TODAS as situações devido ao problema de ativação perdida. Qualquer pessoa que não entenda isso não deve escrever código de simultaneidade de missão crítica. Se você estiver preocupado com o problema de agrupamento, uma maneira segura de ativar um encadeamento de cada vez é: 1. Construir uma fila de espera explícita para os encadeamentos em espera; 2. Faça com que cada um dos encadeamentos na fila aguarde seu antecessor; 3. Faça com que cada chamada de thread notifiqueAllAll () quando terminar. Ou você pode usar Java.util.concurrent. *,

jzl106
fonte
na minha experiência, o uso de espera / notificação é frequentemente usado em mecanismos de fila em que um encadeamento ( Runnableimplementação) processa o conteúdo de uma fila. O wait()é então usado sempre que a fila estiver vazia. E o notify()é chamado quando as informações são adicionadas. -> nesse caso, há apenas 1 thread que chama o wait(), então não parece um pouco bobo usar a notifyAll()se você souber que há apenas 1 thread em espera.
bvdb
-2

Acordar tudo não faz muita importância aqui. wait notify e notifyall, todos estes são colocados depois de possuir o monitor do objeto. Se um segmento estiver no estágio de espera e a notificação for chamada, esse segmento ocupará o bloqueio e nenhum outro segmento nesse ponto poderá ocupar esse bloqueio. Portanto, o acesso simultâneo não pode ocorrer. Tanto quanto eu sei, qualquer chamada para aguardar, notificar e notificar só pode ser feita depois de bloquear o objeto. Corrija-me se eu estiver enganado.

Abhay Bansal
fonte