☕ [Java deep series] [concurrent programming series] let's explore the technical principle and source code analysis of CountDownLatch

Posted by henryblake1979 on Tue, 25 Jan 2022 18:02:13 +0100

Working principle analysis of CountDownLatch

1, General introduction

So this article shares and analyzes jdk1 8. Working principle of CountDownLatch;

Get to know CountDownLatch

What is CountDownLatch?

  1. CountDownLatch literally means that count does the subtraction of down, and Latch means door Latch;

  2. CountDownLatch is a synchronization help that allows one or more threads to wait until a set of operations performed in other threads is completed;

  3. There is no so-called static internal class of fair lock / non fair lock in CountDownLatch. There is only one static internal class of Sync. CountDownLatch basically uses Sync XXX and so on;

  4. CountDownLatch internally maintains a virtual resource pool. If the number of licenses is not 0, the thread blocks and waits until the number of licenses is 0, and then releases and continues to execute;

state keyword of CountDownLatch

  1. In fact, the implementation of CountDownLatch also makes good use of the state variable value of its parent class AQS;

  2. Initialize a quantity value as the default value of the counter. Assuming N, when any thread calls countDown once, the count value will be reduced by 1, and the wait will not be released until the permission is 0;

  3. CountDownLatch simply means: group A thread waits for another group B thread. After group B thread executes, group A thread can execute;

Common and important methods

Creates a count synchronizer object for a given allowable count value

public CountDownLatch(int count)

Queue up and wait until the counter value is 0, then release the wait

public void await()

Release the permission. The counter value decreases by 1. If the counter value is 0, it will trigger the release of useless nodes

public void countDown()

Get the latest counter value of shared resources

public long getCount()

Design and implementation of pseudo code

Get shared lock:

  • If the interrupt state is detected and found to have been interrupted, an InterruptedException exception is thrown
  • If the attempt to obtain the shared lock fails (the various ways of trying to obtain the shared lock are implemented by the subclass of AQS),
  • Then add a new shared lock node to the queue through spin operation, and then call locksupport Park enters the blocking wait and does not release the wait until the counter value is zero
    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

Release shared lock:

  • If the attempt to release the shared lock fails (the various ways to release the shared lock are implemented by the subclass of AQS),
  • Then the call operation of blocking thread is completed through spin operation
public void countDown() {
        sync.releaseShared(1);
}

Detailed understanding of CountDownLatch's life

For example, in the 100 meter race, I will take the race as an example to illustrate the CountDownLatch principle in life:

  • 1. Scene: Ten participants in the 100 meter race, with a referee counting at the end;

  • 2. With a start signal, ten people rushed to the finish line. It was really exciting for many seconds and exciting;

  • 3. When a person reaches the finish line, he will finish his race and play while he has nothing to do. Then the referee will subtract one person;

  • 4. As the personnel have reached the end point one after another, the final referee count shows that there are 0 people who have not arrived, which means that the personnel have reached the end point;

  • 5. Then the referee takes the registered results and inputs them into the computer for registration;

  • 6. Stop here. This series of actions is considered that group A thread waits for the operation of other groups of threads until the counter is zero, and then A will do other things;

Source code analysis CountDownLatch

CountDownLatch constructor

Constructor source code:

    /**
     * Constructs a {@code CountDownLatch} initialized with the given count.
     *
     * @param count the number of times {@link #countDown} must be invoked
     *        before threads can pass through {@link #await}
     * @throws IllegalArgumentException if {@code count} is negative
     */
    public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }

2. Create a count synchronizer object with a given allowable count value. The counter value must be greater than zero. The count value is finally assigned to the shared resource value state;

Sync synchronizer

AQS --> Sync

The synchronizers in CountDownLatch operate the calling relationship through the Sync abstract interface. A closer look shows that they are basically through Sync XXX and so on;

await()

Source code

    /**
     * Causes the current thread to wait until the latch has counted down to
     * zero, unless the thread is {@linkplain Thread#interrupt interrupted}.
     *
     * // Causes the current thread to wait until the counter value decreases to zero, and then releases the wait, or it can also release the wait because the thread is interrupted;
     *
     * <p>If the current count is zero then this method returns immediately.
     *
     * <p>If the current count is greater than zero then the current
     * thread becomes disabled for thread scheduling purposes and lies
     * dormant until one of two things happen:
     * <ul>
     * <li>The count reaches zero due to invocations of the
     * {@link #countDown} method; or
     * <li>Some other thread {@linkplain Thread#interrupt interrupts}
     * the current thread.
     * </ul>
     *
     * <p>If the current thread:
     * <ul>
     * <li>has its interrupted status set on entry to this method; or
     * <li>is {@linkplain Thread#interrupt interrupted} while waiting,
     * </ul>
     * then {@link InterruptedException} is thrown and the current thread's
     * interrupted status is cleared.
     *
     * @throws InterruptedException if the current thread is interrupted
     *         while waiting
     */
    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }
  • await: after this method is called, it will always be in a waiting state. Its core is the call of locksupport Park enters blocking waiting;
  • When the counter value state=0, the waiting status can be broken. Of course, the waiting status of threads can also be broken after threads are interrupted;

acquireSharedInterruptibly(int)

Source code:

    /**
     * Acquires in shared mode, aborting if interrupted.  Implemented
     * by first checking interrupt status, then invoking at least once
     * {@link #tryAcquireShared}, returning on success.  Otherwise the
     * thread is queued, possibly repeatedly blocking and unblocking,
     * invoking {@link #tryAcquireShared} until success or the thread
     * is interrupted.
     * @param arg the acquire argument.
     * This value is conveyed to {@link #tryAcquireShared} but is
     * otherwise uninterpreted and can represent anything
     * you like.
     * @throws InterruptedException if the current thread is interrupted
     */
    public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted()) // Before calling, first detect the thread interrupt flag bit to detect whether the thread has been interrupted before
            throw new InterruptedException(); // If it is interrupted, an interrupt exception is thrown
        if (tryAcquireShared(arg) < 0) // If you try to obtain a shared resource lock, it will fail if it is less than 0. This method is implemented by a specific subclass of AQS
            doAcquireSharedInterruptibly(arg); // Queue the thread trying to acquire the lock resource
    }

Since the function of synchronization counter is implemented, the first call of tryAcquireShared must be less than 0, and then the doacquiresharedinterruptible thread is entered smoothly; As for why the first call is less than 0, please see the implementation of the subclass. The implementation of the subclass is judged as "(getstate() = = 0)? 1: - 1";

3.5,tryAcquireShared(int)

Source code

	protected int tryAcquireShared(int acquires) {
		return (getState() == 0) ? 1 : -1; // The counter value is compared with zero. If it is less than zero, the lock acquisition fails, and if it is greater than zero, the lock acquisition succeeds
	}

Try to obtain the shared lock resource, but in the counter CountDownLatch function, if it is less than zero, you need to join the queue and enter the blocking queue to wait; If it is greater than zero, wake up the waiting queue and release the blocking waiting of await method;

doAcquireSharedInterruptibly(int)

Source code

    /**
     * Acquires in shared interruptible mode.
     * @param arg the acquire argument
     */
    private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
		// Create a new node according to the given mode. There are two modes: node Exclusive exclusive mode, node Shared sharing mode;
        final Node node = addWaiter(Node.SHARED); // Create a node for shared mode
        boolean failed = true;
        try {
            for (;;) { // Dead cycle operation mode of spin
                final Node p = node.predecessor(); // Get the precursor node of the node
                if (p == head) { // If the precursor node is head, it means that the current node is needless to say. The second only to the eldest is the second
                    int r = tryAcquireShared(arg); // And the second wants to try to get the lock. What if the head node just happens to be released? There is still hope. What if it comes true
                    if (r >= 0) { // If r > = 0, it indicates that the shared lock resource has been successfully obtained
                        setHeadAndPropagate(node, r); // Set the current node node as the head node, and call doReleaseShared to release the useless node
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
					// However, when the await method is called for the first time, it will flow here. At this time, the acquisition of lock resources will fail, that is, R < 0, so it will enter the judgment of whether to sleep or not
					// However, when entering the sleep method for the first time, because the created node waitStatus=0, it will be modified to the SIGNAL state once and cycle again
					// When entering the shouldParkAfterFailedAcquire method in the second cycle, returning true means that sleep is required, and the park method is called smoothly to block the wait
                }
                if (shouldParkAfterFailedAcquire(p, node) && // See if you need a break according to the precursor node
                    parkAndCheckInterrupt()) // Blocking operation. Under normal circumstances, the shared lock cannot be obtained, and the code stops in this method until it is awakened
					// After being woken up, if it is found that parkAndCheckInterrupt() detects an interrupt, it adds an interrupt exception, so an exception is thrown
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

When implementing the counter principle, the main thing to do is to wait and wait until the counter value is zero;

countDown()

Source code

    /**
     * Decrements the count of the latch, releasing all waiting threads if
     * the count reaches zero.
     *
     * <p>If the current count is greater than zero then it is decremented.
     * If the new count is zero then all waiting threads are re-enabled for
     * thread scheduling purposes.
     *
     * <p>If the current count equals zero then nothing happens.
     */
    public void countDown() {
        sync.releaseShared(1); // Release a licensed resource 
    }

Release the licensed resources, that is, the counter value is continuously reduced by 1. When the counter value is zero, this method will release all waiting thread queues; As for why all are released, please see the subsequent release shared (int ARG) explanation;

releaseShared(int)

Source code:

    /**
     * Releases in shared mode.  Implemented by unblocking one or more
     * threads if {@link #tryReleaseShared} returns true.
     *
     * @param arg the release argument.  This value is conveyed to
     *        {@link #tryReleaseShared} but is otherwise uninterpreted
     *        and can represent anything you like.
     * @return the value returned from {@link #tryReleaseShared}
     */
    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) { // Attempt to release the shared lock resource. This method is implemented by a specific subclass of AQS
            doReleaseShared(); // Spin operation, wake up subsequent nodes
            return true; // Returning true indicates that all threads have been released
        }
        return false; // Returning false indicates that it has not been released completely. As long as the counter value is not zero, false will be returned
    }
  1. The releaseShared method first judges the return value of tryrereleaseshared (ARG), but as long as the counter value is not zero, it will return false. Therefore, the releaseShared method immediately returns false;

  2. Therefore, when the counter value slowly decreases to zero, it will immediately return true, and then it will immediately call doReleaseShared to release all waiting thread queues;

tryReleaseShared(int)

Source code:

	// Static inner class of CountDownLatch tryrereleaseshared method of Sync class	
	protected boolean tryReleaseShared(int releases) {
		// Decrement count; signal when transition to zero
		for (;;) { // Dead cycle operation mode of spin
			int c = getState(); // Get the latest counter value
			if (c == 0) // If the counter value is zero, it indicates that it has been reduced to zero through CAS operation. Therefore, no operation is required when reading zero in concurrency, so false is returned
				return false;
			int nextc = c-1; // Counter value minus 1 operation
			if (compareAndSetState(c, nextc)) // Through CAS comparison, if the setting is successful, it returns true
				return nextc == 0; // When nextc obtained through calculation operation is zero, the CAS modification is successful, which indicates that everything has been completed and all waiting thread queues need to be released
			// If CAS fails, there is no need to think that it must be due to concurrent operation, so the only thing to do is to check whether it has been processed by other threads in the next cycle
		}
	}

The static inner class of CountDownLatch implements the AQS method of the parent class, which is used to handle how to release the lock. Generally speaking, if a negative number is returned, it needs to enter the blocking queue, otherwise it needs to release all waiting queues;

doReleaseShared()

The main purpose is to release all waiting queues in the thread. When the counter value is zero, this method will be called immediately, and all waiting queues will be killed by spin polling;

    /**
     * Release action for shared mode -- signals successor and ensures
     * propagation. (Note: For exclusive mode, release just amounts
     * to calling unparkSuccessor of head if it needs signal.)
     */
    private void doReleaseShared() {
        /*
         * Ensure that a release propagates, even if there are other
         * in-progress acquires/releases.  This proceeds in the usual
         * way of trying to unparkSuccessor of head if it needs
         * signal. But if it does not, status is set to PROPAGATE to
         * ensure that upon release, propagation continues.
         * Additionally, we must loop in case a new node is added
         * while we are doing this. Also, unlike other uses of
         * unparkSuccessor, we need to know if CAS to reset status
         * fails, if so rechecking.
         */
        for (;;) { // Dead cycle operation mode of spin
            Node h = head; // Each time, the head node of the queue is taken out
            if (h != null && h != tail) { // If the head node is not empty and is not the tail node
                int ws = h.waitStatus; // Then get the waitStatus status value of the header node
                if (ws == Node.SIGNAL) { // If the head node is in SIGNAL state, it means that the successor node of the head node needs to be awakened
					// Try to set the state of the header node to null through CAS. If it fails, continue the cycle, because the concurrency may be released elsewhere
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h); // Wake up the successor node of the head node
                }
				// If the header node is in the empty state, it will be changed to the PROPAGATE state. If it fails, it may have been changed due to concurrency, and it will be processed again
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
			// If there is no change in the header node, it indicates that the above settings have been completed, the success is achieved, and you will retire
			// If there is a change, it may be that the header node has been added or something during the operation, then it must be retried to ensure that the wake-up action can continue to pass
            if (h == head)                   // loop if head changed
                break;
        }
    }

summary

1,With analysis AQS After the foundation, we will analyze it again CountDownLatch Much faster;

2,Let me briefly summarize here CountDownLatch Some features of the process:
	• Manage a counter value greater than zero;
	• each countDown One time rule state Reduce 1 time until the number of licenses equals 0, and then release all waiting threads in the queue;
	• You can also pass countDown/await Combined to achieve CyclicBarrier The function of;

CountDownLatch usage

The CountDownLatch class provides only one constructor:

public CountDownLatch(int count) {  };  //The parameter count is the count value

Then, the following three methods are the most important methods in the CountDownLatch class:

public void await() throws InterruptedException { };   //The thread calling the await() method is suspended and waits until the count value is 0
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };  //Similar to await(), it will continue to execute if the count value has not changed to 0 after waiting for a certain time
public void countDown() { };  //Reduce the count value by 1

CountDownLatch, a synchronization helper class, allows one or more threads to wait until a set of operations are completed in other threads.

For example:

package main.java.CountDownLatch; 
import java.util.concurrent.CountDownLatch;

public class countDownlatchTest {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(5);
        for(int i=0;i<5;i++){
            new Thread(new readNum(i,countDownLatch)).start();
        }
        countDownLatch.await();
        System.out.println("Thread execution ends....");
    }
 
    static class readNum  implements Runnable{
        private int id;
        private CountDownLatch latch;
        public readNum(int id,CountDownLatch latch){
            this.id = id;
            this.latch = latch;
        }
        @Override
        public void run() {
            synchronized (this){
                System.out.println("id:"+id);
                latch.countDown();
                System.out.println("Thread group task"+id+"End, other tasks continue");
            }
        }
    }
}

Output result:

id:1
 Thread group task 1 ends and other tasks continue
id:0
 Thread group task 0 ends and other tasks continue
id:2
 Thread group task 2 ends and other tasks continue
id:3
 Thread group task 3 ends and other tasks continue
id:4
 Thread group task 4 ends and other tasks continue
 Thread execution ends....

Thread in countDown()After that, I will continue to perform my tasks

Topics: cas