Desbloqueo de la piedra angular de la programación concurrente de Java: exploración en profundidad de los misterios de AQS

Introducción a la cerradura

Aprendí a través de un análisis en profundidad de la palabra clave sincronizada en Javasynchronized Aprendamos otra implementación importante de la sincronización de subprocesos de programación concurrente Lock.

LockLa interfaz define un conjunto de métodos para controlar el acceso de subprocesos a los recursos compartidos, de la siguiente manera: 

En comparación con el  Synchronized bloqueo de sincronización que requiere que la JVM adquiera y libere el bloqueo implícitamente, Lock el bloqueo de sincronización (en adelante,  Lock el bloqueo) debe adquirir y liberar el bloqueo de forma explícita, lo que proporciona más flexibilidad para adquirir y liberar el bloqueo. Lock El funcionamiento básico de la cerradura se implementa a través del bloqueo optimista, pero dado que  Lock la cerradura también se suspende cuando está bloqueada, sigue siendo una cerradura pesimista. Simplemente podemos comparar los siguientes dos bloqueos de sincronización a través de una imagen para comprender sus respectivas características:

Lockes una interfaz que solo proporciona métodos abstractos para liberar y adelantar bloqueos, y las siguientes implementaciones específicas se proporcionan en JUC.

  • ReentrantLock, una cerradura reentrante, pertenece al tipo de cerradura exclusiva y tiene synchronizedfunciones similares.
  • ReentrantReadWriteLock(RRW), bloqueo de lectura-escritura reentrante, se mantienen dos bloqueos en esta clase, uno ReadLockes y el otro es WriteLockque implementan Lockla interfaz respectivamente.
  • StampedLock, un nuevo mecanismo de bloqueo introducido en Java 8, que es ReentrantReadWriteLockuna versión mejorada de .

AbstractQueuedSynchronizer(AQS)Todos están implementados por clases dependientes  .

Introducción a AQS

AQS es una clase abstracta, que principalmente mantiene el valor (estado) de una variable de estado de sincronización de recursos y una cola bidireccional CLH para almacenar subprocesos en cola Al mismo tiempo, el mecanismo de bloqueo de subprocesos espera y bloquea la asignación cuando se despierta. La estructura específica es la siguiente:

AQSUse una volatilevariable miembro de tipo int para representar el estado de sincronización, complete el trabajo de puesta en cola de los subprocesos de adquisición de recursos a través de la cola integrada de primero en entrar, primero en salir, y CLHencapsule cada subproceso que desee aprovechar los recursos en un nodo de nodo para realizar la asignación de bloqueos La modificación del valor de Estado se completa a través de CAS.

El bloqueo CLH es en realidad un tipo de bloqueo de giro justo basado en el hambre sin subprocesos de cola lógica. Se llama bloqueo CLH porque es la invención de tres maestros, Craig, Landin y Hagersten.

AQS tiene una clase interna Node, el nodo Node encapsula cada subproceso que espera adquirir recursos, lo que incluye el propio subproceso y su estado de espera que debe sincronizarse, por ejemplo, si está bloqueado, si está esperando para activarse, si tiene ha sido cancelado, etc

Aquí se puede ver intuitivamente que el nodo del nodo almacenará el hilo de recursos de solicitud actualmente bloqueado, y la variable waitStatus indica el estado de espera del nodo del nodo actual.Hay cinco valores de la siguiente manera:

  • CANCELADO (1): Indica que el nodo actual ha cancelado la programación. Cuando se agote el tiempo o se interrumpa (en el caso de una respuesta interrumpida), activará un cambio a este estado, y el nodo después de ingresar a este estado ya no cambiará.
  • SEÑAL (-1): Indica que el nodo sucesor está esperando que el nodo actual se despierte. Cuando el nodo sucesor se une a la cola, el estado del nodo predecesor se actualizará a SEÑAL.
  • CONDICIÓN (-2): Indica que el nodo está esperando la Condición. Cuando otros subprocesos llaman al método signal() de la Condición, el nodo en el estado CONDICIÓN será adquirir el bloqueo de sincronización.
  • PROPAGAR (-3): en el modo compartido, el nodo predecesor no solo despertará a su nodo sucesor, sino que también puede despertar al nodo sucesor.
  • 0 : el estado predeterminado cuando se ponen en cola nuevos nodos.

Los métodos clave de AQS son los siguientes:

  1. adquirir(): Sirve para que el hilo obtenga el estado del sincronizador, si no se puede obtener, el hilo entrará en estado bloqueado y esperará a que otros hilos liberen el sincronizador.
  2. release(): se usa para que el subproceso libere el estado del sincronizador y despierte otros subprocesos en la cola de espera.
  3. tryAcquire(int): Modo exclusivo. Intenta obtener el recurso, devolviendo verdadero en caso de éxito y falso en caso de falla.
  4. tryRelease(int): Modo exclusivo. Intenta liberar el recurso, devolviendo verdadero en caso de éxito y falso en caso de falla.
  5. tryAcquireShared(int): método de compartir. Trate de buscar recursos. Un número negativo indica falla; 0 indica éxito, pero no quedan recursos; un número positivo indica éxito, pero quedan recursos.
  6. tryReleaseShared(int): método de compartir. Intenta liberar el recurso y devuelve verdadero si se permite que los nodos en espera posteriores se activen después de la liberación; de lo contrario, devuelve falso.

Análisis de la implementación de AQS a través de ReentrantLock

ReentrantLockEs un sincronizador importante y de uso común en el paquete concurrente de JUC, y su capa inferior se realiza confiando en AQS. A continuación veremos ReentrantLockla implementación en detalle.

Adquirir proceso de bloqueo

Diagrama de interacción de bloqueo preventivo de ReentrantLock:

ReentrantLockSyncNofairSyncAQSlocklockacquiretryAcquirenofairTryAcquireverdadero/falsoJuzgar el éxito del bloqueo de preferencia y el fracaso de la preferencia addWaiterReentrantLockSyncNofairSyncAQS

El proceso general es el siguiente:

Obtener análisis de código fuente de bloqueo

Echemos un vistazo al código fuente de estos métodos:

Abra el método de la clase interna que lock.lock()salta en este momento ReentrantLockNonfairSync(NonfairSync继承自AQS)lock()

adquirir

acquire()El método es el método en AQS, aquí está que el subproceso no logra bloquear y ocupar recursos, y el subproceso se coloca en la cola CLH para esperar la notificación para activar la entrada principal

 Los pasos básicos del proceso son los siguientes:

  1. tryAcquire() intenta adquirir recursos directamente y regresa directamente si tiene éxito ( aquí refleja el bloqueo injusto, cada subproceso intentará adelantarse y bloquearse una vez al adquirir el bloqueo, y puede haber otros subprocesos esperando en la cola CLH );
  2. addWaiter() agrega el hilo al final de la cola de espera y lo marca como modo exclusivo;
  3. adquirirQueued() hace que el subproceso se bloquee en la cola de espera para adquirir recursos y regresa solo después de que se adquieran los recursos. Devuelve verdadero si se interrumpe durante todo el proceso de espera, de lo contrario devuelve falso.
  4. Si el subproceso se interrumpe mientras espera, no responde. La autointerrupción selfInterrupt() solo se realiza después de obtener recursos para compensar la interrupción.

probarAdquirir

La implementación de FairSync.tryAcquire es la siguiente:

 
 

Java

copiar codigo

final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }

Este código es la implementación del método nonfairTryAcquire en AbstractQueuedSynchronizer (AQS). Este método se utiliza para intentar obtener injustamente el estado de un sincronizador.

  1. Primero, obtenga el hilo actual y obtenga el valor del estado actual c.
  2. Si el valor de estado actual c es 0, significa que el sincronizador no está ocupado actualmente por otros subprocesos. En este caso, use directamente el método compareAndSetState para modificar el estado de 0 a adquiere (el valor esperado es 0 y el valor de actualización es adquiere). Si la modificación tiene éxito, significa que el subproceso actual ha adquirido correctamente el sincronizador, y el subproceso actual se establece como el propietario del bloqueo exclusivo y luego devuelve verdadero.
  3. Si el valor del estado actual c no es 0, significa que el sincronizador ha sido ocupado por otros subprocesos. En este momento, es necesario juzgar si el subproceso actual es el propietario exclusivo del bloqueo del sincronizador. Si es así, agregue adquisiciones al valor del estado actual (aumente el número de veces que se mantiene el bloqueo) y devuelva verdadero.
  4. Si no se cumple ninguna de las condiciones anteriores, significa que el subproceso actual no puede obtener el estado del sincronizador y devuelve falso.

añadirCamarero

Este método se utiliza para agregar el subproceso actual al final de la cola de espera CLH y devolver el nodo donde se encuentra el subproceso actual.

 
 

Java

copiar codigo

private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure Node pred = tail; if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } enq(node); return node; }

  1. Primero, cree un nuevo nodo Nodo que contenga el subproceso actual y el modo de espera especificado.
  2. Intenta agregar nuevos nodos a la cola mediante la ruta rápida. Primero obtenga el nodo final pred de la cola actual, si el nodo final no es nulo, apunte el prev del nuevo nodo a pred e intente usar el método compareAndSetTail para actualizar el nodo final al nuevo nodo. Si la actualización es exitosa, apunte el siguiente nodo de cola original antes del nuevo nodo y devuelva el nuevo nodo.
  3. Si falla la ruta rápida, es decir, el nodo final es nulo o falla compareAndSetTail, significa que otros subprocesos están modificando la cola al mismo tiempo. En este momento, el nuevo nodo debe agregarse a la cola mediante una operación de puesta en cola completa (enq).
  4. En el método enq, los nuevos nodos se agregan primero al final de la cola mediante una operación de giro.
  5. Devolver el nuevo nodo.

La función de este método es agregar el subproceso actual como un subproceso en espera a la cola de sincronización. Emplea una estrategia optimista, primero tratando de agregar nuevos nodos al final de la cola utilizando la ruta rápida y, en su defecto, se utiliza una operación de puesta en cola completa. De esta manera, la competencia y el bloqueo de subprocesos se pueden evitar en la mayoría de los casos y se puede mejorar el rendimiento de la concurrencia.

adquiriren cola

Este método se utiliza para adquirir el bloqueo en la cola de sincronización. Cuando el bloqueo no se puede adquirir directamente, el subproceso actual se agrega a la cola de espera y gira y espera.

 
 

Este

copiar codigo

final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return interrupted; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }

  1. En primer lugar, establezca un bit de indicador que falle en verdadero para registrar si se produce una excepción durante el proceso de adquisición del bloqueo.
  2. Repita un bloque try-catch-finally hasta que el bloqueo se adquiera o interrumpa con éxito.
  3. En el ciclo, primero obtenga el nodo predecesor p del nodo actual y juzgue si el nodo predecesor del nodo actual es el nodo principal e intente adquirir el bloqueo con éxito (llamando al método tryAcquire). Si es así, configure el nodo actual como el nuevo nodo principal, desconecte el nodo principal original del nodo actual, configure el siguiente puntero del nodo predecesor en nulo y finalmente configure el bit de indicador como falso y regrese al estado interrumpido interrumpido .
  4. Si el nodo precursor p no cumple las condiciones, llame al método shouldParkAfterFailedAcquire para determinar si es necesario bloquear el subproceso actual y llame al método parkAndCheckInterrupt para bloquear el subproceso y comprobar si está interrumpido. Si se interrumpe, establezca el estado interrumpido interrumpido en verdadero.
  5. Regrese al paso 2 y continúe intentando adquirir bloqueos o bloqueos.
  6. Si aún no se puede adquirir el bloqueo al final del bucle, es decir, se produce una excepción durante el proceso de adquisición del bloqueo, llame al método cancelAcquire para cancelar la operación de adquisición del nodo actual.

deberíaParkAfterFailedAcquire

Este método se utiliza para determinar si el subproceso actual debe bloquearse después de que falle la adquisición del bloqueo:

 
 

Este

copiar codigo

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; if (ws == Node.SIGNAL) return true; if (ws > 0) { do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }

  1. Primero, obtenga el estado de espera del nodo precursor pred.
  2. Si el estado de espera waitStatus es igual a Node.SIGNAL, significa que el nodo predecesor ha establecido el estado para liberar el bloqueo para enviar una señal, por lo que el nodo actual puede bloquear de forma segura y devolver verdadero.
  3. Si el estado de espera waitStatus es mayor que 0, significa que el nodo predecesor ha sido cancelado. Recorra el nodo predecesor omitido y el nodo cancelado anterior a él, hasta que se encuentre un nodo predecesor pred cuyo estado de espera sea menor o igual a 0. A continuación, actualice el puntero anterior del nodo actual al nodo predecesor encontrado pred y apunte el siguiente puntero del nodo predecesor al nodo actual.
  4. Si el estado de espera waitStatus es 0 o Node.PROPAGATE, indica que se necesita una señal para activar el nodo actual, pero no se bloquea inmediatamente. Llame al método compareAndSetWaitStatus para cambiar el estado de espera del nodo predecesor antes de Node.SIGNAL para indicar que se necesita una señal.
  5. Devuelve falso, lo que indica que no es necesario bloquear el subproceso actual.

Desbloquee el análisis del código fuente

desbloquear

NonfairSync(NonfairSync继承自AQS)método unlock():

 
 

Java

copiar codigo

public void unlock() { sync.release(1); }

liberar

 
 

Java

copiar codigo

public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }

intentar liberar

Este método se utiliza para liberar recursos de bloqueo.

 
 

Java

copiar codigo

protected final boolean tryRelease(int releases) { int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free; }

  1. Primero, obtenga el valor de estado c = getState() - liberaciones del bloqueo actual, que indica el nuevo estado después de que se libere el bloqueo.

  2. Determine si el subproceso actual es el propietario del bloqueo exclusivo y, en caso contrario, lance una IllegalMonitorStateException.

  3. Proceso según el nuevo estado c:

    • Si el nuevo estado c es igual a 0, significa que el bloqueo se libera por completo. Establezca exclusiveOwnerThread en nulo, lo que indica que actualmente no hay ningún propietario, y establezca el indicador libre en verdadero.
    • De lo contrario, el estado del bloqueo de actualización es el nuevo estado c.
  4. Devuelve el indicador libre que indica si el bloqueo se ha liberado por completo.

unparkSucesor

Si la liberación de recursos anterior devuelve verdadero con éxito, entonces unparkSuccessor()se ejecutará el siguiente subproceso en la cola de espera para despertar

 
 

scs

copiar codigo

private void unparkSuccessor(Node node) { /* * If status is negative (i.e., possibly needing signal) try * to clear in anticipation of signalling. It is OK if this * fails or if status is changed by waiting thread. */ int ws = node.waitStatus; if (ws < 0) compareAndSetWaitStatus(node, ws, 0); /* * Thread to unpark is held in successor, which is normally * just the next node. But if cancelled or apparently null, * traverse backwards from tail to find the actual * non-cancelled successor. */ Node s = node.next; if (s == null || s.waitStatus > 0) { s = null; for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } if (s != null) LockSupport.unpark(s.thread); }

  1. Primero, juzgue el estado de espera ws del nodo, si ws es menor que 0, intente establecerlo en 0. Este es un estado de preaclarado que puede requerir una señal. Incluso si esta operación falla o el subproceso en espera cambia de estado, las operaciones posteriores no se verán afectadas.
  2. Obtenga los nodos sucesores del nodo, normalmente el nodo sucesor es el siguiente nodo del nodo actual. Pero si el nodo sucesor está cancelado o vacío, avanza desde la cola para encontrar el nodo sucesor que no ha sido cancelado. Esto es para encontrar los nodos que realmente necesitan ser activados.
  3. Si se encuentran los nodos sucesores que deben activarse, llame al método LockSupport.unpark(s.thread) para activar el subproceso correspondiente al nodo.

Bloqueo justo y bloqueo injusto

ReentrantLockSe divide en bloqueo justo y bloqueo injusto. El método de construcción predeterminado es bloqueo injusto. Si necesitamos construir un bloqueo justo, solo necesitamos pasar el parámetro verdadero:

 
 

Java

copiar codigo

Lock lock = new ReentrantLock(true);

Su proceso de bloqueo y desbloqueo es casi el mismo que el bloqueo injusto anterior, con algunas diferencias en algunos detalles:

 
 

Este

copiar codigo

protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }

Aquí, lock()no se usa CAS para tratar de bloquear y ocupar recursos como bloqueos injustos, es una llamada directa acquire(1)para ingresar a la cola. Hay más lógica de hasQueuedPredecessors en el bloqueo

 
 

Java

copiar codigo

public final boolean hasQueuedPredecessors() { Node t = tail; // Read fields in reverse initialization order Node h = head; Node s; return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());

Supongo que te gusta

Origin blog.csdn.net/BASK2312/article/details/131305744
Recomendado
Clasificación