Semaphore of AQS

Today we will talk about Semaphore, another important member of the AQS family. I only collected one interview question about Semaphore, asking "what" and "how to achieve it":

  • What are Semaphore? How is it achieved?

According to our practice, we still analyze Semaphore according to the three steps of "what", "how to use" and "how to implement". In addition, problem solutions are provided today .

Use of Semaphore

Semaphore is literally translated as a semaphore , which is a very Old School mechanism for processing synchronization and mutual exclusion in computer science . Unlike a mutex, it allows a specified number of threads or processes to access shared resources .

Semaphore's mechanism for handling synchronization and mutual exclusion is very similar to the gates we usually pass through subway stations. Swipe the card to open the gate (acquire operation ), after passing ( access critical area ), the gate closes (release operation ), and the people behind can continue to swipe the card, but before the previous person passes, the people behind can only wait in line (queue mechanism ). Of course, it is impossible for a subway station to have only one gate. With several gates, several people are allowed to pass through at the same time.

The same is true for semaphores. Define the number of licenses through the constructor, apply for a license when using it, and release the license after processing the business logic:

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

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

......

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

When we define a permission for a Semaphore , it is the same as a mutex, allowing only one thread to enter the critical section at a time . But when we define multiple permissions, it differs from 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();
}

Execute this code and you can see that all three threads have entered the critical section at the same time, and only the fourth thread is blocked outside the critical section.

The realization principle of Semaphore

Remember the synchronization state mentioned in " AQS's present life, building the foundation of JUC " ? We were saying it was a counter for some synchronizer:

In AQS, state is not only used to represent the synchronization state, but also a counter implemented by some synchronizers , such as: the number of threads allowed to pass in Semaphore, and the realization of the reentrant feature in ReentrantLock, all rely on the state as a counter feature.

Let's first look at the relationship between Semaphore and AQS:

Like ReentrantLock, Semaphore internally implements the synchronizer abstract class Sync inherited from AQS, and has two implementation classes, FairSync and NonfairSync. Next, we will verify our previous statement by analyzing the source code of Semaphore.

Construction method

Semaphore provides two constructors:

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

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

It can be seen that the design ideas of Semaphore and ReentrantLock are consistent. Semaphore also implements two synchronizers, FairSync and NonfairSync, to implement fair mode and unfair mode respectively. The construction of Semaphore is essentially the realization of constructing a synchronizer. Let's take the implementation of NonfairSync in the unfair mode as an example:

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;
	}
}

Tracing back to the source, the parameter permits of the constructor finally returns to the state of AQS, and the function of Semaphore is realized by using the state as a counter characteristic.

acquire method

Now that we have set a certain number of permissions (permits) for the Semaphore, we need to Semaphore#acquireobtain the permission through the method and enter the critical section "guarded" by the 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);
		}
	}
}

These two steps are very similar to ReentrantLock. First try to obtain the license directly through tryAcquireShared, and then join the waiting queue through doAcquireSharedInterruptibly after failure.

The logic of obtaining permission directly in Semaphore is very 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;
			}
		}
	}
}

The first is to obtain and reduce the number of available licenses. When the number of licenses is less than 0, a negative number is returned, or after the number of licenses is successfully updated through CAS, a positive number is returned. At this time, doAcquireSharedInterruptibly will add the current thread applying for the Semaphore license to the waiting queue of 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;
		}
	}
}

The core logic of the doAcquireSharedInterruptibly used by Semaphore and the acquireQueued method used by ReentrantLock are the same, but there are subtle implementation differences:

  • Create a node using the Node.SHARED mode;
  • Updating the head node uses the setHeadAndPropagate method.
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();
		}
	}
}

We know that acquireQueued is executed in ReentrantLock. After successfully acquiring the lock, only need to execute setHead(node), so why does Semaphore wake up again?

Suppose there are 3 licensed Semaphore with T1, T2, T3 and T4 competing for a total of 4 threads at the same time:

  • They enter the nonfairTryAcquireShared method at the same time. Assuming that only T1 successfully modifies the effective license quantity through compareAndSetState(available, remaining), T1 enters the critical section;
  • T2, T3 and T4 enter the doAcquireSharedInterruptibly method, and build the AQS waiting queue through addWaiter(Node.SHARED) (refer to the analysis of the addWaiter method in AQS's present life );
  • Assuming that T2 becomes the direct successor node of the head node, T2 executes tryAcquireShared again to try to obtain a license, and T3 and T4 execute parkAndCheckInterrupt;
  • T2 successfully obtains the license and enters the critical section. At this time, Semaphore has 1 license left, while T3 and T4 are in the suspended state.

In this scenario, only two licenses work, which is obviously not in line with our original intention. Therefore, when executing setHeadAndPropagate to update the head node, judge the number of remaining licenses, and continue to wake up the successor nodes when the number is greater than 0.

Tips

  • Semaphore's process of obtaining permission is highly similar to the process of ReentrantLock locking~~
  • The following analyzes how doReleaseShared wakes up waiting nodes.

release method

The release method of Semaphore is very 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;
			}
		}
	}
}

We can see that the release method of Semaphore is divided into two parts:

  • The tryReleaseShared method updates the number of valid licenses for the Semaphore;
  • doReleaseShared wakes up waiting nodes.

The wake-up logic is not complicated. It is still the judgment of the node status waitStatus to determine whether unparkSuccessor needs to be executed. When the status is ws == 0, the node status will be updated to Node.PROPAGAT, that is, unconditional propagation.

Tips : Unlike ReentrantLock, Semaphore does not support the Node.CONDITION state, and the same ReentrantLock does not support the Node.PROPAGATE state.

epilogue

This is the end of the content about Semaphore. Today we only specifically analyzed the implementation of the core method in the unfair mode. As for the implementation of the fair mode and other methods, we will leave it to you to explore on your own.

Well, I hope this article can bring you some help, see you next time! Finally, everyone is welcome to pay attention to Wang Youzhi 's column " What do Java interviews ask?" ".

Guess you like

Origin blog.csdn.net/m0_74433188/article/details/132596694
AQS
AQS