Explicar los miembros de la familia AQS: Semáforo

Siga: Wang Youzhi , un pescador de oro mutuo que comparte tecnología Java de núcleo duro.
Bienvenido a unirse al grupo de personas de Java que llevan cubos : personas de Java que se enriquecen juntas

Hoy hablaremos sobre Semaphore, otro miembro importante de la familia AQS. Solo recopilé una pregunta de la entrevista sobre Semaphore, preguntando "qué" y "cómo lograrlo":

  • ¿Qué son los semáforos? ¿Cómo se logra?

De acuerdo con nuestra práctica, todavía analizamos Semaphore de acuerdo con los tres pasos de "qué", "cómo usar" y "cómo implementar". Además, hoy se proporcionan soluciones a los problemas .

Uso de semáforo

Semáforo se traduce literalmente como semáforo , que es un mecanismo muy de la vieja escuela para procesar la sincronización y la exclusión mutua en informática . A diferencia de un mutex, permite que un número específico de subprocesos o procesos accedan a recursos compartidos .

El mecanismo de Semaphore para manejar la sincronización y la exclusión mutua es muy similar a las puertas que solemos pasar en las estaciones de metro. Pase la tarjeta para abrir la puerta ( acquireoperación ), después de pasar ( área crítica de acceso ), la puerta se cierra ( releaseoperación ), y las personas detrás pueden continuar deslizando la tarjeta, pero antes de que pase la persona anterior, las personas detrás solo pueden espera en línea ( mecanismo de cola ). Por supuesto, es imposible que una estación de metro tenga una sola puerta, con varias puertas se permite el paso de varias personas al mismo tiempo.

inserte la descripción de la imagen aquí

Lo mismo es cierto para los semáforos. Defina el número de licencias a través del constructor, solicite una licencia cuando lo use y libere la licencia después de procesar la lógica comercial:

// 信号量中定义1个许可
Semaphore semaphore = new Semaphore(1);

// 申请许可
semaphore.acquire();

......

// 释放许可
semaphore.release();

Cuando definimos un permiso para un semáforo , es lo mismo que un mutex, lo que permite que solo un subproceso ingrese a la sección crítica a la vez . Pero cuando definimos múltiples permisos, difiere de los mutexes:

Semaphore semaphore = new Semaphore(3);
for(int i = 1; i < 5; i++) {
  int finalI = i;
  new Thread(()-> {
    try {
      semaphore.acquire();
      System.out.println("第[" + finalI + "]个线程获取到semaphore");
      TimeUnit.SECONDS.sleep(10);
      semaphore.release();
    } catch (InterruptedException e) {
      throw new RuntimeException(e);
    }
  }).start();
}

Ejecute este código y podrá ver que los tres subprocesos han ingresado a la sección crítica al mismo tiempo, y solo el cuarto subproceso está bloqueado fuera de la sección crítica.

El principio de realización de Semaphore

¿Recuerda el estado de sincronización mencionado en " La vida actual de AQS, construyendo los cimientos de JUC " ? Decíamos que era un contador para algún sincronizador:

En AQS, el estado no solo se usa para indicar el estado de sincronización, sino también un contador implementado por algunos sincronizadores , como: la cantidad de subprocesos permitidos para pasar en Semaphore y la realización de funciones reentrantes en ReentrantLock, todo depende de statelas funciones como contadores.

Primero veamos la relación entre Semaphore y AQS:

inserte la descripción de la imagen aquí

Al igual que ReentrantLock, Semaphore implementa internamente la clase abstracta de sincronizador heredada de AQS Syncy tiene FairSyncdos NonfairSyncclases de implementación. A continuación, verificaremos nuestra afirmación anterior analizando el código fuente de Semaphore.

Método de construcción

Semaphore proporciona dos constructores:

public Semaphore(int permits) {
  sync = new NonfairSync(permits);
}

public Semaphore(int permits, boolean fair) {
  sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}

Se puede ver que las ideas de diseño de Semaphore y ReentrantLock son consistentes. Semaphore también implementa dos sincronizadores FairSynce NonfairSyncimplementa el modo justo y el modo injusto respectivamente. La construcción de Semaphore es esencialmente la realización de la construcción del sincronizador. Tomemos NonfairSynccomo ejemplo la implementación del modo injusto:

public class Semaphore implements java.io.Serializable {
  static final class NonfairSync extends Sync {
    NonfairSync(int permits) {
      super(permits);
    }
  }
  
  abstract static class Sync extends AbstractQueuedSynchronizer {
    Sync(int permits) {
      setState(permits);
    }
  }
}

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
  protected final void setState(int newState) {
    state = newState;
  }
}

Volviendo al origen, los parámetros del constructor permitsfinalmente se devuelven a AQS statey statela función de Semaphore se realiza utilizando las características de un contador.

método de adquisición

Ahora que hemos establecido una cierta cantidad de permisos (permisos) para el Semáforo, necesitamos Semaphore#acquireobtener el permiso a través del método e ingresar a la sección crítica "custodiada" por el Semáforo:

public class Semaphore implements java.io.Serializable {
  public void acquire() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
  }
}

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
  public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
    if (Thread.interrupted()) {
      throw new InterruptedException();
    }
    if (tryAcquireShared(arg) < 0) {
      doAcquireSharedInterruptibly(arg);
    }
  }
}

Estos dos pasos son muy similares a ReentrantLock: primero, intente obtener la licencia directamente y luego agréguela a la cola de espera tryAcquireShareddespués de fallar .doAcquireSharedInterruptibly

La lógica de obtener permiso directamente en Semaphore es muy simple:

static final class NonfairSync extends Sync {
  protected int tryAcquireShared(int acquires) {
    return nonfairTryAcquireShared(acquires);
  }
}

abstract static class Sync extends AbstractQueuedSynchronizer {
  final int nonfairTryAcquireShared(int acquires) {
    for (;;) {
      // 获取可用许可数量
      int available = getState();
      // 计算许可数量
      int remaining = available - acquires;
      if (remaining < 0 || compareAndSetState(available, remaining)) {
        return remaining;
      }
    }
  }
}

El primero es obtener y reducir el número de licencias disponibles, cuando el número de licencias es menor a 0, se devuelve un número negativo, o después de que el número de licencias se actualiza con éxito a través de CAS, se devuelve un número positivo. En este momento, doAcquireSharedInterruptiblyel subproceso actual que solicita la licencia de Semaphore se agregará a la cola de espera de AQS.

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {    
 private void doAcquireSharedInterruptibly(int arg) throws InterruptedException {
   // 创建共享模式的等待节点
   final Node node = addWaiter(Node.SHARED);
   try {
     for (;;) {
       final Node p = node.predecessor();
       if (p == head) {
         // 再次尝试获取许可,并返回剩余许可数量
         int r = tryAcquireShared(arg);
         if (r >= 0) {
           // 获取成功,更新头节点
           setHeadAndPropagate(node, r);
           p.next = null;
           return;
         }
       }
       // 获取失败进入等待状态
       if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) {
         throw new InterruptedException();
       }
     }
   } catch (Throwable t) {
     cancelAcquire(node);
     throw t;
   }
 }
}

La lógica central de Semaphore es la misma doAcquireSharedInterruptiblyque la del método ReentrantLockutilizado acquireQueued, pero existen sutiles diferencias de implementación:

  • Cree un Node.SHAREDpatrón de uso de nodos;

  • La actualización del nodo principal utiliza setHeadAndPropagateel método.

private void setHeadAndPropagate(Node node, int propagate) {
  Node h = head;
  setHead(node);
  
  // 是否要唤醒等待中的节点
  if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) {
    Node s = node.next;
    if (s == null || s.isShared()) {
      // 唤醒等待中的节点
      doReleaseShared();
    }
  }
}

Sabemos que se ejecuta en ReentrantLock.Después acquireQueuedde que el bloqueo se adquiere con éxito, solo necesita ejecutarse setHead(node), entonces, ¿por qué Semaphore se despierta de nuevo?

Supongamos que hay 3 semáforos con licencia con T1, T2, T3 y T4 compitiendo por un total de 4 subprocesos al mismo tiempo:

  • Ingresan nonfairTryAcquireSharedal método al mismo tiempo, suponiendo que solo T1 compareAndSetState(available, remaining)modifica con éxito el número de permisos válidos, y T1 ingresa a la sección crítica;

  • T2, T3 y T4 ingresan doAcquireSharedInterruptiblyal método y addWaiter(Node.SHARED)construyen la cola de espera de AQS (consulte el análisis del método en la vida actual de AQSaddWaiter );

  • Suponiendo que T2 se convierte en el nodo sucesor directo del nodo principal, T2 se ejecuta tryAcquireSharedpara intentar obtener el permiso nuevamente, y T3 y T4 se ejecutan parkAndCheckInterrupt;

  • T2 obtiene exitosamente la licencia e ingresa a la sección crítica, en este momento a Semaphore le queda 1 licencia, mientras que T3 y T4 se encuentran en estado suspendido.

En este escenario, solo funcionan dos licencias, lo que obviamente no está en línea con nuestra intención original. Por lo tanto, al actualizar el setHeadAndPropagatenodo principal, juzgue la cantidad de licencias restantes y continúe activando los nodos sucesores cuando el número sea mayor que 0 .

Consejos :

  • El proceso de obtención de permisos de Semaphore es muy similar al proceso de bloqueo de ReentrantLock~~

  • El siguiente análisis doReleaseSharedes cómo despertar el nodo en espera.

método de liberación

El método de liberación de Semaphore es muy simple:

public class Semaphore implements java.io.Serializable {
  public void release() {
    sync.releaseShared(1);
  }
  
  abstract static class Sync extends AbstractQueuedSynchronizer {
    protected final boolean tryReleaseShared(int releases) {
      for (;;) {
        int current = getState();
        // 计算许可数量
        int next = current + releases;
        if (next < current) {
          throw new Error("Maximum permit count exceeded");
        }
        // 通过CAS更新许可数量
        if (compareAndSetState(current, next)) {
            return true;
        }
      }
    }
  }
}

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
  public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
      doReleaseShared();
      return true;
    }
    return false;
  }
  
  private void doReleaseShared() {
    for (;;) {
      Node h = head;
      // 判断AQS的等待队列是否为空
      if (h != null && h != tail) {
        int ws = h.waitStatus;
        // 判断当前节点是否处于待唤醒的状态
        if (ws == Node.SIGNAL) {
          if (!h.compareAndSetWaitStatus(Node.SIGNAL, 0)){
            continue;
          }
          unparkSuccessor(h);
        } else if (ws == 0 && !h.compareAndSetWaitStatus(0, Node.PROPAGATE)) {
          // 状态为0时,更新节点的状态为无条件传播
          continue;
        }
      }
      if (h == head) {
        break;
      }
    }
  }
}

Podemos ver que releaseel método de Semaphore se divide en dos partes:

  • tryReleaseSharedEl método actualiza el número de licencias válidas para Semaphore;

  • doReleaseSharedDespierta los nodos en espera.

La lógica de la reactivación no es complicada. Sigue siendo el juicio del estado del nodo waitStatuspara determinar si es necesario ejecutarlo unparkSuccessor. Cuando el estado es ws == 0, el estado del nodo se actualizará a Node.PROPAGAT, es decir, propagación incondicional.

Sugerencias : a diferencia de ReentrantLock, Semaphore no admite Node.CONDITIONel estado y el mismo ReentrantLock no admite Node.PROPAGATEel estado.

epílogo

Este es el final del contenido sobre Semaphore. Hoy solo analizamos específicamente la implementación del método central en el modo injusto. En cuanto a la implementación del modo justo y otros métodos, dejaremos que lo explore por su cuenta.

Bueno, espero que este artículo te pueda ayudar, ¡hasta la próxima! Finalmente, todos pueden prestar atención a la columna de Wang Youzhi " ¿Qué preguntan las entrevistas de Java?" ".

Supongo que te gusta

Origin blog.csdn.net/wyz_1945/article/details/131097692
Recomendado
Clasificación