Analysis of the underlying principle of the starting gun CountDownLatch (you must understand the series after reading it)

    CountDownLatch is a relatively well-known concurrent tool under the JUC package, which is also known as the starting gun. Similar to Lock, it uses the AQS queue synchronizer to complete the function. The following is a relatively simple example:

CountDownLatch latch=new CountDownLatch(2);
        Work worker1=new Work("程序员1", 5000, latch);
        Work worker2=new Work("程序员2", 8000, latch);
        worker1.start();//
        worker2.start();//
        latch.await();//
        System.out.println("Programmer can get off work, off-duty time: "+sdf.format(new Date()));

Work is a thread defined as follows:

class Work extends Thread{
        String Name;
        int workTime;
        CountDownLatch latch;
        public Work(String Name ,int workTime ,CountDownLatch latch){
            this.Name=Name;
            this.workTime=workTime;
            this.latch=latch;
        }
        public void run(){
            System.out.println(Name+" started working, now the time is "+sdf.format(new Date()));
            doSomething();
            System.out.println(Name+" work done, now the time is "+sdf.format(new Date()));
            latch.countDown();

        }
        private void doSomething(){
            try {
                Thread.sleep(workTime);
            } catch (InterruptedException e) {
                e.printStackTrace ();
            }
        }
    }

The output is as follows:

Programmer 1 started work, now the time is 2018-05-03 16:18:45
Programmer 2 started working, now the time is 2018-05-03 16:18:45
Programmer 1 completed the work, the time is now 2018-05-03 16:18:50
Programmer 2 has completed the work, the time is now 2018-05-03 16:18:53
Programmers can get off work, off work time: 2018-05-03 16:18:53

From the results, it can be seen that the two programmers start working together (the time may not be exactly the same), and the latch.await() passes only after the work of both is completed. From this, it can be seen that the role of the starting gun is to make all threads wait for all other threads to release until all threads have completed their work, a bit like when the test for grades 4 and 6, the security guard blocked everyone until the examiner collected all the papers before releasing us. Going out Smileis a bit like running in a race, and you have to wait until all the competitors are in place before firing the gun (the origin of the starting gun). Therefore, the starting gun should be more suitable for the work of the topology.

        As can be seen from the above simple example, the main function implementation of the starting gun is realized by latch.countDown() and latch.await(). The next step is to trace the source code.

        At the beginning of the program, there is a new process, which is the initial value of the state that constructs the synchronizer

this.sync = new Sync(count);

The countDown method does the following:

public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) { //Try to use CAS to do the state-- operation, the specific implementation code is in the inner class Sync of the CountDwonLatch class
            doReleaseShared();//Only the thread that completes the task last executes
            return true;
        }
        return false;
    }

When state is not equal to 1, other threads will return false in the following method until state==1&&nextc==0:

protected boolean tryReleaseShared(int releases) {
            // Decrement count; signal when transition to zero
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c-1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }

Let's focus on what doReleaseShared() does here (the last thread executes once ):

for (;;) { // spin operation
            Node h = head; //h points to the head node of the synchronizer
            if (h != null && h != tail) { //The queue synchronizer is not empty and there are at least two non-empty nodes
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))//SIGNAL state indicates that the current node is waiting to be woken up
                        continue;            // loop to recheck cases
                    unparkSuccessor(h); //Give access to threads on subsequent nodes of this node, which is equivalent to waking up the main thread
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))//, set the wait state of h to PROPAGATE
                    continue;                // loop on failed CAS
            }
            if (h == head)                   
                break;
        }

It should be noted that the head and tail nodes are auxiliary nodes and do not contain any thread information. For CountDownLatch, in the end, it only needs to release the main thread for execution to complete the function, and the thread executing the task does not enter the queue. Here is an explanation of the waiting state. The waiting state can be said to be the actual execution unit of the management strategy in the queue synchronizer. It contains 5 state values, representing different functions:

    1. Initialization state, value=0       

    2. SIGNAL value=-1, the actual meaning is that if the current node releases the synchronization state, it will wake up subsequent nodes

    3. PROPAGATE value=-3 The actual meaning is that the subsequent nodes will always obtain the synchronization state in a shared manner

    4. CONDITION value=-2 This is a special state. Its appearance means that it has left the synchronization queue and entered the waiting queue of the Condition object. Usually, the await method is executed at the node of the synchronization queue.

    5.CANCELED value=-1 The node in the synchronization queue encounters the state of waiting for timeout or being interrupted. As long as this state is entered, it is equivalent to the thread on the node dying.

To sum up, all nodes modify the state in a shared state.

Next, let's take a look at the await method. I use the above small example and use jconsole to track its stack information as follows:



It can be clearly seen from the figure that the specific location of the await method blocking is parkAndCheckInterrupt(), returning to the source code of await:

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);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt ())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

At first, I thought about how to design a similar function for me. My idea is to put all threads into the waiting queue of Condition, and then wake them up uniformly, but the wake-up mechanism must be the last one to complete. There is no way to determine which thread is the last completed thread during the running process, so this concurrent tool actually determines the last completed thread and then ends other threads.

Intuitively, I really can't understand the process inside. I'm going to use the debug method to track step by step how the last await causes the main thread to exit directly.


The first is that await first executes the addWaiter method:

Before execution, the status is as follows:


At this time, the synchronization queue is empty, and according to the definition of this method, it will enter the enq (node) method:


    According to the previous state, it can be known that tail is originally null, so the first execution of this method will make head and tail point to the same node and not null; then the incoming node will be added to the original tail for the second time to become a new one The tail then returns. At this time, there are two nodes in the synchronization queue, one is head and the other is node. At this time, node contains the main thread main.

    Then go back to doAcquireSharedInterruptibly(), because p==head is always true, so it will always check whether state==0 is true. If it is true, in this example, doReleaseShared() is executed again, which should be considered here. Execution order, because the countDown method will also execute this method, and this method is used to unlock the main thread. If state==0, await first detects this state and can end early.

    There is also a function shouldParkAfterFailedAcquire(p, node), because p is always the predecessor node head of node, and node is always tail, so after executing the following sentence, it returns false directly:

compareAndSetWaitStatus(pred, ws, Node.SIGNAL);  
If the above function is executed again during the spin, an exception will be thrown, but the exception will be handled, which ensures that the wait state of the head is always SIGNAL.

The above functions can also be implemented with Thread.join, but it will reduce the efficiency of CPU usage.

Application scenario: ranking calculation in competition, or parsing process of big data text





        

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325763243&siteId=291194637