Semáforo de AQS

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

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

Según 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 del semáforo

Semaphore se traduce literalmente como semáforo , que es un mecanismo de sincronización de procesamiento y exclusión mutua muy de la vieja escuela 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 al de las puertas por las que normalmente pasamos en las estaciones de metro. Pase la tarjeta para abrir la puerta ( operación de adquisición ), después de pasar ( acceder al área crítica ), la puerta se cierra ( operación de liberación ) y las personas que están detrás pueden continuar deslizando la tarjeta, pero antes de que pase la persona anterior, las personas que están detrás pueden solo espere en la fila ( mecanismo de cola ). Por supuesto, es imposible que una estación de metro tenga una sola puerta, ya que con varias puertas se permite el paso de varias personas al mismo tiempo.

Lo mismo ocurre con 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 empresarial:

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

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

......

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

Cuando definimos un permiso para un Semaphore , es lo mismo que un mutex, permitiendo que solo un hilo ingrese a la sección crítica a la vez . Pero cuando definimos permisos múltiples, se diferencia de los mutex:

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 ingresaron 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 las bases de JUC " ? Decíamos que era un contador de algún sincronizador:

En AQS, el estado no solo se usa para representar el estado de sincronización, sino también un contador implementado por algunos sincronizadores , como: la cantidad de subprocesos permitidos en Semaphore y la realización de la función reentrante en ReentrantLock, todo depende de estado como una característica de contador.

Primero veamos la relación entre Semaphore y AQS:

Al igual que ReentrantLock, Semaphore implementa internamente la clase abstracta de sincronizador Sync heredada de AQS y tiene dos clases de implementación, FairSync y NonfairSync. 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 y NonfairSync, para implementar el modo justo y el modo injusto respectivamente. La construcción de Semaphore es esencialmente la realización de la construcción de un sincronizador. Tomemos como ejemplo la implementación de NonfairSync en 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 a la fuente, los permisos de parámetros del constructor finalmente regresan al estado de AQS, y la función de Semaphore se realiza utilizando el estado como una característica de contador.

método de adquisición

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

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 a través de tryAcquireShared y luego únase a la cola de espera a través de doAcquireSharedInterruptiblemente después de una falla.

La lógica para 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 la cantidad de licencias disponibles: cuando la cantidad de licencias es menor que 0, se devuelve un número negativo, o después de que la cantidad de licencias se actualiza con éxito a través de CAS, se devuelve un número positivo. En este momento, doAcquireSharedInterruptiblemente agregará el hilo actual que solicita la licencia de Semaphore 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 doAcquireSharedInterruptively utilizado por Semaphore y el método adquirirQueued utilizado por ReentrantLock son la misma, pero existen sutiles diferencias de implementación:

  • Cree un nodo usando el modo Node.SHARED;
  • La actualización del nodo principal utiliza el método setHeadAndPropagate.
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 adquirirQueued se ejecuta en ReentrantLock. Después de adquirir con éxito el bloqueo, solo es necesario ejecutar setHead (nodo), entonces, ¿por qué Semaphore se activa nuevamente?

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

  • Ingresan al método nonfairTryAcquireShared al mismo tiempo. Suponiendo que solo T1 modifica con éxito la cantidad de licencia efectiva a través de compareAndSetState (disponible, restante), T1 ingresa a la sección crítica;
  • T2, T3 y T4 ingresan al método doAcquireSharedInterruptfully y construyen la cola de espera de AQS a través de addWaiter (Node.SHARED) (consulte el análisis del método addWaiter en la vida actual de AQS );
  • Suponiendo que T2 se convierte en el nodo sucesor directo del nodo principal, T2 ejecuta tryAcquireShared nuevamente para intentar obtener una licencia, y T3 y T4 ejecutan parkAndCheckInterrupt;
  • T2 obtiene exitosamente la licencia y 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 ejecutar setHeadAndPropagate para actualizar el nodo principal, juzgue el número 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 permiso de Semaphore es muy similar al proceso de bloqueo de ReentrantLock ~~
  • A continuación se analiza cómo doReleaseShared despierta los nodos 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 el método de liberación de Semaphore se divide en dos partes:

  • El método tryReleaseShared actualiza la cantidad de licencias válidas para Semaphore;
  • doReleaseShared despierta los nodos en espera.

La lógica de activación no es complicada. Todavía depende del juicio del estado del nodo waitStatus determinar si se debe ejecutar unparkSuccessor. Cuando el estado es ws == 0, el estado del nodo se actualizará a Node.PROPAGAT, es decir , propagación incondicional.

Consejos : a diferencia de ReentrantLock, Semaphore no admite el estado Node.CONDITION y el mismo ReentrantLock no admite el estado Node.PROPAGATE.

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, lo dejaremos para que usted lo explore por su cuenta.

Bueno, espero que este artículo pueda brindarte ayuda, ¡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/m0_74433188/article/details/132596694
Recomendado
Clasificación