Java: notify () vs. notifyAll () de nuevo


377

Si uno busca en Google "diferencia entre notify()y notifyAll()", aparecerán muchas explicaciones (dejando aparte los párrafos de javadoc). Todo se reduce a la cantidad de hilos en espera que se están despertando: uno adentro notify()y todo adentro notifyAll().

Sin embargo (si entiendo la diferencia entre estos métodos correctamente), solo se selecciona un subproceso para una mayor adquisición del monitor; en el primer caso, el seleccionado por la VM, en el segundo caso, el seleccionado por el planificador de subprocesos del sistema. El programador desconoce los procedimientos de selección exactos para ambos (en el caso general).

¿Cuál es la utilidad diferencia entre notificar () y notify () , entonces? ¿Me estoy perdiendo de algo?


66
Las bibliotecas útiles para usar para la concurrencia se encuentran en las bibliotecas de concurrencia. Sugiero que estas son una mejor opción en casi todos los casos. La biblioteca de Concurency es anterior a Java 5.0 (donde se agregaron como estándar en 2004)
Peter Lawrey

44
No estoy de acuerdo con Peter. La biblioteca de concurrencia se implementa en Java, y hay un montón de código Java ejecutado cada vez que llama a lock (), unlock (), etc. Puede dispararse en el pie utilizando la biblioteca de concurrencia en lugar de la antigua synchronized, a excepción de ciertos , casos de uso bastante raros.
Alexander Ryzhov

2
El malentendido clave parece ser este: ... solo se selecciona un hilo para una mayor adquisición del monitor; en el primer caso, el seleccionado por la VM, en el segundo caso, el seleccionado por el planificador de subprocesos del sistema. La implicación es que son esencialmente lo mismo. Si bien el comportamiento descrito es correcto, lo que falta es que, en el notifyAll()caso, los otros subprocesos después del primero permanecen despiertos y adquirirán el monitor, uno por uno. En el notifycaso, ninguno de los otros hilos se ha despertado. ¡Funcionalmente son muy diferentes!
BeeOnRope

1) Si hay muchos subprocesos esperando un objeto, y se llama a notify () solo una vez en ese objeto. Excepto uno de los hilos de espera, ¿los hilos restantes esperan para siempre? 2) Si se utiliza notify (), solo uno de los muchos subprocesos en espera comienza a ejecutarse. Si se utiliza notifyall (), todos los subprocesos en espera se notifican, pero solo uno de ellos comienza a ejecutarse, entonces, ¿para qué sirve notifyall () aquí?
Chetan Gowda

@ChetanGowda Notificar a todos los hilos vs Notificar exactamente solo un hilo arbitrario en realidad tiene una diferencia significativa hasta que esa diferencia aparentemente sutil pero importante nos golpea. Cuando notifica () solo 1 hilo, todos los otros hilos estarán en estado de espera hasta que reciba una notificación explícita /señal. Notificando a todos, todos los subprocesos se ejecutarán y completarán en un orden uno tras otro sin ninguna otra notificación; aquí deberíamos decir que los subprocesos son blockedy no waiting. Cuando blockedsu ejecutivo se suspende temporalmente hasta que otro hilo esté dentro del syncbloque.
usuario104309

Respuestas:


248

Sin embargo (si entiendo la diferencia entre estos métodos correctamente), solo se selecciona un subproceso para una mayor adquisición del monitor.

Eso no es correcto. o.notifyAll()despierta todos los hilos que están bloqueados en las o.wait()llamadas. Los hilos solo pueden regresar o.wait()uno por uno, pero cada uno tendrá su turno.


En pocas palabras, depende de por qué sus hilos están esperando ser notificados. ¿Desea contarle a uno de los hilos de espera que sucedió algo o quiere contarles todos al mismo tiempo?

En algunos casos, todos los hilos de espera pueden tomar medidas útiles una vez que finaliza la espera. Un ejemplo sería un conjunto de subprocesos que esperan que finalice una determinada tarea; Una vez que la tarea ha finalizado, todos los hilos en espera pueden continuar con sus negocios. En tal caso, utilizaría notifyAll () para activar todos los hilos en espera al mismo tiempo.

Otro caso, por ejemplo, el bloqueo mutuamente exclusivo, solo uno de los hilos en espera puede hacer algo útil después de ser notificado (en este caso, adquirir el bloqueo). En tal caso, preferiría utilizar notify () . Implementado correctamente, también podría utilizar notifyAll () en esta situación, pero despertaría innecesariamente hilos que de todos modos no pueden hacer nada.


En muchos casos, el código para esperar una condición se escribirá como un bucle:

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

De esa manera, si una o.notifyAll()llamada despierta más de un hilo en espera, y el primero que regresa de las o.wait()marcas deja la condición en el estado falso, entonces los otros hilos que se despertaron volverán a esperar.


29
si notifica solo un subproceso pero varios están esperando un objeto, ¿cómo determina la VM cuál notificar?
anfibio

66
No puedo decir con certeza acerca de las especificaciones de Java, pero en general debe evitar hacer suposiciones sobre tales detalles. Sin embargo, creo que puede suponer que la VM lo hará de una manera sensata y en su mayoría justa.
Liedman

15
Liedman está muy equivocado, la especificación de Java establece explícitamente que no se garantiza que la notificación () sea justa. es decir, cada llamada para notificar podría despertar el mismo hilo nuevamente (la cola de hilos en el monitor NO ES JUSTA o FIFO). Sin embargo, se garantiza que el planificador sea justo. Es por eso que en la mayoría de los casos en los que tiene más de 2 hilos debería preferir notificar a Todos.
Yann TM

45
@ YannTM Estoy a favor del critisismo constructivo, pero creo que tu tono es un poco injusto. Dije explícitamente "no puedo decir con certeza" y "creo". Tranquilízate, ¿alguna vez escribiste algo hace siete años que no era 100% correcto?
Liedman

10
El problema es que esta es la respuesta aceptada, no es una cuestión de orgullo personal. Si sabe que estaba equivocado ahora, edite su respuesta para decirlo y señale, por ejemplo, la respuesta correcta y pedagógica de xagyg a continuación.
Yann TM

330

Claramente, notifydespierta (cualquier) un hilo en el conjunto de espera, notifyAlldespierta todos los hilos en el conjunto de espera. La siguiente discusión debería aclarar cualquier duda. notifyAlldebe usarse la mayor parte del tiempo. Si no está seguro de cuál usar, use notifyAll. Consulte la explicación que sigue.

Lea con mucho cuidado y comprenda. Por favor envíeme un correo electrónico si tiene alguna pregunta.

Mire al productor / consumidor (la suposición es una clase ProducerConsumer con dos métodos). ESTÁ ROTO (porque usa notify), sí, PUEDE funcionar, incluso la mayoría de las veces, pero también puede causar un punto muerto; veremos por qué:

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;
}

PRIMERAMENTE,

¿Por qué necesitamos un bucle while que rodea la espera?

Necesitamos un whilebucle en caso de que tengamos esta situación:

El consumidor 1 (C1) ingresa el bloque sincronizado y el búfer está vacío, por lo que C1 se coloca en el conjunto de espera (a través de la waitllamada). El consumidor 2 (C2) está a punto de ingresar el método sincronizado (en el punto Y anterior), pero el productor P1 coloca un objeto en el búfer y luego llama notify. El único subproceso en espera es C1, por lo que se despierta y ahora intenta volver a adquirir el bloqueo del objeto en el punto X (arriba).

Ahora C1 y C2 están intentando adquirir el bloqueo de sincronización. Se elige uno de ellos (de manera no determinista) y se ingresa al método, el otro se bloquea (no espera, pero se bloquea, tratando de obtener el bloqueo del método). Digamos que C2 obtiene el bloqueo primero. C1 sigue bloqueando (tratando de adquirir el bloqueo en X). C2 completa el método y libera el bloqueo. Ahora, C1 adquiere la cerradura. Adivina qué, por suerte tenemos un whilebucle, porque, C1 realiza la verificación del bucle (guardia) y se le impide eliminar un elemento inexistente del búfer (¡C2 ya lo tiene!). Si no tuviéramos un while, obtendríamos un IndexArrayOutOfBoundsExceptionmensaje cuando C1 intente eliminar el primer elemento del búfer.

AHORA,

Ok, ahora ¿por qué necesitamos notificar a todos?

En el ejemplo anterior de productor / consumidor, parece que podemos salirse con la suya notify. Parece de esta manera, porque podemos demostrar que los guardias en los bucles de espera para el productor y el consumidor son mutuamente excluyentes. Es decir, parece que no podemos tener un hilo esperando tanto en el putmétodo como en el getmétodo, porque, para que eso sea cierto, lo siguiente debería ser cierto:

buf.size() == 0 AND buf.size() == MAX_SIZE (suponga que MAX_SIZE no es 0)

SIN EMBARGO, esto no es lo suficientemente bueno, NECESITAMOS usarlo notifyAll. Veamos por qué ...

Supongamos que tenemos un búfer de tamaño 1 (para que el ejemplo sea fácil de seguir). Los siguientes pasos nos llevan al punto muerto. Tenga en cuenta que en CUALQUIER MOMENTO un hilo se despierta con notificación, puede ser seleccionado de forma no determinista por la JVM, es decir, cualquier hilo en espera se puede despertar. También tenga en cuenta que cuando varios subprocesos están bloqueando la entrada a un método (es decir, tratando de adquirir un bloqueo), el orden de adquisición puede ser no determinista. Recuerde también que un hilo solo puede estar en uno de los métodos a la vez: los métodos sincronizados permiten que solo un hilo se ejecute (es decir, mantener el bloqueo de) cualquier método (sincronizado) en la clase. Si se produce la siguiente secuencia de eventos, se produce un punto muerto:

PASO 1:
- P1 pone 1 char en el búfer

PASO 2:
- Intentos de P2 put- comprueba el ciclo de espera - ya es un char - espera

PASO 3:
- Intentos de P3 put- comprueba el ciclo de espera - ya es un char - espera

PASO 4:
- C1 intenta obtener 1 char
- C2 intenta obtener 1 char - bloques al ingresar al getmétodo
- C3 intenta obtener 1 char - bloques al ingresar al getmétodo

PASO 5:
- C1 está ejecutando el getmétodo - obtiene el método char, llama notify, sale
- notifyP2 se despierta
- PERO, C2 ingresa al método antes de que P2 pueda (P2 debe recuperar el bloqueo), por lo que P2 se bloquea al ingresar al putmétodo
- C2 comprueba el bucle de espera, no hay más caracteres en el búfer, por lo que espera
: C3 ingresa al método después de C2, pero antes de P2, comprueba el bucle de espera, no hay más caracteres en el búfer, por lo que espera

PASO 6:
- AHORA: ¡hay P3, C2 y C3 esperando!
- Finalmente P2 adquiere el bloqueo, pone un char en el búfer, llama a notificar, sale del método

PASO 7:
- La notificación de P2 despierta a P3 (recuerde que cualquier subproceso se puede despertar)
- P3 verifica la condición del bucle de espera, ya hay un carácter en el búfer, por lo que espera.
- ¡NO HAY MÁS HILOS PARA LLAMAR NOTIFICACIÓN y TRES HILOS SUSPENDIDOS PERMANENTEMENTE!

SOLUCIÓN: Reemplace notifycon notifyAllen el código de productor / consumidor (arriba).


1
finnw: P3 debe volver a comprobar la condición porque las notifycausas P3 (el subproceso seleccionado en este ejemplo) proceden del punto que estaba esperando (es decir, dentro del whilebucle). Hay otros ejemplos que no causan un punto muerto, sin embargo, en este caso, el uso de notifyno garantiza un código libre de punto muerto. El uso de notifyAllhace.
xagyg

44
@marcus Muy cerca. Con notifyAll, cada subproceso volverá a adquirir el bloqueo (uno a la vez), pero tenga en cuenta que después de que un subproceso haya readquirido el bloqueo y ejecutado el método (y luego salido) ... el siguiente subproceso vuelve a adquirir el bloqueo, comprueba el "mientras" y volverá a "esperar" (dependiendo de cuál sea la condición, por supuesto). Por lo tanto, notifique despierta un hilo, como usted dice correctamente. notifyAll activa todos los subprocesos y cada subproceso vuelve a adquirir el bloqueo de uno en uno: comprueba la condición del "while" y ejecuta el método o "espera" nuevamente.
xagyg

1
@xagyg, ¿estás hablando de un escenario en el que cada productor tiene un solo personaje para almacenar? Si es así, su ejemplo es correcto, pero no muy interesante. Con los pasos adicionales que sugiero, puede bloquear el mismo sistema, pero con una cantidad ilimitada de entrada, que es la forma en que estos patrones generalmente se utilizan en la realidad.
eran

3
@codeObserver Usted preguntó: "Llamar a notifyAll () conduciría a múltiples subprocesos en espera que verifican la condición while () al mismo tiempo ... y, por lo tanto, existe la posibilidad de que antes de que sane el tiempo, 2 subprocesos ya estén fuera de él causando outOfBound excepción? " No, esto no es posible, ya que a pesar de que se activarían varios subprocesos, no pueden verificar la condición while al mismo tiempo. Cada uno de ellos debe volver a alcanzar el bloqueo (inmediatamente después de la espera) antes de poder volver a ingresar a la sección del código y volver a verificar el tiempo. Por lo tanto, uno a la vez.
xagyg

44
@xagyg buen ejemplo. Esto está fuera del tema de la pregunta original; solo por el bien de la discusión. El punto muerto es un problema de diseño de la OMI (corrígeme si me equivoco). Porque tienes un candado compartido por put y get. Y JVM no es lo suficientemente inteligente como para llamar a poner después de que se libere el bloqueo y viceversa. El bloqueo muerto ocurre porque put despierta a otro put, que vuelve a esperar () debido a while (). ¿Funcionaría hacer dos clases (y dos cerraduras)? Así que pon {synchonized (get)}, get {(synchonized (put)}. En otras palabras, get se despertará solo put y put se despertará get only.
Jay

43

Diferencias útiles:

  • Use notify () si todos sus hilos de espera son intercambiables (el orden en que se despiertan no importa), o si solo tiene un hilo de espera. Un ejemplo común es un grupo de subprocesos utilizado para ejecutar trabajos desde una cola: cuando se agrega un trabajo, se notifica a uno de los subprocesos para que se active, ejecute el siguiente trabajo y vuelva a dormir.

  • Utilice notifyAll () para otros casos en los que los subprocesos en espera pueden tener diferentes propósitos y deberían poder ejecutarse simultáneamente. Un ejemplo es una operación de mantenimiento en un recurso compartido, donde varios subprocesos esperan que se complete la operación antes de acceder al recurso.


19

Creo que depende de cómo se producen y consumen los recursos. Si hay 5 objetos de trabajo disponibles a la vez y tiene 5 objetos de consumo, tendría sentido reactivar todos los subprocesos utilizando notifyAll () para que cada uno pueda procesar 1 objeto de trabajo.

Si solo tiene un objeto de trabajo disponible, ¿cuál es el punto de despertar todos los objetos de consumo para competir por ese único objeto? El primero que verifique el trabajo disponible lo obtendrá y todos los otros subprocesos lo comprobarán y descubrirán que no tienen nada que hacer.

Encontré una gran explicación aquí . En breve:

El método de notificación () se usa generalmente para grupos de recursos , donde hay un número arbitrario de "consumidores" o "trabajadores" que toman recursos, pero cuando se agrega un recurso al grupo, solo uno de los consumidores o trabajadores en espera puede tratar con eso. El método notifyAll () se usa realmente en la mayoría de los otros casos. Estrictamente, se requiere notificar a los camareros de una condición que podría permitir que procedan varios camareros. Pero esto es a menudo difícil de saber. Por lo tanto, como regla general, si no tiene una lógica particular para usar notify (), entonces probablemente debería usar notifyAll () , porque a menudo es difícil saber exactamente qué hilos esperarán en un objeto en particular y por qué.


11

Tenga en cuenta que con las utilidades de concurrencia también tiene la opción de elegir entre estos métodos signal()y signalAll()cómo se llaman allí. Entonces la pregunta sigue siendo válida incluso con java.util.concurrent.

Doug Lea plantea un punto interesante en su famoso libro : si a notify()y Thread.interrupt()sucede al mismo tiempo, la notificación podría perderse. Si esto puede suceder y tiene implicaciones dramáticas, notifyAll()es una opción más segura a pesar de que paga el precio de los gastos generales (la mayoría de las veces se despierta demasiados hilos).


10

Breve resumen:

Siempre prefiera notifyAll () sobre notify () a menos que tenga una aplicación paralela masiva en la que un gran número de subprocesos hagan lo mismo.

Explicación:

notify () [...] despierta un solo hilo. Debido a que notify () no le permite especificar el hilo que se ha despertado, es útil solo en aplicaciones masivamente paralelas, es decir, programas con una gran cantidad de hilos, todos haciendo tareas similares. En tal aplicación, no te importa qué hilo se despierte.

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

Compare notify () con notifyAll () en la situación descrita anteriormente: una aplicación masivamente paralela donde los hilos están haciendo lo mismo. Si llama a notifyAll () en ese caso, notifyAll () inducirá el despertar (es decir, la programación) de una gran cantidad de subprocesos, muchos de ellos innecesariamente (ya que solo un subproceso puede continuar, es decir, el subproceso al que se le otorgará el monitorear para el objeto wait () , notify () , o notifyAll () fue llamado sucesivamente), por lo tanto, el desperdicio de recursos de computación.

Por lo tanto, si no tiene una aplicación en la que una gran cantidad de subprocesos hacen lo mismo al mismo tiempo, prefiera notifyAll () sobre notify () . ¿Por qué? Porque, como otros usuarios ya han respondido en este foro, notifique ()

despierta un solo hilo que está esperando en el monitor de este objeto. [...] La elección es arbitraria y ocurre a discreción de la implementación.

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

Imagine que tiene una aplicación de consumidor productor donde los consumidores están listos (es decir, esperando ) para consumir, los productores están listos (es decir, esperando ) para producir y la cola de artículos (para ser producida / consumida) está vacía. En ese caso, notify () podría despertar solo a los consumidores y nunca a los productores porque la elección de quién se despierta es arbitraria . El ciclo del consumidor productor no avanzaría, aunque los productores y los consumidores están listos para producir y consumir, respectivamente. En cambio, un consumidor se despierta (es decir, deja el estado de espera () ), no saca un artículo de la cola porque está vacío y notifica () a otro consumidor que continúe.

Por el contrario, notifyAll () despierta tanto a productores como a consumidores. La elección de quién está programado depende del planificador. Por supuesto, dependiendo de la implementación del planificador, el planificador también puede programar únicamente a los consumidores (por ejemplo, si asigna hilos de consumo con una prioridad muy alta). Sin embargo, la suposición aquí es que el peligro de que el planificador programe solo consumidores es menor que el peligro de que la JVM solo despierte a los consumidores porque cualquier planificador implementado razonablemente no toma decisiones arbitrarias . Por el contrario, la mayoría de las implementaciones de planificador hacen al menos algún esfuerzo para evitar la inanición.


9

Aquí hay un ejemplo. Ejecutarlo. Luego, cambie uno de notifyAll () para notificar () y ver qué sucede.

Clase 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();
    }
}

Clase 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();
    }
}

Clase de consumo

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) { }
        }
    }
}

Clase de productor

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) { }
        }
    }
}

8

De Joshua Bloch, el propio Java Guru en Effective Java 2nd edition:

"Artículo 69: Prefiere utilidades de concurrencia para esperar y notificar".


16
El por qué es más importante que la fuente.
Pacerier

2
@Pacerier Bien dicho. También me interesaría más descubrir las razones. Una posible razón podría ser que esperar y notificar en la clase de objeto se basan en una variable de condición implícita. Entonces, en el ejemplo estándar de productor y consumidor ... tanto el productor como el consumidor esperarán en la misma condición que podría llevar a un punto muerto como explica xagyg en su respuesta. Entonces, un mejor enfoque es usar 2 variables de condición como se explica en docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/…
rahul

6

Espero que esto aclare algunas dudas.

notify () : el método notify () activa un subproceso que espera el bloqueo (el primer subproceso que llamó a wait () en ese bloqueo).

notifyAll () : el método notifyAll () activa todos los subprocesos que esperan el bloqueo; JVM selecciona uno de los subprocesos de la lista de subprocesos que esperan el bloqueo y activa ese subproceso.

En el caso de un solo subproceso en espera de un bloqueo, no hay una diferencia significativa entre notify () y notifyAll (). Sin embargo, cuando hay más de un subproceso esperando el bloqueo, tanto en notify () como notifyAll (), el subproceso exacto despertado está bajo el control de la JVM y no puede controlar mediante programación el despertar un subproceso específico.

A primera vista, parece que es una buena idea simplemente llamar a notify () para activar un hilo; Puede parecer innecesario despertar todos los hilos. Sin embargo, el problema con notify () es que el hilo despertado podría no ser el adecuado para despertarse (el hilo podría estar esperando alguna otra condición, o la condición aún no se cumple para ese hilo, etc.). En ese caso , la notificación () podría perderse y ningún otro subproceso se despertará potencialmente dando lugar a un tipo de punto muerto (la notificación se pierde y todos los demás subprocesos esperan la notificación, para siempre).

Para evitar este problema , siempre es mejor llamar a notifyAll () cuando hay más de un subproceso esperando un bloqueo (o más de una condición en la que se realiza la espera). El método notifyAll () activa todos los hilos, por lo que no es muy eficiente. Sin embargo, esta pérdida de rendimiento es insignificante en las aplicaciones del mundo real.


6

Hay tres estados para un hilo.

  1. WAIT: el subproceso no usa ningún ciclo de CPU
  2. BLOQUEADO: el subproceso está bloqueado al intentar adquirir un monitor. Todavía podría estar utilizando los ciclos de la CPU
  3. EN EJECUCIÓN: el hilo se está ejecutando.

Ahora, cuando se llama a notify (), JVM elige un subproceso y los mueve al estado BLOCKED y, por lo tanto, al estado RUNNING, ya que no hay competencia para el objeto monitor.

Cuando se llama a notifyAll (), JVM selecciona todos los subprocesos y los mueve al estado BLOCKED. Todos estos subprocesos obtendrán el bloqueo del objeto de forma prioritaria. El subproceso que pueda adquirir el monitor primero podrá ir al estado EN EJECUCIÓN primero y así sucesivamente.


Simplemente una explicación asombrosa.
royatirek

5

Estoy muy sorprendido de que nadie haya mencionado el infame problema del "despertar perdido" (google it).

Básicamente:

  1. si tiene varios subprocesos esperando una misma condición y,
  2. múltiples hilos que pueden hacer la transición del estado A al estado B y,
  3. múltiples subprocesos que pueden hacer la transición del estado B al estado A (generalmente los mismos subprocesos que en 1.) y,
  4. La transición del estado A al B debe notificar los hilos en 1.

ENTONCES, debe usar notifyAll a menos que tenga garantías comprobables de que la activación de la pérdida es imposible.

Un ejemplo común es una cola FIFO concurrente donde: múltiples colas (1. y 3. arriba) pueden hacer la transición de su cola de múltiples colas vacías a no vacías (2. arriba) pueden esperar la condición "la cola no está vacía" vacía -> no vacío debe notificar a los solicitantes

Puede escribir fácilmente un entrelazado de operaciones en el que, a partir de una cola vacía, interactúan 2 y 2 dequeuers, y 1 de ellos permanecerá dormido.

Este es un problema posiblemente comparable con el problema del punto muerto.


Mis disculpas, xagyg lo explica en detalle. El nombre del problema es "despertar perdido"
NickV

@Abhay Bansal: Creo que te estás perdiendo el hecho de que condition.wait () libera el bloqueo y el hilo que se activa vuelve a adquirirlo.
NickV

4

notify()despertará un hilo mientras notifyAll()despertará a todos. Que yo sepa, no hay término medio. Pero si no está seguro de qué notify()hará con sus hilos, úselo notifyAll(). Funciona como un encanto cada vez.


4

Todas las respuestas anteriores son correctas, por lo que puedo decir, así que voy a decirte algo más. Para el código de producción, realmente debería usar las clases en java.util.concurrent. Hay muy poco que no puedan hacer por usted, en el área de concurrencia en Java.


4

notify()te permite escribir código más eficiente que notifyAll().

Considere la siguiente pieza de código que se ejecuta desde múltiples hilos paralelos:

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

Se puede hacer más 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();
}

En el caso de que tenga una gran cantidad de subprocesos, o si la condición del bucle de espera es costosa de evaluar, notify()será significativamente más rápida que notifyAll(). Por ejemplo, si tiene 1000 subprocesos, se despertarán y evaluarán 999 subprocesos después del primero notifyAll(), luego 998, luego 997, y así sucesivamente. Por el contrario, con la notify()solución, solo se despertará un hilo.

Úselo notifyAll()cuando necesite elegir qué hilo hará el trabajo a continuación:

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

Finalmente, es importante comprender que, en caso de que notifyAll(), el código dentro de los synchronizedbloques que se han despertado se ejecute secuencialmente, no todos a la vez. Digamos que hay tres hilos esperando en el ejemplo anterior, y el cuarto hilo llama notifyAll(). Los tres subprocesos se despertarán pero solo uno comenzará la ejecución y comprobará la condición del whilebucle. Si la condición es true, volverá a llamar wait(), y solo entonces el segundo subproceso comenzará a ejecutarse y comprobará su whilecondición de bucle, y así sucesivamente.


4

Aquí hay una explicación más simple:

Tiene razón en que, ya sea que use notify () o notifyAll (), el resultado inmediato es que exactamente otro subproceso adquirirá el monitor y comenzará a ejecutarse. (Suponiendo que algunos hilos estén bloqueados en wait () para este objeto, otros hilos no relacionados no absorben todos los núcleos disponibles, etc.) El impacto llega más tarde.

Suponga que los hilos A, B y C estaban esperando este objeto, y el hilo A obtiene el monitor. La diferencia radica en lo que sucede una vez que A libera el monitor. Si usó notify (), entonces B y C todavía están bloqueados en wait (): no están esperando en el monitor, están esperando ser notificados. Cuando A suelta el monitor, B y C seguirán sentados allí, esperando una notificación ().

Si usó notifyAll (), entonces B y C han avanzado más allá del estado "esperar notificación" y ambos están esperando adquirir el monitor. Cuando A libera el monitor, B o C lo adquirirán (suponiendo que ningún otro subproceso esté compitiendo por ese monitor) y comenzarán a ejecutarse.


Muy clara explicación. El resultado de este comportamiento de notify () puede conducir a "Señal perdida" / "Notificación perdida", lo que lleva a una situación de punto muerto / no progreso del estado de la aplicación. P-Productor, C-Consumidor P1, P2 y C2 están esperando a C1. C1 llama a notify () y está destinado a un productor, pero C2 se puede despertar, por lo que tanto P1 como P2 perdieron la notificación y esperarán más "notificación" explícita (una llamada de notificación ()).
user104309

4

Esta respuesta es una reescritura gráfica y una simplificación de la excelente respuesta de xagyg , incluidos los comentarios de eran .

¿Por qué usar notifyAll, incluso cuando cada producto está destinado a un solo consumidor?

Considere productores y consumidores, simplificados de la siguiente manera.

Productor:

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

Consumidor:

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

Suponga que 2 productores y 2 consumidores comparten un búfer de tamaño 1. La siguiente imagen muestra un escenario que conduce a un punto muerto , que se evitaría si todos los hilos utilizados se notificaran a AllAll .

Cada notificación está etiquetada con el hilo que se está despertando.

punto muerto debido a notificar


3

Me gustaría mencionar lo que se explica en la concurrencia de Java en la práctica:

Primer punto, ¿Notify o NotifyAll?

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

Si dos hilos A y B están esperando diferentes predicados de condición de la misma cola de condición y se llama a notificar, entonces depende de JVM a qué hilo notificará JVM.

Ahora, si la notificación era para el hilo A y el hilo B notificado por JVM, el hilo B se activará y verá que esta notificación no es útil, por lo que esperará nuevamente. Y el hilo A nunca se enterará de esta señal perdida y alguien secuestró su notificación.

Por lo tanto, llamar a notifyAll resolverá este problema, pero nuevamente tendrá un impacto en el rendimiento, ya que notificará a todos los hilos y todos los hilos competirán por el mismo bloqueo e involucrará un cambio de contexto y, por lo tanto, se cargará en la CPU. Pero deberíamos preocuparnos por el rendimiento solo si se comporta correctamente, si su comportamiento en sí mismo no es correcto, entonces el rendimiento no sirve.

Este problema se puede resolver con el uso del objeto Condición de bloqueo explícito de bloqueo, proporcionado en jdk 5, ya que proporciona una espera diferente para cada predicado de condición. Aquí se comportará correctamente y no habrá problemas de rendimiento, ya que llamará a la señal y se asegurará de que solo un subproceso esté esperando esa condición


3

notify()- Selecciona un hilo aleatorio del conjunto de espera del objeto y lo coloca en el BLOCKEDestado. El resto de los subprocesos en el conjunto de espera del objeto todavía están en el WAITINGestado.

notifyAll()- Mueve todos los hilos del conjunto de espera del objeto al BLOCKEDestado. Después de usar notifyAll(), no quedan hilos en el conjunto de espera del objeto compartido porque todos ellos están ahora en BLOCKEDestado y no en WAITINGestado.

BLOCKED- bloqueado para la adquisición de la cerradura. WAITING- esperando notificación (o bloqueado para completar la unión).


3

Tomado del blog sobre Java efectivo:

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

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

Entonces, lo que entiendo es (del blog mencionado anteriormente, comentario de "Yann TM" sobre la respuesta aceptada y los documentos de Java ):

  • notify (): JVM despierta uno de los hilos de espera en este objeto. La selección de hilos se realiza arbitrariamente sin equidad. Así mismo hilo puede ser despertado una y otra vez. Por lo tanto, el estado del sistema cambia pero no se realiza ningún progreso real. Creando así un livelock .
  • notifyAll (): JVM despierta todos los hilos y luego todos los hilos compiten por el bloqueo de este objeto. Ahora, el planificador de la CPU selecciona un subproceso que adquiere bloqueo en este objeto. Este proceso de selección sería mucho mejor que la selección de JVM. Por lo tanto, asegurando la vida.

2

Echa un vistazo al código publicado por @xagyg.

Suponga que dos subprocesos diferentes esperan dos condiciones diferentes:
el primer subproceso está esperando buf.size() != MAX_SIZEy el segundo subproceso está esperando buf.size() != 0.

Supongamos que en algún momento buf.size() no es igual a 0 . JVM llama en notify()lugar de notifyAll(), y se notifica el primer subproceso (no el segundo).

El primer subproceso se despierta, comprueba buf.size()cuáles podrían regresar MAX_SIZEy vuelve a esperar. El segundo hilo no se despierta, continúa esperando y no llama get().


1

notify()despierta el primer hilo que invocó wait()el mismo objeto.

notifyAll()despierta todos los hilos que llamaron wait()al mismo objeto.

El subproceso de mayor prioridad se ejecutará primero.


13
En caso de notify()que no sea exactamente " el primer hilo ".
Bhesh Gurung

66
no puede predecir cuál será elegido por VM. Sólo Dios sabe.
Sergii Shevchyk

No hay garantía de quién será el primero (no es justo)
Ivan Voroshilin

Solo activará el primer hilo si el sistema operativo lo garantiza, y es probable que no lo haga. Realmente difiere al sistema operativo (y su programador) para determinar qué hilo activar.
Paul Stelian

1

Notificar notificará solo un subproceso que está en estado de espera, mientras que notificar a todos notificará a todos los subprocesos en estado de espera ahora todos los subprocesos notificados y todos los subprocesos bloqueados son elegibles para el bloqueo, de los cuales solo uno obtendrá el bloqueo y todos los demás (incluidos los que están en estado de espera antes) estarán en estado bloqueado.


1

Para resumir las excelentes explicaciones detalladas anteriores, y de la manera más simple que se me ocurre, esto se debe a las limitaciones del monitor incorporado JVM, que 1) se adquiere en toda la unidad de sincronización (bloque u objeto) y 2) no discrimina sobre la condición específica que se espera / notifica en / sobre.

Esto significa que si varios subprocesos están esperando en diferentes condiciones y se utiliza notify (), el subproceso seleccionado puede no ser el que avanzaría en la nueva condición cumplida, lo que provocaría que ese subproceso (y otros subprocesos actualmente en espera que pudieran para cumplir con la condición, etc.) para no poder avanzar y, finalmente, pasar hambre o colgar el programa.

Por el contrario, notifyAll () permite que todos los subprocesos en espera vuelvan a adquirir el bloqueo y verifiquen su estado respectivo, lo que finalmente permitirá el progreso.

Por lo tanto, notify () se puede usar de forma segura solo si se garantiza que cualquier subproceso en espera permita avanzar si se selecciona, lo que en general se satisface cuando todos los subprocesos dentro del mismo monitor verifican solo una y la misma condición, algo bastante raro caso en aplicaciones del mundo real.


0

Cuando llama al wait () del "objeto" (esperando que se obtenga el bloqueo del objeto), internamente esto liberará el bloqueo de ese objeto y ayudará a los otros hilos a bloquear este "objeto", en este escenario habrá más de 1 subproceso en espera del "recurso / objeto" (teniendo en cuenta que los otros subprocesos también emitieron la espera en el mismo objeto anterior y en el camino habrá un subproceso que llenará el recurso / objeto e invoca notify / notifyAll).

Aquí, cuando emite la notificación del mismo objeto (desde el mismo / otro lado del proceso / código), esto liberará un subproceso único bloqueado y en espera (no todos los subprocesos en espera: este subproceso liberado será elegido por JVM Thread El programador y todo el proceso de obtención de bloqueo en el objeto es el mismo que el habitual).

Si solo tiene un hilo que compartirá / trabajará en este objeto, está bien usar solo el método notify () en su implementación de esperar-notificar.

Si se encuentra en una situación en la que más de un hilo lee y escribe sobre recursos / objetos en función de su lógica empresarial, debe ir a notifyAll ()

ahora estoy mirando cómo exactamente la jvm está identificando y rompiendo el hilo de espera cuando emitimos notify () en un objeto ...


0

Si bien hay algunas respuestas sólidas arriba, estoy sorprendido por la cantidad de confusiones y malentendidos que he leído. Esto probablemente prueba la idea de que uno debería usar java.util.concurrent tanto como sea posible en lugar de intentar escribir su propio código concurrente roto. Volviendo a la pregunta: para resumir, la mejor práctica hoy es EVITAR notificar () en TODAS las situaciones debido al problema de activación perdido. A cualquiera que no entienda esto no se le debe permitir escribir código de concurrencia de misión crítica. Si le preocupa el problema del pastoreo, una forma segura de lograr despertar un hilo a la vez es: 1. Crear una cola de espera explícita para los hilos en espera; 2. Haga que cada hilo de la cola espere a su predecesor; 3. Haga que cada subproceso llame a notifyAll () cuando haya terminado. O puede usar Java.util.concurrent. *,


en mi experiencia, el uso de esperar / notificar a menudo se usa en mecanismos de cola donde un hilo ( Runnableimplementación) procesa el contenido de una cola. La wait()continuación, se utiliza cada vez que la cola está vacía. Y notify()se llama cuando se agrega información. -> en tal caso, solo hay 1 hilo que alguna vez llama al wait(), entonces no parece un poco tonto usar un notifyAll()si sabes que solo hay 1 hilo en espera.
bvdb

-2

Despertar a todos no tiene mucha importancia aquí. espere notifique y notifique a todos, todos estos se colocan después de poseer el monitor del objeto. Si un subproceso está en la etapa de espera y se llama a notificar, este subproceso ocupará el bloqueo y ningún otro subproceso en ese punto puede ocupar ese bloqueo. Por lo tanto, el acceso concurrente no puede tener lugar en absoluto. Hasta donde sé, cualquier llamada para esperar notificar y notificar a todos solo se puede hacer después de bloquear el objeto. Corrígeme si estoy equivocado.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.