¿Por qué java.util.concurrent.ArrayBlockingQueue usa ‘while’ loops en lugar de ‘if’ alrededor de llamadas a await ()?

He estado jugando con mi propia versión de esto, usando ‘si’, y todo parece estar funcionando bien. Por supuesto, esto se romperá horriblemente si se usa signalAll () en lugar de signal (), pero si solo se notifica un hilo a la vez, ¿cómo puede salir mal?

Su código aquí – echa un vistazo a los métodos put () y take (); una implementación más simple y más al punto se puede ver en la parte superior de JavaDoc for Condition .

Parte relevante de mi implementación a continuación.

public Object get() { lock.lock(); try { if( items.size() = capacity ) hasSpace.await(); items.addFirst(item); hasItems.signal(); return; } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } 

PD. Sé que, en general, particularmente en clases de lib como esta, uno debe dejar que las excepciones se filtren.

Para proteger contra los despertadores espurios. La JVM no le garantiza que la única razón posible por la que el hilo volverá a ejecutarse es porque usted llamó a la señal de la manera que usted quería. A veces simplemente se iniciará accidentalmente y se activará (Despertar espurio). Por lo tanto, debe seguir esperando nuevamente si la condición en la que desea ejecutar no es verdadera.

Esto se explica en javadoc para el método de espera: http://java.sun.com/javase/6/docs/api/java/lang/Object.html#wait%28long%29

Y mencionado en los documentos a la espera: http://java.sun.com/javase/6/docs/api/java/util/concurrent/locks/Condition.html#await%28%29

El locking asociado a esta condición se libera atómicamente y el hilo actual se desactiva para fines de progtwigción de hilos y permanece inactivo hasta que ocurre una de estas cuatro cosas:

  • Algún otro subproceso invoca el método signal () para esta condición y el hilo actual se elige como el subproceso que se debe activar; o

  • Algún otro subproceso invoca el método signalAll () para esta condición; o

  • Algún otro hilo interrumpe el hilo actual, y la interrupción de la suspensión del hilo es compatible; o

* Se produce una “activación espuria”.

Algunas implementaciones de la interfaz Condición pueden suprimir las activaciones espúreas, pero confiar en eso dependerá de un detalle de implementación y hace que su código sea poco práctico.

¿Por qué java.util.concurrent.ArrayBlockingQueue usa ‘while’ loops en lugar de ‘if’ alrededor de llamadas a await ()?

Usan en lugar de if para proteger contra las clásicas condiciones de carrera de subprocesos en modelos de productores / consumidores y para proteger contra el caso mucho más raro de despertar falsos.

Cuando (por ejemplo) varios consumidores esperan una condición en particular (como que la cola está vacía) y se notifica la condición, existe la posibilidad de que otro hilo se bloquee primero y “robe” el artículo agregado a la cola. El ciclo while es necesario para que el subproceso se asegure de que la cola tenga un elemento antes de que intente eliminar la cola.

Código de muestra

He escrito un código de muestra y más documentación que demuestra la condición de carrera.

Descripción de la condición de carrera

En cuanto a su código específico, la carrera es la siguiente:

  1. thread # 1, un consumidor, está en await() en el ciclo while, esperando que haya elementos en la cola
  2. thread # 2, un productor, bloquea en la cola
  3. thread # 3, un consumidor, termina de consumir el último artículo, llama a get() , bloquea en la cola y tiene que esperar a que se desbloquee el # 2 ( NO está esperando en hasItems pero está esperando obtener el lock )
  4. thread # 2, agrega un elemento a la cola y llama a hasItems.signal() para notificar a alguien que hay un elemento allí
  5. el subproceso n. ° 1 se despierta y va a bloquear la cola, tiene que esperar a que se desbloquee el n. ° 2
  6. el hilo # 2 se desbloquea
  7. el subproceso n. ° 3 está delante del subproceso n. ° 1 esperando el candado, por lo que bloquea primero la cola, entra en el ciclo while y quita el objeto por el que se notificó n. ° 1, y luego se desbloquea
  8. el hilo # 1 ahora puede bloquear. Si fuera solo una statement if , avanzaría e intentaría eliminar de una lista vacía lo que arrojaría ArrayIndexOutOfBoundsException o algo así.

La razón por la cual la instrucción while es necesaria es manejar estas condiciones de carrera. En el paso 8 anterior, con un while , el subproceso n. ° 1 volvería a la prueba para ver si hay elementos en la cola, se da cuenta de que no hay ninguno y luego vuelve a esperar.

Este es un problema clásico que hace tropezar a muchos progtwigdores reentrantes. Las versiones iniciales de O’Reilly pthreads biblia, por ejemplo, tenían un código de ejemplo sin el bucle while y tuvieron que volver a publicarse.

Con algunos sistemas de subprocesos, es más fácil para el sistema despertar todas las condiciones en lugar de la condición específica que se ha señalado para que pueda producirse un “despertar espúreo” . Los bucles while protegen contra esto también.

Otro problema con su implementación es una “señal” potencialmente perdida. si observa detenidamente las impls de jdk que manejan InterruptedException, algunas de ellas señalan antes de regresar. esto se debe a que un hilo podría elegirse para ser señalizado y luego terminar siendo interrumpido, en cuyo caso esa señal se perdería. por lo tanto, cuando se interrumpe, jdk impls re-signal antes de achicar. dado que este hilo puede o no haber recibido una señal, esto también puede causar un “despertar espúreo” (como se mencionó anteriormente).

Tal vez te falta tu punto, pero el código original usa un tiempo en lugar de si porque tal vez haya varios hilos escuchando / consumiendo la cola …

La razón principal de esto es que cuando un hilo es notificado que no puede actuar de inmediato, primero tiene que adquirir el locking. Y no hay garantía de que sea el siguiente hilo para obtener el locking.

Cuando un hilo recibe una notificación mientras espera, no tiene el locking. (Soltó el locking cuando comenzó a esperar.) Antes de que pueda abandonar el método de espera, tiene que volver a adquirir el locking. No tiene prioridad sobre ningún otro hilo contendedor para el locking, es muy probable que algún otro hilo llegue primero.

Cualquiera que sean los hilos intermedios, esto podría cambiar el estado del objeto al que se accede, posiblemente haciendo que la notificación sea irrelevante. Entonces, una vez que el hilo notificado ha readquirido con éxito el locking, necesita verificar la condición nuevamente para verificar que la condición sobre la que se notificó sigue siendo verdadera. El ciclo while está ahí para que el hilo pueda verificar la condición una vez que tenga el locking.

Además de eso, hay otras razones para usar un ciclo while para verificar que la condición que estás esperando esté presente:

  • No es válido deducir de que su hilo haya dejado de esperar que necesariamente se debe haber notificado; este es el “despertar espurio”.

  • Además, si hay varias cosas que una notificación podría significar, entonces debería verificar la condición nuevamente para asegurarse de que el evento que le interese sea para lo que lo despertaron.