头图
This article is subtitled From CountDownLatch to AQS

foreword

I use CountDownLatch daily in many scenarios. The scheduled tasks use multiple threads to collect data, and then do further processing after collecting the data. Usually I write the following in the program:

 public class CountDownLatchDemo {

    // 避免指令重排序
    private volatile  ThreadPoolExecutor threadPoolExecutor;


    public void countDownLatchDemo(){
        // 一共五十个任务
        CountDownLatch countDownLatch = new CountDownLatch(50);
        for (int i = 0; i < 50; i++) {
            getThreadPoolExecutor().execute(()->{
                // 线程做完对应的任务,任务数减一
                countDownLatch.countDown();
            });
        }
        try {
            // main 线程走到这里,如果50个任务没完成,就会被阻塞在这这里。
            // hello world 不会输出
            countDownLatch.await();
            System.out.println("hello world");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
            // 接着做对应的业务处理
    }

    private ThreadPoolExecutor getThreadPoolExecutor() {
        // 获取当前系统的逻辑核心数,我的是八核十六线程。
        int cpuNumbers = Runtime.getRuntime().availableProcessors();
        if (Objects.isNull(threadPoolExecutor)) {
            synchronized (CountDownLatchDemo.class) {
                // 为线程池起名,便于定位问题
                // CustomizableThreadFactory 来自于Spring
                ThreadFactory threadFactory = new CustomizableThreadFactory("fetch-data-pool-");
                threadPoolExecutor = new ThreadPoolExecutor(cpuNumbers, cpuNumbers, 1L, TimeUnit.HOURS, new LinkedBlockingDeque<>(), threadFactory);
                return threadPoolExecutor;
            }
        }
        return threadPoolExecutor;
    }

    public static void main(String[] args) {
        CountDownLatchDemo countDownLatchDemo = new CountDownLatchDemo();
        countDownLatchDemo.countDownLatchDemo();
    }
}

So I can't help but wonder how CountDownLatch is implemented, which is the origin of this article. My main concerns are mainly two:

  • How to wake up the thread calling the await method after the task is completed.
  • Exactly how to control the completion of the task, that is, what the countDown method does

The source code of this article is based on JDK8 (the higher version of JDK has some adjustments to the AbstractQueuedSynchronizer, and a source code related article will be published for the AbstractQueuedSynchronizer of JDK 17 later, focusing on comparing the optimization of the higher version of JDK). We first open CountDownLatch to view:

CountDownLatch一览

We see that when the constructor of CountDownLatch is called to initialize, it actually initializes the static inner class Sync of CountDownLatch indirectly. We call the countDown method of CountDownLatch to indirectly call the releaseShared() method of Sync. These two methods are not in Sync, which means they are inherited from AbstractQueuedSynchronizer. The author of this class is still Doug Lea, and there are many comments. I still like the source code with many comments. Before looking at the source code, I first looked at the comments above, and then looked at the basic structure of the class. This time we adjust the order, first take a general look at the basic structure of this class and then look at the comments above the class.

Open AbstractQueuedSynchronizer and scroll down. The first data structure we see at a glance is probably the Node class. Node is a node of a doubly linked list. It seems that the main data of each node is a thread.

JDK8中的AbstractQueuedSynchronizer

Node队列

The rest seem to operate around this linked list. In this section, the problem we are concerned about is the implementation principle of CountDownLatch, and then the AbstractQueuedSynchronizer. We focus on the implementation of countDown and await in CountDownLatch.

The countDown method is called as follows:

 // 这个sync事实上是CountDownLatch中静态内部类Sync的实例
public void countDown() {
      //releaseShared调用的是父类AbstractQueuedSynchronizer的releaseShared方法
   sync.releaseShared(1);
 }
// tryReleaseShared为CountDownLatch中静态内部类Sync重写父类的方法
public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
 }
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
            // 减少count
            for (;;) {
                // 我们看getState和setState方法,getState和setState是父类的方法
                // 获取state
                int c = getState();
                  // 如果等于0 返回false
                if (c == 0)
                    return false;
                // 否则就将state 变量 减一
                int nextc = c - 1;     
                // compareAndSetState调用的是AbstractQueuedSynchronizer的方法
                // 默认调用的是CAS,给AQS的state变量设置值。
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }
 
//下面是AbstractQueuedSynchronizer的compareAndSetState
// 这里的stateOffset用可以理解为字段state的偏移地址,通过this+stateOffset可以定位到state变量的内存地址
 protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
 }

What exactly does the countDown method do?

CountDownLatch初始化

When we use CountDownLatch, we first call its constructor for initialization, which will call the static inner class Sync of CountDownLatch for initialization, and the number of tasks passed in is finally given to the setState method inherited from AbstractQueuedSynchronizerd.

When the thread calls the countDown method to complete the task, the call chain is as follows:

CountDown调用链

Note that although tryReleaseShared comes from AbstractQueued, this method is an empty method, which is left to subclasses to override, so we need to go back to the Sync of CountDownLatch.

aqs-sync

tryReleaseShared represents the currently completed task, and the total number of tasks needs to be decremented by one. If the number of tasks has been cleared to 0, then call the doReleaseShared method. The doReleaseShared method needs to cooperate with the await method of CountDownLatch. So far we have got the answer to the first question: the number of divided tasks is updated by CAS, in fact, we can call it the synchronization state in AQS.

Analysis of await method

AQS-await

The await method we call the CountDownLatch method actually calls the acquireSharedInterruptibly of the Sync of the CountDownLatch inner class, this acquireSharedInterruptibly comes from the AbstractQueuedSynchronizer, tryAcquireShared is an empty method in the AbstractQueuedSynchronizer, which is left to subclasses to override, the implementation of CountDownLatch is, if state == 0, return 1, we focus on the doAcquireSharedInterruptibly method when the task is not completed, and addWaiter is called first in doAcquireSharedInterruptibly, so we then look at the addWaiter method, addWaiter, and push it up from the method name, which is to add a waiter.

 /**
* 为当前线程创建一个排队结点,并且给一个模式
* Creates and enqueues node for current thread and given mode.
* mode 有两种类型, 一种是独占模式,一种是共享模式。有没有想到独占锁和共享锁呢
* @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
* @return the new node
*/
private Node addWaiter(Node mode) {
        // 当前结点的下一个结点指向传入的结点
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        // 这个操作相当于设置尾结点
       // 首次添加为尾结点为null
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        // 走到这里说明 当前队列是空队列,直接添加结点
        enq(node);
        return node;
}

/* 
* 将当前结点插入到队列中,执行初始化。上面有图。突然觉得这里挺有意思的。
* 下面用图片演示一下这个过程
* Inserts node into queue, initializing if necessary. See picture above.
* @param node the node to insert
* @return node's predecessor // 返回插入结点的前结点
*/
private Node enq(final Node node) {
        for (;;) {
            // 获得tail的应用
            Node t = tail;
            // 如果tail 为空
            if (t == null) { // Must initialize
                // 设置头结点,此时头结点又是尾结点
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                // 如果尾结点非空代表此时队列非空,尾结点成为该结点的前驱结点
                node.prev = t;
                // 将当前结点设置尾结点
                if (compareAndSetTail(t, node)) {
                    // 将尾结点的后继结点指向插入结点
                    t.next = node;
                    return t;
                }
            }
        }
 }

New Node

队列补充

Exclusive lock If you are unfamiliar, let's change the translation name to an exclusive lock, which means that a lock can only be held by one thread at a time. If thread A adds an exclusive lock to data A, other threads cannot add any type of lock to A. The thread that obtains the exclusive lock can both read and modify data. The implementation classes of synchronized and Lock in JDK are exclusive locks , so ReentrantLock is also implemented with the help of AbstractQueuedSynronizer. A shared lock means that the lock can be held by multiple threads. If thread T adds a shared lock to thread data A, other threads can only add shared locks to A, but not exclusive locks. The thread that acquires the shared lock can only read the data and cannot modify the data.

ReentranLock has an inner class called Sync, and Sync has two subclasses: FairSync and NonfairSync. Fair means fair. If you are familiar with ReentranLock, you will think of fair locks and unfair locks of ReentranLock.

 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) {
                    // 获取state状态
                    // 大于零任务完成
                    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);
        }
}

The three methods of setHeadAndPropagate, shouldParkAfterFailedAcquire, and parkAndCheckInterrupt are related to each other. Let's take it out separately. According to the principle of knowing the name of the method, let's recognize the word. Propagate means proliferation. That is, we say that CountDownLatch is a thread cooperation tool. In the scenario mentioned, a thread needs to enable multi-threaded concurrent processing of data, and the processing logic in subsequent threads needs to process data concurrently before proceeding to the next stage of processing. Here we can speculate that when calling the await method of the CountDownLatch method, due to the task Threads that are blocked without completion are awakened by this method. At present, there is only one thread in the queue, which is a relatively simple situation. In this case, when one thread waits for multiple threads, the external manifestation is that only one thread calls the await method. Another usage scenario of CountDownLatch is to wait for one more, that is, multiple threads call the await method, and one thread calls countDown to complete the task, and wake up the thread in the waiting state.

Note that the above is an infinite loop, that is, when the thread calls the await method of CountDownLatch, there is no wait or Condition queue from Object to make the current thread wait, but to continuously obtain the state variable. Park means parking, so shouldParkAfterFailedAcquire should take a rest after failing to acquire the status? shouldParkAfterFailedAcquire has applications that involve node state. Let's first introduce the state, and then look at the source code of shouldParkAfterFailedAcquire.

waitStatus is an integer variable with the following candidate values:

  • 0 Default value when Node is initialized
  • CANCELLED = 1,

The Meituan technical team's article "The Principle and Application of AQS from the Implementation of ReentrantLock" in the reference material explains the state as:

Indicates that the thread's request to acquire the lock has been cancelled. After reading it, I feel a little immobile. This article starts with ReentrantLock, but I don't understand why the request to acquire the lock is cancelled.

This node is cancelled due to timeout or interrupt. Nodes never leave this state. In particular, a thread with cancelled node never again blocks.

Due to the timeout or the thread being interrupted, the node is canceled, this state will be fixed, and the thread node in the canceled state will not be blocked again.

  • SIGNAL = -1

The Meituan technical team's "Principle and Application of AQS from the Implementation of ReentrantLock" interprets this state as: It means that the current thread is ready, just waiting for the resource to be released.

"Java Concurrent Programming Series: Awesome AQS (Part 1)" (there is a reference link at the end of the article): Indicates that the thread corresponding to the next node is in a waiting state.

But the two explanations are complementary, and the combined understanding is: It means that the current thread is ready, just waiting for the resource to be released. At the same time, the thread corresponding to the node behind is in a waiting state.

The relevant comments about this node are listed below:

waitStatus value to indicate successor's thread needs unparking

Successor node threads need to release the wait state. (The corresponding Chinese translation of Parking is: parking, here is the thread as a car? The queue is a parking lot? )

The successor of this node is (or will soon be) blocked (via park), so the current node must unpark its successor when it releases or cancels. To avoid races, acquire methods must first indicate they need a signal, then retry the atomic acquire, and then, on failure, block.

The successor node of the current node is blocked or will be blocked (with park, it is difficult to translate, so I have to mix Chinese and English). Therefore, when the current node is released or cancelled, the blocking state of the subsequent node must be released. In order to avoid competition, the related method of acquire must first judge from the signal, then retry the acquisition atomically, and then fail and block.

  • CONDITION = -2
Indicates that the node is in the waiting queue, and the node thread is waiting to wake up.
  • PROPAGATE=-3
This field will only be used when the current thread is in the SHARED state, and the next synchronization state will be propagated unconditionally. (We will talk about this later, our current goal is to get a macro understanding of AQS from CountDownLatch to AQS)
 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
          // 获取前驱结点的状态
        int ws = pred.waitStatus;
        // 前继结点处于唤醒,当前结点可以被安全的被阻塞
        if (ws == Node.SIGNAL)
            return true;
         // 通过看注释可以看明白 ws > 0,说明当前结点pred处于取消状态。
        // 将该结点pred从队列里面移除,接着向前遍历
        if (ws > 0) {         
            do {
               node.prev = pred = pred.prev;
                
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            // 将pred,也就是当前结点的前一个结点设置为-1.
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
 }
 private final boolean parkAndCheckInterrupt() {
         // 用的是UnSafe,所以这里我们暂时不用关心它的实现原理,只需要知道线程调用这个方法
        // 相当于请求操作系统阻塞线程
        LockSupport.park(this);
        // 如果当前线程已经被中断,且满足shouldParkAfterFailedAcquire
        // 抛出异常。中断了就没有被阻塞这个概念。
        return Thread.interrupted();
}

Here we have roughly understood that when the thread calls the await method of the CountDownLatch method, it will actually enter the AQS queue. If the predecessor node of the current thread is in the wake-up state, then the thread will be blocked for a period of time. Since the thread here is blocked, how is the thread notified when the task is completed? We then look at the doReleaseShared method:

 private void doReleaseShared() {
        // 无限循环
        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);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

The idea of CountDownLatch

Now we can take a look at the idea of CountDownLatch. CountDownLatch is implemented with the help of AbstractQueuedSynchronizer. AbstractQueuedSynchronizer maintains a state variable. When calling the countDown method, the tryReleaseShared method of the CountDownLatch inner class is called to decrement the state. The fact of calling CountDownLatch is to indirectly call the acquireSharedInterruptibly method of AQS. The acquireSharedInterruptibly method will construct a queue. If the task is not completed, LockSupport will be called to block the thread calling the await method. When the task is completed, the blocked thread is finally awakened through doReleaseShared.

From CountDownLatch to AQS

We have basically introduced AQS above, but it is not very in-depth. We did not elaborate on some methods, but just treated them as a black box. In the process of exploring CountDownLatch, we found that most of the thread synchronization tool classes in the current JDK tools, such as AbstractQueuedSynchronizer appears in ReentrantLock, ReentrantReadWriteLock, Semaphore, and TheadPoolExecutor. This is a powerful thread synchronization framework. The idea of this article is from CountDownLatch to AQS, from understanding the design principle of CountDownLatch, and then simply eliciting AQS for the overall introduction later AbstractQueuedSynchronizer is the foreshadowing, focusing on the experience.

References


北冥有只鱼
147 声望35 粉丝