The thread waits for other threads to execute the synchronization class CountDownLatch

foreword

Everyone knows that multithreading is used a lot in our actual coding process, and in many cases we need to rely on multithreading to improve system performance. But sometimes we need to block some threads and let these threads wait for the execution of other threads to complete and get the results. For example: data statistics, waiting for other tasks to complete, deadlock checking, etc. In order to deal with these scenarios, our JUC provides the CountDownLatch class, which can help us make one or more threads wait for other threads to complete in the use of multi-threading.

core principle

CountDownLatch internally maintains the counter and blocking queue. The counter caches the count value, and there are also multiple threads that need to be executed first; the blocking queue caches the thread that calls the await() method, which means joining the blocking queue. When CountDownLatch is initialized, it will pass in the count value of the number of threads that need to be executed first. Subsequent threads calling the await() method will join the blocking queue and wait for other threads to complete their execution. The countDown() method is to decrement the counter count value by one after a certain thread business is completed, indicating that this thread has been executed. If all the preceding threads are executed and the final count value is equal to 0, then CountDownLatch will wake up the threads in the blocking queue to continue execution.

Source code analysis

Synchronous source code analysis

CountDownLatch dependency graph
insert image description here

Enter the CountDownLatch class under JUC, and find that its internal class Sync integrates AbstractQueuedSynchronizer abstract blocking queue:

//内部类sync集成AQS
private static final class Sync extends AbstractQueuedSynchronizer {
    
    
    private static final long serialVersionUID = 4982264981922014374L;

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

    int getCount() {
    
    
        return getState();
    }

    protected int tryAcquireShared(int acquires) {
    
    
        return (getState() == 0) ? 1 : -1;
    }

    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;
        }
    }
}

private final Sync sync;

Continue to view the source code:

//有参数的构造方法
public CountDownLatch(int count) {
    
    
    if (count < 0) throw new IllegalArgumentException("count < 0");
    this.sync = new Sync(count);
}


//线程加入阻塞队列方法
public void await() throws InterruptedException {
    
    
    sync.acquireSharedInterruptibly(1);
}


//线程执行完成计数器减少方法
public void countDown() {
    
    
    sync.releaseShared(1);
}

As shown in the source code above, we found that the main methods await(), countDown(), and construction methods of CountDownLatch all call methods in the Syn internal class. However, the Sync internal class inherits AQS, so essentially our CountDownLatch is synchronized through AQS.

The internal principle of AQS is realized through the state state and the blocking queue. When state == 0, the thread in the blocking queue is awakened. Students who are not familiar with AQS can refer to my blog: Concurrency Basics: Abstract Synchronization Queue AQS

await source code analysis

Above we analyzed that CountDownLatch synchronization is achieved through AQS, now we analyze in detail the core method await() method of CountDownLatch.

Query the source code of the await() method, and find that CountDownLatch provides two methods for letting threads join the blocking queue, one directly interrupts and joins the blocking, and the other joins the blocking queue with a blocking timeout.

/**
 * 当前线程中断执行,加入阻塞队列
ublic void await() throws InterruptedException {
    
    
    sync.acquireSharedInterruptibly(1);
}

/**
 * 当前多线程中断加入阻塞队列,加入了阻塞超时时间
 */
public boolean await(long timeout, TimeUnit unit)
    throws InterruptedException {
    
    
    return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

Continue to view the source code:

//AQS执行中断加入阻塞前验证
public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    
    
    if (Thread.interrupted())
        throw new InterruptedException();
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
}
//AQS执行中断加入阻塞队列
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);
    }
}

As shown in the above source code, the essence of the await() method of CountDownLatch is to call the blocking method in AQS. Internally verify whether there is a thread in the blocking queue, if not, it will be used as the head of the blocking queue, if there is a blocking thread, it will be added to the tail of the blocking queue.

countDown source code analysis

The above analyzes that the await() method calls the AQS blocking logic. Not surprisingly, the countDown() method should also be the method of calling AQS to handle the release of the thread lock.

We query the source code of countDown:

//countDownLatch释放共享锁
public void countDown() {
    
    
    sync.releaseShared(1);
}
//AQS释放共享锁验证
public final boolean releaseShared(int arg) {
    
    
    if (tryReleaseShared(arg)) {
    
    
        //释放共享锁验证通过
        doReleaseShared();
        return true;
    }
    return false;
}

//countdownlatch内部类sync 覆写tryReleaseShared 方法
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))
            //只有state == 0 时候会释放共享锁
            return nextc == 0;
    }
}

As shown in the above source code, the countDown() method internally calls releaseShared() of AQS to release the shared lock method. Before releasing the lock, it will verify whether the release condition is met. At this time, the tryReleaseShared() method overridden by our sync is called. The logic of this method is to obtain the current state value and use CAS to modify the state minus one. Then judge whether the state is equal to 0, if it is not equal to 0, it will not pass the verification of releasing the shared lock, and if it is equal to 0, it will pass the verification.

Let's continue to check the source of releasing the shared lock:

//释放共享锁具体逻辑
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 (;;) {
    
    
        Node h = head;
        if (h != null && h != tail) {
    
    
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
    
    
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                //唤醒后续节点
                unparkSuccessor(h);
            }
            //用CAS重新设置state值
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}
/**
 * 唤醒后续阻塞线程方法
 */
private void unparkSuccessor(Node node) {
    
    
    /*
     * If status is negative (i.e., possibly needing signal) try
     * to clear in anticipation of signalling.  It is OK if this
     * fails or if status is changed by waiting thread.
     */
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    /*
     * Thread to unpark is held in successor, which is normally
     * just the next node.  But if cancelled or apparently null,
     * traverse backwards from tail to find the actual
     * non-cancelled successor.
     */
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
    
    
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        //唤醒线程
        LockSupport.unpark(s.thread);
}

As shown in the above source code, the doReleaseShared() method releases the shared lock, and after the release is completed, it is found that there is a blocked thread in the blocking queue and the thread will be woken up.

Combat demonstration

Above we talked about the synchronization method of CountCownLatch and the source code analysis of the main method. I believe that everyone has a certain understanding of CountDownLatch. Next, we let the main thread wait for the execution of 5 threads to complete before performing the actual combat demonstration.

1. Create demo code

/**
 * CountDownLatch运用
 * @author senfel
 * @version 1.0
 * @date 2023/4/20 14:58
 */
public class CountDownDemo {
    
    

    public static void actionCountDown() throws Exception{
    
    
        //创建一个定长线程池
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        //主线程等待5个线程执行完才执行
        CountDownLatch countDownLatch = new CountDownLatch(5);
        //并发量控制在2个线程
        Semaphore semaphore = new Semaphore(2);
        //创建线程执行数据
        for(int i=0;i<5;i++){
    
    
            executorService.execute(new Runnable() {
    
    
                @Override
                public void run() {
    
    
                    try {
    
    
                        semaphore.acquire();
                        System.err.println("线程:"+Thread.currentThread().getName()+"开始执行");
                        Thread.sleep(5000);
                        System.err.println("线程:"+Thread.currentThread().getName()+"执行完成");
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                    countDownLatch.countDown();
                    semaphore.release();
                }
            });
        }
        countDownLatch.await();
        System.err.println("主线程执行");
    }
}

2. Create test cases

@SpringBootTest
class DemoApplicationTests {
    
    

    /**
     * countDownLatchTest
     * @throws Exception
     */
    @Test
    public void countDownLatchTest() throws Exception{
    
    
        CountDownDemo.actionCountDown();
    }
}

3. Demonstration of test results

Thread: pool-1-thread-1 starts execution
Thread: pool-1-thread-4 starts execution
Thread: pool-1-thread-1 execution completes
Thread: pool-1-thread-4 execution completes
Thread: pool-1- Thread-3 starts execution
Thread: pool-1-thread-2 starts execution
Thread: pool-1-thread-3 execution completes
Thread: pool-1-thread-2 execution completes
Thread: pool-1-thread-5 starts execution
thread : The execution of pool-1-thread-5 is completed and
the main thread is executed

Explanation:
countDownLatch ensures that the main thread is executed last, and other sub-threads use semaphore to ensure the number of concurrency.

write at the end

In this article, we described that CountDownLatch realizes the synchronization function through the AQS abstract blocking queue, and that the essence of the awat() method is to call AQS to join the blocking queue, and the essence of countDown is to call AQS to release the shared lock. For the most important logic of waking up blocked threads, the condition for releasing shared locks is also satisfied by overriding the tryReleaseShared() method of CountDownLatch. In AQS releasing shared locks, if there are blocked threads in the blocking queue, the threads will be woken up.

Guess you like

Origin blog.csdn.net/weixin_39970883/article/details/130268100