CountDownLatch source code analysis and usage examples

If you don't understand AQS and may not be able to understand the content of this chapter, it is recommended to check another blogger's article about AQS: AQS source code analysis

note

A synchronization aid that allows one or more threads to wait for a set of operations to complete in other threads.

The initial value of a counter is specified when CountDownLatch is initialized. Since the countDown method is called, the await method will be blocked until the current counter value reaches zero, and then all waiting threads will be released, and the next call to the await method will return immediately. This is a one-shot (one-time) phenomenon - the counter cannot be reset. If you need a version that resets the counter, consider CyclicBarrier.

CountDownLatch is a versatile synchronization tool that can be used for many purposes. A CountDownLatch with an initial counter of 1 can be used as a simple switch gate or gate: all threads that call the await method will wait at the gate until the gate is opened by a thread that calls the countDown method. A CountDownLatch with an initial count of N can use one thread to wait until N threads complete an operation, or an operation is completed N times.

A thread can continue executing immediately after calling the countDown method without waiting for the counter to reach zero. However, any thread will be blocked before calling the await method, until all threads call the countDown method, the counter is reduced to zero, and execution can continue. This ensures that no thread can proceed to the next step until all threads have completed their work, thereby achieving synchronization between threads.

Use case one

Here's a pair of classes that use CountDownLatch, where a set of worker threads use two CountDownLatch:

The first is a start signal, which prevents any worker threads from continuing execution until the driver is ready for them;
the second is a completion signal, which allows the driver to wait until all worker threads have completed.

class Driver {
    
     // ...
	void main() throws InterruptedException {
    
    
		CountDownLatch startSignal = new CountDownLatch(1);
		CountDownLatch doneSignal = new CountDownLatch(N);
		for (int i = 0; i < N; ++i) // create and start threads
			new Thread(new Worker(startSignal, doneSignal)).start();
		doSomethingElse();            // don't let run yet 
		startSignal.countDown();      // let all threads proceed      
		doSomethingElse();
		doneSignal.await();           // wait for all to finish
	}
}

class Worker implements Runnable {
    
    
	private final CountDownLatch startSignal;
	private final CountDownLatch doneSignal;

	Worker(CountDownLatch startSignal, CountDownLatch doneSignal) {
    
    
		this.startSignal = startSignal;
		this.doneSignal = doneSignal;
	}

	public void run() {
    
    
		try {
    
    
			startSignal.await();
			doWork();
			doneSignal.countDown();
		} catch (InterruptedException ex) {
    
    
		} // return;    
	}

	void doWork() {
    
     ...}
}
Use case two

Another common use is to divide a problem into N parts, describe each part with a Runnable and decrement the counter, and then queue all the Runnables to the Executor. When all subsections are complete, the coordinating thread will go through the await method. (CyclicBarrier should be used when a thread has to repeatedly decrement the counter like this)

class Driver2 {
    
     // ...
	void main() throws InterruptedException {
    
    
		CountDownLatch doneSignal = new CountDownLatch(N);
		Executor e = ...
		for (int i = 0; i < N; ++i) // create and start threads
			e.execute(new WorkerRunnable(doneSignal, i));
		doneSignal.await();           // wait for all to finish
	}
}

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() {
    
    
		try {
    
    
			doWork(i);
			doneSignal.countDown();
		} catch (InterruptedException ex) {
    
    
		} // return;
	}

	void doWork() {
    
     ...}
}

Memory consistency effects: Until the count reaches zero, actions in a thread prior to calling countDown() happen-before actions following a successful return from a corresponding await() in another thread.

In CountDownLatch, until the value of the counter is reduced to zero, the operation before calling the countDown method in one thread happens-before the operation after the corresponding await method returns successfully in another thread.

how do I say this? This sentence describes the impact of CountDownLatch on memory consistency. This means that all operations performed before the thread called the countDown method will be seen after .

Constructor

The constructor requires us to pass a counter value, the number of times countDown must be called before the thread can pass the await method. The constructor is still quite simple, so I won't describe it too much.

 public CountDownLatch(int count) {
    
    
        // 参数合法校验
        if (count < 0) throw new IllegalArgumentException("count < 0");
     	// 内部类Sync,继承AQS,设置AQS的state值为count
        this.sync = new Sync(count);
    }

 private static final class Sync extends AbstractQueuedSynchronizer {
    
    
        private static final long serialVersionUID = 4982264981922014374L;
		
        Sync(int count) {
    
    
            setState(count);
        }
  //......   
 }

await method

According to the comment section, we can know that the function of await is to develop or gate, and open the gate when the value of count is zero.

// #CountDownLatch类
// 当前方法可能会有中断异常
public void await() throws InterruptedException {
    
    
    	// 进入到aqs类的方法
        sync.acquireSharedInterruptibly(1);
    }

This method is in the AbstractQueuedSynchronizer class, and will be uniformly described as the AQS class later.

// #AQS类中
public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
    
    
    	// 检测线程是否中断过
        if (Thread.interrupted())
            throw new InterruptedException();
    	// 两个核心方法出现
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }

First try to obtain the state value in AQS. If the state is 0, it means that all current threads are completed and the await thread needs to be woken up. In this case, the thread should complete the work quickly, and there is no need for await at present, and the main thread will directly do the follow-up work.

// #CountDownLatch类中实现
// int acquires参数没有使用
protected int tryAcquireShared(int acquires) {
    
    
     return (getState() == 0) ? 1 : -1;
        }

If the state value in the obtained AQS is not 0, then the current thread needs to be "stopped" and wait for the child thread to complete the work. Let's briefly look at the source code of the doAcquireSharedInterruptibly method first, and we may have a concept and then slowly deduct the details.

// #AQS类中
// 根据上述代码可知arg==1
private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
    
    
    // 可以现理解为创建一个SHARED标志的节点入队,下方再进行源码分析
    final Node node = addWaiter(Node.SHARED);
    try {
    
    
        // for循环和cas 老套路就是为了不使用锁来保证线程安全
        // 除此之外,for循环会在LockSupport.unpark后继续
        for (;;) {
    
    
            // 获取当前node节点的前序节点
            final Node p = node.predecessor();
            // 如果前需节点是head节点,进入判断条件,同时也说明会从队列从head-tail去唤醒
            if (p == head) {
    
    
                // 获取state的状态值,如果state等于0返回1否则返回-1
                int r = tryAcquireShared(arg);
                // 说明当前state的值为0,也就是不需要线程等待了
                if (r >= 0) {
    
    
                    // 设置当前node节点为头节点
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    return;
                }
            }
            // 执行LockSupport.park,让线程休眠等待被唤醒,也就是state为零值的时候会被唤醒
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } catch (Throwable t) {
    
    
        // 异常就取消当前node节点,并抛出异常
        cancelAcquire(node);
        throw t;
    }
}

Detail 1: addWaiter method

The incoming parameter is Node.SHARED, and the inner class in AQS defines SHARED and EXCLUSIVE , one for sharing and one for exclusive.

private Node addWaiter(Node mode) {
    
    
    // 设置了nextWaiter为Node.SHARED
    Node node = new Node(mode);
    // 老规矩,无参数的for循环和cas保证线程安全
    for (;;) {
    
    
        // 获取到当前的队尾节点,标记为旧的队尾节点
        Node oldTail = tail;
        //如果当前队尾为null,说明队列没有初始化,直接跳过if进入else进行同步队列的初始化
        if (oldTail != null) {
    
    
            // 队尾不为null的情况,就进入if逻辑执行插入队尾操作
            // 让当前node节点的PREV指向oldTail,这一步是建立节点间的关联
            node.setPrevRelaxed(oldTail);
            // 更新tail节点,如果不成功说明有竞争就再来一次循环
            if (compareAndSetTail(oldTail, node)) {
    
    
                // 绑定
                oldTail.next = node;
                return node;
            }
        } else {
    
    
            // 初始化一个队列,初始化之后head和tail都会为同一个new Node()
            initializeSyncQueue();
        }
    }
}

Detail 2: setHeadAndPropagate method

int r = r in tryAcquireShared(arg) is greater than or equal to 0 to enter this method. Obviously, this method will only return 1 and -1. If the state value in aqs is 0, it will return 1, otherwise it will return -1. That is, only when the state value (the value of count) in aqs is zero, it will enter this method.

In the current step, the execution of the await method does not enter the waiting period, because the value of count is already 0 at this time, so special processing will be performed inside this method.

private void setHeadAndPropagate(Node node, int propagate) {
    
    
        Node h = head; // Record old head for check below
        setHead(node);
        /*
         * Try to signal next queued node if:
         *   Propagation was indicated by caller,
         *     or was recorded (as h.waitStatus either before
         *     or after setHead) by a previous operation
         *     (note: this uses sign-check of waitStatus because
         *      PROPAGATE status may transition to SIGNAL.)
         * and
         *   The next node is waiting in shared mode,
         *     or we don't know, because it appears null
         *
         * The conservatism in both of these checks may cause
         * unnecessary wake-ups, but only when there are multiple
         * racing acquires/releases, so most need signals now or soon
         * anyway.
         */
    	//CountDownLatch并不关心节点的状态值,只关心count的值是不是被至为零值。
    	// 所以根本没有设置过waitStatus
    	// 进入这个方法这个if判断必定是true,因为propagate为1,count为0
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
    
    
            Node s = node.next;
            if (s == null || s.isShared())
                // 唤醒
                doReleaseShared();
        }
    }

Detail three: shouldParkAfterFailedAcquire

In fact, in the above method, we have never seen that waitStatus has been assigned a value, that is, the default value is 0, and this step is to set the value to SIGNAL.

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    
    
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) {
    
    
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
    
    
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
    
    
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
        }
        return false;
    }

Detail four: doReleaseShared

This method is to wake up the await thread.

// #AQS
// 共享模式下的释放操作--向后继节点发出信号并确保传播
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 (!h.compareAndSetWaitStatus(Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !h.compareAndSetWaitStatus(0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

countDown method

If you read the comments carefully, we have actually seen that the countDown method is to reduce the count value passed in by the constructor to zero. However, until these are not enough, we need to know how the bottom layer is implemented.

// #CountDownLatch类
public void countDown() {
    
    
    // 就一句代码,此处调用的是Sync父类AQS中的releaseShared方法
    sync.releaseShared(1);
}
// #AQS中

//共享模式下的释放,通常用于实现共享锁机制。如果tryReleaseShared方法返回true,
// 就会通过解除一个或多个线程的阻塞状态来实现共享模式下的释放。
// 这样,其他线程就可以获取对共享资源的访问权并进入共享模式
public final boolean releaseShared(int arg) {
    
    
    if (tryReleaseShared(arg)) {
    
    
        doReleaseShared();
        return true;
    }
    return false;
}

Through the releaseShared method, we know that there are two core methods, one is tryReleaseShared and the other is doReleaseShared. From the name, we can know that one of these two methods is to try to release, and the other is to release.

// #CountDownLatch类
// 这个参数 int release根本没使用
protected boolean tryReleaseShared(int releases) {
    
    
    // Decrement count; signal when transition to zero
    for (;;) {
    
    
        // 获取aqs中的state的值
        int c = getState();
        // 如果c==0,直接返回false
        if (c == 0)
            return false;
        int nextc = c - 1;
        // 只是使用了for循环和cas来保证线程安全,没有添加任何锁
        if (compareAndSetState(c, nextc))
            // 如果nextc不为0 返回false
            return nextc == 0;
    }
}

When the tryReleaseShared method returns true, that is, the state value in aqs (the count value is reduced to zero) is zero, the doReleaseShared method is executed. This method is to release the parked thread. Consistent with detail four above.

// #AQS
// 共享模式下的释放操作--向后继节点发出信号并确保传播
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 (!h.compareAndSetWaitStatus(Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !h.compareAndSetWaitStatus(0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

Summarize

As for why the await method and the countDown method both need the doReleaseShared method, let me talk about my understanding after reading the source code.

The await method needs to use the doReleaseShared method to suspend the current thread and wake up when the CountDownLatch counter is reduced to 0. When a thread calls the await method, it will try to acquire the CountDownLatch lock, if the value of the counter is not 0, the thread will be suspended. When the value of the counter becomes 0, wake up all waiting threads, which is achieved by calling the doReleaseShared method.

Similarly, the countDown method also needs to use the doReleaseShared method to decrement the value of the counter by 1, and wake up all waiting threads when the value of the counter becomes 0. When a thread calls the countDown method, it will decrement the value of the counter by 1, and if the value of the counter becomes 0, then call the doReleaseShared method to wake up all waiting threads.

Guess you like

Origin blog.csdn.net/Tanganling/article/details/131411865