AbstractQueuedSynchronizer (AQS) source code detailed analysis - Semaphore source code analysis

Posted by mona02 on Sat, 19 Feb 2022 06:30:32 +0100

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

Topics: Java Semaphore