1. Introduction
- Semaphore, a semaphore, holds a series of permissions. Each call to the acquire() method will consume a license, and each call to the release() method will return a license.
- Semaphore is usually used to limit the number of accesses to shared resources at the same time, which is often referred to as flow restriction.
- Semaphore semaphore, obtain the flow chart of peer certificate.
2. Introductory case
Case 1
public class Pool { /** * The maximum number of threads that can access resources at the same time */ private static final int MAX_AVAILABLE = 100; /** * Semaphore representation: obtainable object pass */ private final Semaphore available = new Semaphore(MAX_AVAILABLE, true); /** * Shared resources can be imagined as items array, which stores Connection objects and simulates Connection pool */ protected Object[] items = new Object[MAX_AVAILABLE]; /** * The occupation of shared resources corresponds to the items array one by one, for example: * items[0]If the object is occupied by an external thread, then used[0] == true, otherwise used[0] == false */ protected boolean[] used = new boolean[MAX_AVAILABLE]; /** * Get a free object * If there are no free objects in the current pool, wait Until there are free objects */ public Object getItem() throws InterruptedException { // Each call to acquire() consumes a license available.acquire(); return getNextAvailableItem(); } /** * Return objects to pool */ public void putItem(Object x) { if (markAsUnused(x)) available.release(); } /** * Get an idle Object in the pool. If it succeeds, it will return Object. If it fails, it will return Null * After success, the corresponding used[i] = true */ private synchronized Object getNextAvailableItem() { for (int i = 0; i < MAX_AVAILABLE; ++i) { if (!used[i]) { used[i] = true; return items[i]; } } return null; } /** * Return the object to the pool, and return true after successful return * Return failed: * 1.The object reference does not exist in the pool, and false is returned * 2.The object reference exists in the pool, but the current state of the object is idle, which also returns false */ private synchronized boolean markAsUnused(Object item) { for (int i = 0; i < MAX_AVAILABLE; ++i) { if (item == items[i]) { if (used[i]) { used[i] = false; return true; } else return false; } } return false; } }
Case 2
public class SemaphoreTest02 { public static void main(String[] args) throws InterruptedException { // Declare the semaphore. The initial permissions are 2 // fair mode: fair is true final Semaphore semaphore = new Semaphore(2, true); Thread tA = new Thread(() ->{ try { // Each call to acquire() consumes a license semaphore.acquire(); System.out.println("thread A Pass obtained successfully"); TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { }finally { // Each call to release() returns a license semaphore.release(); } }); tA.start(); // Ensure that thread A has been executed TimeUnit.MILLISECONDS.sleep(200); Thread tB = new Thread(() ->{ try { // Calling acquire(2) will consume 2 licenses semaphore.acquire(2); System.out.println("thread B Pass obtained successfully"); } catch (InterruptedException e) { }finally { // Calling release(2) will return 2 licenses semaphore.release(2); } }); tB.start(); // Ensure that thread B has been executed TimeUnit.MILLISECONDS.sleep(200); Thread tC = new Thread(() ->{ try { // Each call to acquire() consumes a license semaphore.acquire(); System.out.println("thread C Pass obtained successfully"); } catch (InterruptedException e) { }finally { // Each call to release() returns a license semaphore.release(); } }); tC.start(); } }
The results are as follows:
thread A Pass obtained successfully thread B Pass obtained successfully thread C Pass obtained successfully
3. Source code analysis
3.1. Internal class
- Through several implementation methods of Sync, we get the following information:
- The number of licenses is passed in when constructing the method
- The license is stored in the state variable state
- When trying to acquire a license, the value of state is reduced by 1
- When the value of state is 0, the license can no longer be obtained
- When releasing a license, the value of state is increased by 1
- The number of licenses can be changed dynamically
abstract static class Sync extends AbstractQueuedSynchronizer { private static final long serialVersionUID = 1192457210091910933L; // Construction method, pass in the number of licenses, and put them into the state Sync(int permits) { setState(permits); } // Number of licenses obtained final int getPermits() { return getState(); } // Unfair mode attempts to obtain permission final int nonfairTryAcquireShared(int acquires) { for (;;) { // Let's see how many more permits there are int available = getState(); // How many licenses are left after subtracting the licenses that need to be obtained this time int remaining = available - acquires; // If the remaining licenses are less than 0, return directly // If the remaining licenses are not less than 0, try to update the value of state atomically, and return the remaining licenses successfully if (remaining < 0 || compareAndSetState(available, remaining)) return remaining; } } // Release license protected final boolean tryReleaseShared(int releases) { for (;;) { // Let's see how many more permits there are int current = getState(); // With permission for this release int next = current + releases; // Detect overflow if (next < current) // overflow throw new Error("Maximum permit count exceeded"); // If the atom updates the value of state successfully, it indicates that the license is released successfully, and returns true if (compareAndSetState(current, next)) return true; } } // Reduce license final void reducePermits(int reductions) { for (;;) { // Let's see how many more permits there are int current = getState(); // Minus licenses to be reduced int next = current - reductions; // Detect overflow if (next > current) // underflow throw new Error("Permit count underflow"); // Atom updates the value of state successfully and returns true if (compareAndSetState(current, next)) return; } } // Destruction permit final int drainPermits() { for (;;) { // Let's see how many more permits there are int current = getState(); // If 0, return directly // If it is not 0, update the state atom to 0 if (current == 0 || compareAndSetState(current, 0)) return current; } } }
3.2. Internal class NonfairSync
In unfair mode, directly call nonfairtryacquiresered() of the parent class to try to obtain the license
static final class NonfairSync extends Sync { private static final long serialVersionUID = -2694183684443567898L; // Constructor, calling the constructor of the parent class NonfairSync(int permits) { super(permits); } // Try to get permission and call the nonfairtryacquiresered() method of the parent class protected int tryAcquireShared(int acquires) { return nonfairTryAcquireShared(acquires); } }
3.3. Internal FairSync
In the fair mode, first check whether there is a queue in front. If there is a queue, the license acquisition fails and enters the queue. Otherwise, try to update the value of state atomically
Note: for the convenience of reading, some methods in AQS are pasted in this internal class, and the method header comments are marked!
static final class FairSync extends Sync { private static final long serialVersionUID = 2014338818796000944L; FairSync(int permits) { super(permits); } //It is located in AQS and can respond to interrupt to obtain the method of shared lock public final void acquireSharedInterruptibly(int arg) throws InterruptedException { // The condition is true: it indicates that the thread calling the acquire method of the current thread is in the interrupt state, and an exception is thrown directly if (Thread.interrupted()) throw new InterruptedException(); // Try to obtain the pass, and return the value of > = 0 if you succeed in obtaining it by reducing the value of state. If you fail to obtain it, return the value of < 0 if (tryAcquireShared(arg) < 0) //Add the thread that failed to obtain the pass to the blocking queue of AQS doAcquireSharedInterruptibly(arg); } //In AQS: share interrupt private void doAcquireSharedInterruptibly(int arg) throws InterruptedException { //Will call semaphore The thread of acquire () method is wrapped as node and added to the blocking queue of AQS final Node node = addWaiter(Node.SHARED); boolean failed = true; //Whether there is an exception try { for (;;) { //Gets the precursor node of the current thread node final Node p = node.predecessor(); //If the condition is true, it indicates that the node corresponding to the current thread is head Next node if (p == head) { //head. The next node has the right to obtain the shared lock int r = tryAcquireShared(arg); //Only when all the threads in the queue that obtain the shared lock are not released will it succeed if (r >= 0) { setHeadAndPropagate(node, r); p.next = null; // help GC failed = false; return; } } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) //Response interrupt throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); } } // When trying to obtain a pass (by reducing the value of state), it returns a value of > = 0 if it succeeds. If it fails, it returns a value of < 0 protected int tryAcquireShared(int acquires) { // Spin operation for (;;) { // Judge whether there is a waiting thread in the current AQS blocking queue. If there is a direct return of - 1, it indicates that the thread of the current acquire operation needs to enter the queue to wait if (hasQueuedPredecessors()) return -1; // So far, what are the situations? // 1. When acquire is called, there are no other waiters in the AQS blocking queue // 2. The current node in the blocking queue is head Next node (reentry lock) // Get the value of state, which represents the pass int available = getState(); // Remaining: indicates the number of semaphore s remaining after the current thread obtains the pass int remaining = available - acquires; // Condition 1 is true: remaining < 0: indicates that the thread failed to obtain the pass // Condition 2: precondition: remaining > = 0, which means that the current thread can obtain the pass // compareAndSetState(available, remaining): valid: indicates that the current thread obtains the pass successfully, and the CAS fails if (remaining < 0 || compareAndSetState(available, remaining)) return remaining; } } //In AQS, release the shared lock public final boolean releaseShared(int arg) { // If the condition is true, it indicates that the current thread has successfully released resources. After successfully releasing resources, wake up the thread that failed to obtain resources if (tryReleaseShared(arg)) { // Wake up the thread that failed to get resources doReleaseShared(); return true; } return false; } // Attempting to return the token, the current thread releases resources protected final boolean tryReleaseShared(int releases) { // spin for (;;) { // Get the state value of the current AQS int current = getState(); // Number of new passes obtained int next = current + releases; if (next < current) // overflow throw new Error("Maximum permit count exceeded"); // Number of CAS exchange passes if (compareAndSetState(current, next)) return true; } } //Which paths will call the doReleaseShared method? //1.latch.countDown() -> AQS.state==0 -> doReleaseShared() //Wake up the head in the current blocking queue Thread corresponding to next //2. Awakened thread - > doacquiresharedinterruptible parkandcheckinterrupt() wake up //-> setHeadAndPropagate() -> doReleaseShared() // Semaphore version // Wake up the thread that failed to get resources //Wake up waiting threads private void doReleaseShared() { for (;;) { //Get the head node of the queue Node h = head; //If the queue has been initialized successfully and the number of nodes in the queue is > 1 //Condition 1: H= Null holds, indicating that the blocking queue is not empty //Not true: h==null when will this be the case? //After the latch is created, no thread has called the await() method. Before that, a thread calls the latch Countdown() and triggers the logic of waking up the blocking node //Condition 2: H= The establishment of tail indicates that there are other nodes in the current blocking queue besides the head node //H = = tail - > head and tail point to the same node object. When will this happen? //Under normal wake-up conditions, the shared lock is obtained in turn. When the current thread executes here, this thread is the tail node //The first thread calling the await() method is concurrent with the thread calling countDown() and triggering the wake-up blocking node //Because the await() thread is the first to call latch At this time, there is nothing in the queue. It chooses to create a head //Before the await() thread is queued, it is assumed that there is only the newly created empty element head in the current queue //In the same period, there is an external thread calling countDown(). If the state value is changed from 1 to 0, this thread needs to wake up and block //Note: the thread that calls await() will enter the spin when it returns to doacquiresharedinterruptible after the queue is fully queued //Get the precursor of the current element and judge that you are head Next, so the thread will set itself to head again, and then the thread will return from the await method if (h != null && h != tail) { //Get the waiting state of the header node int ws = h.waitStatus; //If the waiting state of the head node is SIGNAL, it indicates that the successor node has not been awakened if (ws == Node.SIGNAL) { //Before waking up the successor node, change the state of the head node to 0 //Why use CAS here? Go back and say //When the doReleaseShared method, there are multiple threads waking up the head Next logic //CAS may fail //Case: //When if(h==head) returns false, t3 thread will continue to spin and participate in waking up the next head Logic of next //t3 at this time, CAS waiStatus(h,node.SIGNAL,0) is executed successfully t4 also enters if before t3 is modified successfully //However, t4 modifying CAS WaitStatus(h,Node.SIGNAL, 0) will fail because t3 has been changed if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; // loop to recheck cases //Wake up the successor node unparkSuccessor(h); } //This indicates that the waiting state of the current header node is not SIGNAL else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; // loop on failed CAS } //Conditions are satisfied: //It indicates that the successor node just woke up has not implemented the logic of setting the current wake-up node as head in the setHeadAndPropagate method //At this time, the current thread directly jumps out It's over.. //Don't worry at this time. Will the wake-up logic break here? //There is no need to worry, because the awakened thread will execute the doReleaseShared method sooner or later //After the h==null latch is created, no thread has called the await() method before //A thread calls latch Countdown() operation, and triggered the operation of waking up the blocking node //3. H = tail - > head and tail point to the same node object //Conditions not satisfied: //The awakened node is very active and directly sets itself as a new head. At this time, wake up its node (precursor) and execute h==head //At this time, the predecessor of the head node will not jump out of the doReleaseShared method, and will continue to wake up the successor of the new head node if (h == head) // loop if head changed break; } } }
3.3 construction method
The number of licenses that need to be passed in when creating semaphore. Semaphore is also a non fair mode by default, but you can call the second constructor to declare it a fair mode.
// Construction method. The number of licenses to be passed in during creation. The unfair mode is used by default public Semaphore(int permits) { sync = new NonfairSync(permits); } // Construction method, the number of incoming licenses required, and whether the model is fair public Semaphore(int permits, boolean fair) { sync = fair ? new FairSync(permits) : new NonfairSync(permits); }
3.4. acquire() method
The default way to get a license in the AQS queue is to use the interrupt method in the queue. If the attempt to get a license fails, it will be blocked
public void acquire() throws InterruptedException { sync.acquireSharedInterruptibly(1); } // Obtain a license in a non disruptive manner. If the attempt to obtain a license fails, it will enter the AQS queue. public void acquireUninterruptibly() { sync.acquireShared(1); }
3.5. acquire(int permits) method
Obtain multiple licenses at one time, which can be interrupted.
public void acquire(int permits) throws InterruptedException { if (permits < 0) throw new IllegalArgumentException(); sync.acquireSharedInterruptibly(permits); } // Obtain multiple licenses at one time in a non disruptive manner. public void acquireUninterruptibly(int permits) { if (permits < 0) throw new IllegalArgumentException(); sync.acquireShared(permits); }
Subsection:
- Semaphore, also known as semaphore, is usually used for the access of empty words to shared resources at the same time, that is, the current limiting scenario;
- The internal implementation of Semaphore is based on AQS shared lock
- During Semaphore initialization, you need to specify the number of licenses. The number of licenses is stored in state
- When obtaining a license, the value of state is reduced by one
- When releasing a license, state will wake up the queued threads in the AQS blocking queue