Análisis de condición en JUC

I. Panorama general

La condición descompone los métodos del monitor de objetos (esperar, notificar y notificar a todos) en diferentes objetos. Combinándolos con cualquier implementación de bloqueo, cada objeto puede tener múltiples conjuntos de espera. Cuando "Lock reemplaza el uso de métodos y declaraciones sincronizados", Condition reemplaza el uso de métodos de monitor de "objeto".

Condiciones Las condiciones (también llamadas colas de condición o variables de condición) proporcionan un medio para que un subproceso suspenda la ejecución ("esperar") hasta que otro subproceso informe que ciertas condiciones de estado ahora son verdaderas. Dado que el acceso a esta información de estado compartida se produce en diferentes subprocesos, debe estar protegido, por lo que algún tipo de bloqueo se asocia con esta condición. El atributo clave proporcionado por la condición de espera es que libera automáticamente el bloqueo asociado y suspende el hilo actual, al igual que Object.wait.

La instancia de Condition está esencialmente vinculada a un bloqueo. Para obtener la instancia de Condition de una instancia de Lock específica, use su método newCondition ().

Por ejemplo, supongamos que tenemos un búfer acotado, que admite métodos de colocación y recepción. Si intenta tomar un búfer vacío, el hilo se bloqueará hasta que haya un elemento disponible. Si intenta poner el búfer completo, el hilo se bloqueará hasta que haya espacio libre. Queremos mantener el subproceso de espera y tomar el subproceso en conjuntos de espera separados (diferentes), de modo que podamos usar la optimización de notificar un solo subproceso solo cuando los elementos o el espacio en el búfer están disponibles. Esto se puede lograr utilizando dos instancias de condición. (La clase java.util.concurrent.ArrayBlockingQueue proporciona esta funcionalidad, por lo que no hay razón para implementar esta clase de uso de ejemplo).


   class BoundedBuffer {
     final Lock lock = new ReentrantLock();
     final Condition notFull  = lock.newCondition(); 
     final Condition notEmpty = lock.newCondition(); 
  
     final Object[] items = new Object[100];
     int putptr, takeptr, count;
  
     public void put(Object x) throws InterruptedException {
       lock.lock();
       try {
         while (count == items.length)
           notFull.await();
         items[putptr] = x;
         if (++putptr == items.length) putptr = 0;
         ++count;
         notEmpty.signal();
       } finally {
         lock.unlock();
       }
     }
  
     public Object take() throws InterruptedException {
       lock.lock();
       try {
         while (count == 0)
           notEmpty.await();
         Object x = items[takeptr];
         if (++takeptr == items.length) takeptr = 0;
         --count;
         notFull.signal();
         return x;
       } finally {
         lock.unlock();
       }
     }
   }

La implementación de Condición puede proporcionar diferentes comportamientos y semánticas del método de monitoreo de Objetos, como garantizar el orden de las notificaciones o mantener bloqueos al ejecutar notificaciones. Si la implementación proporciona esta semántica especial, la implementación debe documentar esta semántica.

Tenga en cuenta que las instancias de condición son solo objetos ordinarios. Ellos mismos pueden usarse como objetivos en declaraciones sincronizadas (sincronizadas (condición)) y pueden llamar a sus propios métodos de notificación y espera de monitorización. Obtener el bloqueo del monitor de una instancia de Condición o usar su método de monitoreo no tiene una relación específica con la adquisición del Bloqueo asociado con la Condición o el uso de sus métodos de espera y señal. Se recomienda evitar confusiones y no utilizar las instancias de condición de esta forma a menos que sea posible en su propia implementación. A menos que se indique lo contrario, pasar un valor nulo para cualquier parámetro provocará que se lance una NullPointerException.

Consideraciones de implementación

Cuando se espera la condición, normalmente se permite una "activación falsa" como concesión a la semántica de la plataforma subyacente. Esto tiene poco impacto práctico en la mayoría de las aplicaciones, porque siempre debe esperar la condición en el ciclo para probar el estado de espera para determinar la situación. Una implementación es gratuita para eliminar la posibilidad de reactivaciones falsas, pero se recomienda que los programadores de aplicaciones siempre asuman que sucederán y, por lo tanto, siempre esperen en un bucle.

Las tres formas de espera condicional (interrumpible, no interrumpible y temporizada) pueden ser diferentes en cuanto a facilidad de implementación y características de rendimiento en algunas plataformas. En particular, puede resultar difícil proporcionar estas funciones y mantener una semántica específica, como garantías de pedidos. Además, la capacidad de interrumpir un hilo que en realidad está suspendido no siempre es factible en todas las plataformas. Por lo tanto, no es necesario implementar exactamente las mismas garantías o semánticas definidas para las tres formas de espera, y no es necesario admitir la interrupción de subprocesos realmente suspendidos.

Una implementación necesita registrar claramente la semántica y las garantías proporcionadas para cada método de espera, y cuando la implementación no admite la interrupción del hilo suspendido, debe cumplir con la semántica de interrupción definida en esta interfaz (interfaz de condición).

Dado que la interrupción generalmente significa cancelación, y las verificaciones de interrupciones generalmente no se realizan con frecuencia, las implementaciones pueden preferir responder a las interrupciones que las devoluciones de métodos normales. Esto es cierto incluso si se puede probar que la interrupción ocurrió después de otra operación que podría desbloquear el hilo. Las implementaciones deben documentar este comportamiento.

2. Análisis del código fuente de ConditionObject en AbstractQueuedSynchronizer

Node firstWaite: el primer nodo de la lista enlazada individualmente

Node lastWaiter: el último nodo de la lista enlazada individualmente

método await (): primero analiza el bloqueo

Breve descripción del proceso del método:

       1 Si se interrumpe el hilo actual, se lanza una InterruptedException.
       2 Guarde el estado de bloqueo devuelto por getState.
       3 Llame a release con el estado guardado como parámetro y arroje IllegalMonitorStateException si falla.
       4 Bloquear hasta que se le indique o se interrumpa.
       5 Vuelva a adquirir llamando a la versión especial de adquirir que toma el estado guardado como parámetro.
       6 Si se bloquea e interrumpe en el paso 4, se lanza una InterruptedException

    public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            Node node = addConditionWaiter();
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

1.addConditionWaiter: agregue el nodo al final de la cola de condiciones. Nota: La tabla de collar único aquí usa nextWaiter de Node en lugar de next. Estos dos se usan para distinguir entre colas de bloqueo y colas condicionales.

   private Node addConditionWaiter() {
            Node t = lastWaiter;
            // If lastWaiter is cancelled, clean out.
            //为节点不为空且waitStatus值不是等待(保证新节点前一个节点必须是等待状态),
            //则移除队列中已经取消等待的节点
            //(取消过程中尾节点可能已经重新指向其它堆内存,所以t需要再次指向尾节点)
            if (t != null && t.waitStatus != Node.CONDITION) {
                unlinkCancelledWaiters();
                t = lastWaiter;
            }
            //尾节点为空,头节点和尾节点都指向该节点
            //不为空,则尾节点指向该节点,之前的尾节点的下一个节点指向该节点(新的尾节点)
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            if (t == null)
                firstWaiter = node;
            else
                t.nextWaiter = node;
            lastWaiter = node;
            return node;
        }

2. FullyRelease: libera completamente el bloqueo, el valor de retorno SavedState es el valor de estado antes de que se libere el bloqueo. Antes de await (), el hilo actual debe mantener el bloqueo, que debe liberarse aquí. Debido a que await equivale a la espera del objeto, el bloqueo actual debe liberarse. Para los bloqueos reentrantes, el valor de estado representa el número de operaciones de lock (). Para liberar el bloqueo por completo, el valor de estado debe establecerse en 0. Si ocurre una interrupción durante el proceso de liberación, el waitStatus del nodo actual se configurará para cancelar (Node.CANCELLED), y cuando llegue el siguiente nodo, activará la eliminación del nodo que se ha cancelado en la cola de espera .

  Cuando se despierta, debe mantener nuevamente el bloqueo SavedState para continuar.

3. isOnSyncQueue: Determine si está en la cola de bloqueo y suspenda el hilo actual si no lo está. (Después de ser despertado, pasará de la cola condicional a la cola de bloqueo)

3.1 Si waitStatus es un valor de condición de espera o no hay un nodo predecesor (la cola de bloqueo debe tener un nodo predecesor), significa que no está en la cola de bloqueo.
       3.2 Si el nodo de publicación no está vacío, debe estar en la cola de bloqueo (la cola de bloqueo es la siguiente y la cola de condición es nextWaiter)
       3.3 findNodeFromTail: comenzando desde la cola de bloqueo como el nodo, bucle hacia adelante para encontrar si la dirección de memoria del nodo actual es el mismo, existen los mismos medios.

   final boolean isOnSyncQueue(Node node) {
        if (node.waitStatus == Node.CONDITION || node.prev == null)
            return false;
        if (node.next != null) // If has successor, it must be on queue
            return true;

        return findNodeFromTail(node);
    }

método signal ():

Mueva el hilo con el tiempo de espera más largo (si lo hay) de la cola de espera para esta condición a la cola de espera que contiene el candado.

     public final void signal() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignal(first);
        }

El hilo actual no es el titular del bloqueo y lanza una excepción. Si es así, realice el despertador.

    private void doSignal(Node first) {
            do {
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                first.nextWaiter = null;
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }

1. Apunte firstWaiter para apuntar al primer nodo después del primer nodo, porque el primer nodo está a punto de transferirse (de la cola de espera de la condición a la cola de espera con el candado). Si no hay un próximo nodo en espera, solo hay un nodo en la cola de espera. Por lo tanto, lastWaiter también debe apuntar a null.

2. Debido a que primero se debe transferir, ya no puede permanecer en la cola condicional y no se puede relacionar con la cola condicional, por lo que el nextWaiter del primero debe apuntar a nulo.

3. Realice un ciclo para determinar si la primera transferencia actual se ha realizado correctamente. Si la transferencia no es exitosa (se canceló antes de la transferencia) y hay un siguiente camarero (firstWaiter en el paso 1 ha apuntado a nextWaiter, es decir, el nuevo firstWaiter), entonces el paso 1. Si la transferencia es exitosa o no hay camarero, salga del circuito.

#transferForSignal: transfiera el nodo de la cola condicional a la cola síncrona. Si tiene éxito, devuelve verdadero. De lo contrario, falso, significa que el nodo se canceló antes de la señal.

    final boolean transferForSignal(Node node) {
        /*
         * If cannot change waitStatus, the node has been cancelled.
         */
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;

        Node p = enq(node);
        int ws = p.waitStatus;
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }

1. Cambie el estado de espera del nodo de espera a 0, si falla, significa que ha sido cancelado.

2. Si se establece correctamente en 0, el nodo se agrega al final de la cola de bloqueo. enq (nodo) devuelve el nodo antes del nodo actual .

3. Si el valor del nodo anterior es mayor que 0, el nodo anterior se cancela y el hilo del nodo actual se despierta directamente. O el nodo anterior waitStatus <= 0, y el nodo anterior waitStatus se establece en Node.SIGNAL (-1 significa que el nodo anterior necesita despertar su siguiente nodo cuando el nodo anterior libera el bloqueo) falla, entonces el subproceso del El nodo actual se despierta directamente. En circunstancias normales, estas dos condiciones no se establecerán, por lo que el subproceso del nodo actual generalmente no se despierta directamente, y se devuelve verdadero directamente, y el nodo actual se agrega a la cola de bloqueo y espera volver al bloqueo.

Luego, mire el código posterior después de ser despertado en el método await ()

     while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }

#checkInterruptWhileWaiting: Verifica si el nodo actual se interrumpe durante la suspensión. Si se interrumpe antes de enviar la señal (señal ()), devuelve THROW_IE; después de enviar la señal, devuelve REINTERRUPT; en caso contrario, devuelve 0.

  private int checkInterruptWhileWaiting(Node node) {
            return Thread.interrupted() ?
                (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
                0;
        }

THROW_IE: -1, InterruptedException debe lanzarse cuando se ha producido una interrupción y esperar salidas

REINTERRUPT: 1, cuando se ha producido una interrupción y espera salidas, el estado de la interrupción debe restablecerse

0: significa que no se ha producido ninguna interrupción

#transferAfterCancelledWait: Si es necesario, transfiera el nodo que canceló la espera a la cola de sincronización. Si el hilo se cancela antes de que se envíe la señal, devuelva verdadero (este método solo se llamará cuando el método anterior determine que el hilo está realmente interrumpido)

    final boolean transferAfterCancelledWait(Node node) {
        if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
            enq(node);
            return true;
        }
        while (!isOnSyncQueue(node))
            Thread.yield();
        return false;
    }

1. Establezca el waitStatus del nodo actual de Node.CONDITION (-2) en 0. Si tiene éxito, significa que la interrupción ocurrió antes de la señal, porque el método de la señal establecerá el waitStatus en 0. Obviamente, si la señal es establecido correctamente en 0, aquí Si se establece en 0, fallará.

2. El primer paso se establece sin éxito en 0, que representa la interrupción que ocurrió antes de la señal. Desde el nodo de transferencia de señal hasta el método de cola de bloqueo, se puede ver que si la señal se interrumpe antes, la transferencia no será exitosa. . Aquí el nodo se agrega a la cola de bloqueo.

3. El primer paso es exitoso, lo que significa que la interrupción ocurrió después de la señal, luego el método de señal establecer waitStatus en 0 para el éxito. Aquí, el ciclo while solo representa que cuando la transferencia del método de señal a la cola no se ha completado, el giro espera a que se complete. La posibilidad de esta situación es muy baja.

Se puede ver que el subproceso interrumpido se agregará activamente a la cola de bloqueo. La condición para la salida del ciclo while en el método de espera anterior se interrumpe o se agrega con éxito a la cola de bloqueo.

Luego mire el código después de esperar lanzamientos mientras sale

            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);

1. El método del método adquiridoQueued devuelve lo que representa que el subproceso actual ha adquirido el bloqueo y el valor de estado del bloqueo es SavedState. El método adquiridoQueued devuelve verdadero, lo que significa que fue interrumpido. Si fue interrumpido antes de la señal, establezca interruptMode en REINTERRUPT para volver a interrumpir más tarde.

2. node.nextWaiter! = Null. En este caso, se interrumpe antes de la señal, es decir, el método de la señal no se utiliza, entonces node.nextWaiter no es nulo y el método de la señal establecerá node.nextWaiter en nulo. Si no está vacío, elimine los nodos que se han cancelado en la cola

3. Manejar el estado de interrupción. Según interruptMode, lanza InterruptedException, vuelve a interrumpir el hilo actual o no realiza ninguna operación. THROW_IE: El método de espera arroja InterruptedException, lo que indica que ocurrió una interrupción durante await (); REINTERRUPT: Re-interrumpir el hilo actual, indicando que no fue interrumpido durante await (), pero ocurrió una interrupción después de la señal ()

Supongo que te gusta

Origin blog.csdn.net/sinat_33472737/article/details/114885786
Recomendado
Clasificación