JUC core control AQS source code analysis part III (shared lock, CountDownLatch and CyclicBarrier)

Posted by khanfahd on Thu, 25 Nov 2021 19:26:53 +0100

Combine CountDownLatch and CyclicBarrier to understand the shared lock part of AQS

1. Use of CountDownLatch

Let's take a look at how CountDownLatch is used

public class CountDownLatchTest {
    public void CountDownLatchTest() throws InterruptedException {
        CountDownLatch doneSignal = new CountDownLatch(5);
        ExecutorService e = Executors.newFixedThreadPool(8);
        // Create N tasks and submit them to the thread pool for execution
        for (int i = 1; i <= 5; ++i) // create and start threads
            e.execute(new WorkerRunnable(doneSignal, i));

        // Wait for all tasks to be completed before this method returns. It is blocked here before it is completed
        doneSignal.await();           // wait for all to finish
        System.out.println("The tasks of all threads are finished");
        e.shutdown();
        System.out.println("Thread pool closed");
    }

    class WorkerRunnable implements Runnable {
        private final CountDownLatch doneSignal;
        private final int i;

        WorkerRunnable(CountDownLatch doneSignal, int i) {
            this.doneSignal = doneSignal;
            this.i = i;
        }

        public void run() {
            doWork(i);
            // When the task of this thread is completed, call the countDown method
            doneSignal.countDown();
        }

        void doWork(int i) {
            System.out.println("thread " + i + "Your task is done");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new CountDownLatchTest().CountDownLatchTest();
    }
}

Output results

The task of thread 1 is finished
 The task of thread 2 is finished
 The task of thread 3 is finished
 The tasks of all threads are finished
 The task of thread 4 is finished
 The task of thread 5 is finished
 Thread pool closed

From the output results, we can see that only five threads have completed the task await() the method will return, and then execute the following. It will be blocked before it is completed. It will not be blocked until five threads complete the task. The practical application is to divide a large task into several small tasks, and then let different threads execute concurrently. For example, divide a piece of data into multiple segments, process a part of each data, and return until the whole data is processed

2. CountDownLatch principle (shared lock)

After using CountDownLatch, you can start the principle of CountDownLatch. Let's see how CountDownLatch uses AQS shared locks

2.1 construction method

The constructor is used to initialize the value of state, which is equivalent to how many locks have been initialized. Negative numbers are not allowed

public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }

Sync(int count) {
            setState(count);
}

2.2 await () method

Suppose that thread 1 and thread 2 now call the await () method
The first call is the await () method of CountDownLatch

public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

Then call the acquireSharedInterruptibly () method of AQS.

//Because this method will handle interrupts, judge the interrupt status first
public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
         //state is not set up for 0 conditions and then calls doAcquireSharedInterruptibly(arg).
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }

Then call the tryAcquireShared () method of the static internal class Sync of CountDownLatch.

protected int tryAcquireShared(int acquires) {
         //1 is returned only when state is 0
            return (getState() == 0) ? 1 : -1;
        }

Tryacquisureshared returns - 1, so the doacquisuresharedinterruptible () method of AQS is called

private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        //Queue thread 1. Since the queue is empty before thread 1 is queued, thread 1 will create a head node, and then queue thread 1
        //Because it is a shared lock and no thread holds the lock alone, the exclusiveOwnerThread variable is not used
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            for (;;) {
            //Get precursor node
                final Node p = node.predecessor();
                //The precursor node is the head node
                if (p == head) {
                //You can try to get the lock
                    int r = tryAcquireShared(arg);
                    //Greater than or equal to 0 indicates success
                    if (r >= 0) {
                    //Set the current node as the new head node
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                //If the lock acquisition fails, suspend and set the ws of the precursor node to - 1
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    //Throw an exception when interrupted, and will not continue to try to grab the lock
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

If thread 1 and thread 2 fail to call the await () method to obtain the lock, they will be suspended first and wait to be awakened
The queue at this time is

2.3. countDown() method

Every time the countDown() method is called, the state will be reduced by 1, which is equivalent to unlocking a lock until the state is reduced to 0, which means that the lock has been unlocked
Thread 3 and thread 4 call the countDown() method of CountDownLatch respectively

public void countDown() {
        sync.releaseShared(1);
    }

Then call the releaseShared () method of AQS.

public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

Then call the tryReleaseShared() method of the CountDownLatch static internal class.

 protected boolean tryReleaseShared(int releases) {
            // Decrement count; signal when transition to zero
            for (;;) {
            //Get current status value
                int c = getState();
                //0 means no unlocking
                if (c == 0)
                    return false;
                int nextc = c-1;
                //cas subtracts state by 1
                if (compareAndSetState(c, nextc))
                //When the state is 0, it returns true
                    return nextc == 0;
            }
        }

If the tryrereleaseshared() method returns true, it means that all locks have been unlocked. You can call the doReleaseShared() method of AQS to wake up all blocked threads

//This method is called only when the value of state is 0 or interrupted
 private void doReleaseShared() {
        for (;;) {
            Node h = head;//Head node
            //The header node is not empty, and the queue has more than one node
            if (h != null && h != tail) {
                int ws = h.waitStatus; //Header node ws
                //If the value of the head node ws is SIGNAL-1, it means that the successor node of the head node will be awakened
                if (ws == Node.SIGNAL) {
                //Set the ws of the head node to 0 through cas. cas is used because multiple threads may modify ws
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                     //Wake up the successor node of the head node   
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            //Until the nodes behind the head are awakened, only the head node in the queue will not continue to wake up
            if (h == head)                   // loop if head changed
                break;
        }
    }

At this time, the successor node of the head node is awakened, that is, thread 1 is awakened

private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    //After being awakened, r is 1, that is, greater than or equal to, so setHeadAndPropagate(node, r) will be called
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                //After being awakened, the interrupt will be checked first, and then the cycle will continue
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        //Set thread 1 as the new head node
        setHead(node);
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            //It will continue to wake up the successor node of the new head node, thread 2, until all threads are awakened
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }

summary

1. CountDownLatch uses the AQS shared lock, which means that multiple threads can lock and unlock, while an exclusive lock can only be locked and unlocked by one thread
2. A thread calls the await () method. If the state is not 0, it will enter the synchronization queue and wait for wake-up to obtain the lock
3. After other threads call the countDown () method, if the state is 0, the nodes behind the head in the synchronization queue will wake up
4. The same thread cannot call the await () method and the countDown () method at the same time because it cannot wake itself up

Topics: Java