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 ( acquire
operación ), después de pasar ( área crítica de acceso ), la puerta se cierra ( release
operació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.
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
state
las funciones como contadores.
Primero veamos la relación entre Semaphore y AQS:
Al igual que ReentrantLock, Semaphore implementa internamente la clase abstracta de sincronizador heredada de AQS Sync
y tiene FairSync
dos NonfairSync
clases 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 FairSync
e NonfairSync
implementa 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 NonfairSync
como 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 permits
finalmente se devuelven a AQS state
y state
la 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#acquire
obtener 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 tryAcquireShared
despué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, doAcquireSharedInterruptibly
el 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 doAcquireSharedInterruptibly
que la del método ReentrantLock
utilizado acquireQueued
, pero existen sutiles diferencias de implementación:
-
Cree un
Node.SHARED
patrón de uso de nodos; -
La actualización del nodo principal utiliza
setHeadAndPropagate
el 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 acquireQueued
de 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
nonfairTryAcquireShared
al método al mismo tiempo, suponiendo que solo T1compareAndSetState(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
doAcquireSharedInterruptibly
al método yaddWaiter(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
tryAcquireShared
para intentar obtener el permiso nuevamente, y T3 y T4 se ejecutanparkAndCheckInterrupt
; -
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 setHeadAndPropagate
nodo 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
doReleaseShared
es 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 release
el método de Semaphore se divide en dos partes:
-
tryReleaseShared
El método actualiza el número de licencias válidas para Semaphore; -
doReleaseShared
Despierta los nodos en espera.
La lógica de la reactivación no es complicada. Sigue siendo el juicio del estado del nodo waitStatus
para 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.CONDITION
el estado y el mismo ReentrantLock no admite Node.PROPAGATE
el 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?" ".