In-depth understanding of AbstractQueuedSynchronizer (AQS)

1. Introduction to AQS

In the previous article , we had a preliminary understanding of lock and AbstractQueuedSynchronizer (AQS). In the implementation of synchronization components, AQS is the core part. The implementers of synchronization components implement the semantics of synchronization components by using the template method provided by AQS, and AQS realizes the management of synchronization status, as well as queuing blocked threads, waiting for notifications , etc. Some low-level implementation handles. The core of AQS also includes these aspects: synchronization queue, acquisition and release of exclusive locks, acquisition and release of shared locks, interruptible locks, timeout waiting for lock acquisition , and these are actually provided by AQS The template method is summarized as follows:

Exclusive lock:

void acquire(int arg): exclusive acquisition of the synchronization state, if the acquisition fails, insert the synchronization queue to wait; void acquireInterruptibly(int arg): the same as the acquire method, but can detect interruption when waiting in the synchronization queue; boolean tryAcquireNanos (int arg, long nanosTimeout): The timeout waiting function is added on the basis of acquireInterruptibly, and the synchronization state is not obtained within the timeout period and returns false; boolean release(int arg): Release the synchronization state, this method will wake up the next in the synchronization queue a node

Shared lock:

void acquireShared(int arg): Shared acquisition of synchronization status, the difference from exclusive is that multiple threads acquire synchronization status at the same time; void acquireSharedInterruptibly(int arg): Added the ability to respond to interrupts based on acquireShared method; boolean tryAcquireSharedNanos(int arg, long nanosTimeout): adds the function of timeout waiting based on acquireSharedInterruptibly; boolean releaseShared(int arg): shared release synchronization state

To master the underlying implementation of AQS, in fact, is to learn the logic of these template methods. Before learning these template methods, we must first understand what kind of data structure the synchronization queue in AQS is, because the synchronization queue is the cornerstone of AQS's management of the synchronization state.

2. Synchronize the queue

When a shared resource is occupied by a thread, other threads requesting the resource will block and enter the synchronization queue. As far as the data structure is concerned, the implementation of the queue is nothing more than two ways: one is in the form of an array, and the other is in the form of a linked list. The synchronization queue in AQS is implemented in a chained manner . Next, obviously we will at least have this question: **1. What is the data structure of the node? 2. Is it one-way or two-way? 3. Is it a lead node or a non-lead node? **We still first look at the source code.

There is a static inner class Node in AQS, which has these properties:

volatile int waitStatus //Node status volatile Node prev //The predecessor node of the current node/thread volatile Node next; //The successor node of the current node/thread volatile Thread thread;//The thread that joins the synchronization queue refers to Node nextWaiter;//Wait the next node in the queue

The states of the nodes are as follows:

int CANCELLED = 1//The node cancels the synchronization queue int SIGNAL = -1//The thread of the successor node is in a waiting state, if the current node releases the synchronization state, it will notify the successor node, so that the thread of the successor node can run; int CONDITION = - 2//The current node enters the waiting queue int PROPAGATE = -3//Indicates that the next shared synchronization state acquisition will be propagated unconditionally int INITIAL = 0;//Initial state

Now that we know the data structure type of the node, and each node has its predecessor and successor, it is clear that this is a two-way queue . Similarly, we can take a look at it with a demo.

public class LockDemo {
    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(() -> {
                lock.lock();
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            });
            thread.start();
        }
    }
}

In the example code, 5 threads are opened, and the lock is acquired first and then sleeps for 10S. In fact, the thread sleep here is to simulate the situation of entering the synchronization queue when the thread cannot acquire the lock. Through debugging, when Thread-4 (the last thread in this example) fails to acquire the lock and enters synchronization, the current synchronization queue at AQS time is as shown in the figure:

LockDemo debug下 .png

Thread-0 first acquires the lock and then sleeps. Other threads (Thread-1, Thread-2, Thread-3, Thread-4) fail to acquire the lock and enter the synchronization queue. At the same time, it can be clearly seen that each node has two Domains: prev (precursor) and next (successor), and each node is used to save information such as thread references and waiting states that failed to obtain synchronization status. In addition, there are two important member variables in AQS:

private transient volatile Node head;
private transient volatile Node tail;

That is to say, AQS actually manages the synchronization queue through the head and tail pointers, and at the same time implements core methods including enqueuing the thread that fails to acquire the lock, and notifying the thread in the synchronization queue when the lock is released. Its schematic diagram is as follows:

Queue diagram.png

Through the understanding of the source code and the way of doing experiments, we can now clearly know the following points:

  1. The data structure of the node, that is, the static inner class Node of AQS, the waiting state of the node and other information ;
  2. The synchronization queue is a two-way queue, and AQS manages the synchronization queue by holding the head and tail pointers ;

So, how do nodes enqueue and dequeue? In fact, this corresponds to the two operations of lock acquisition and release: the lock acquisition fails to perform the enqueue operation, and the lock acquisition successfully performs the dequeue operation.

3. Exclusive lock

3.1 Acquisition of exclusive locks (acquire method)

Let's continue to look at the source code and debug. Take the above demo as an example. Calling the lock() method is to acquire an exclusive lock. If the acquisition fails, the current thread will be added to the synchronization queue. If it succeeds, the thread will be executed. The lock() method will actually call the **acquire()** method of AQS, the source code is as follows

public final void acquire(int arg) {
		//先看同步状态是否获取成功,如果成功则方法结束返回
		//若失败则先调用addWaiter()方法再调用acquireQueued()方法
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
}

For key information, see the comments, acquire does two things according to whether the current synchronization status is successful or not: 1. If it succeeds, the method ends and returns, 2. If it fails, it calls addWaiter() first and then calls the acquireQueued() method.

Failed to get synchronization status, enqueue operation

When the thread fails to acquire the exclusive lock, the current thread will be added to the synchronization queue, so what is the way to join the queue? We should look into addWaiter() and acquireQueued() next. The source code of addWaiter() is as follows:

private Node addWaiter(Node mode) {
		// 1. 将当前线程构建成Node类型
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        // 2. 当前尾节点是否为null?
		Node pred = tail;
        if (pred != null) {
			// 2.2 将当前节点尾插入的方式插入同步队列中
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
		// 2.1. 当前同步队列尾节点为null,说明当前线程是第一个加入同步队列进行等待的线程
        enq(node);
        return node;
}

Analysis can be seen in the comments above. The logic of the program is mainly divided into two parts: **1. The tail node of the current synchronization queue is null, and the method enq() is called to insert; 2. The tail node of the current queue is not null, then the tail insertion (compareAndSetTail() method is used. ) to join the team. ** There will also be another question: what if if (compareAndSetTail(pred, node))is false? It will continue to execute to the enq() method, and obviously compareAndSetTail is a CAS operation. Generally speaking, if the CAS operation fails, it will continue to spin (infinite loop) to retry. Therefore, after our analysis, the enq() method may undertake two tasks: **1. Enqueue when the current synchronization queue tail node is null; 2. If the CAS tail insertion node fails, it is responsible for spinning and trying . **So is it really like what we analyzed? Only the source code will tell us the answer :), the source code of enq() is as follows:

private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
			if (t == null) { // Must initialize
				//1. 构造头结点
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
				// 2. 尾插入,CAS操作失败自旋尝试
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
}

In the above analysis, we can see that the head node will be created first in the first step, indicating that the synchronization queue is a chain storage structure with the head node . Compared with the non-leading node, the leading node will obtain greater convenience in the operations of enqueuing and dequeuing, so the synchronization queue chooses the chain storage structure of the leading node. So what is the timing of the queue initialization of the leading node? Naturally, when tail is null, that is, the current thread is inserted into the synchronization queue for the first time . The compareAndSetTail(t, node) method will use the CAS operation to set the tail node. If the CAS operation fails, it will for (;;)continue to try in the for infinite loop until it returns successfully. Therefore, the enq() method can be summed up like this:

  1. When the current thread is the first to join the synchronization queue, the compareAndSetHead(new Node()) method is called to complete the initialization of the head node of the chained queue ;
  2. Spin keeps trying the CAS tail to insert the node until it succeeds .

Now that we know the process of wrapping the thread that fails to acquire the exclusive lock into a Node and inserting it into the synchronization queue? Then there is the next question? What will the nodes (threads) in the synchronization queue do to ensure that they have a chance to acquire an exclusive lock? With such a problem, let's take a look at the acquireQueued() method. It is clear from the method name. The function of this method is to queue up the process of acquiring locks. The source code is as follows:

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
				// 1. 获得当前节点的先驱节点
                final Node p = node.predecessor();
				// 2. 当前节点能否获取独占式锁					
				// 2.1 如果当前节点的先驱节点是头结点并且成功获取同步状态,即可以获得独占式锁
                if (p == head && tryAcquire(arg)) {
					//队列头指针用指向当前节点
                    setHead(node);
					//释放前驱节点
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
				// 2.2 获取锁失败,线程进入等待状态等待获取独占式锁
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
}

The program logic has been marked by comments. On the whole, this is a process of spin (for (;;)). The code first obtains the precursor node of the current node. If the precursor node is the head node and successfully obtained When the state is synchronized (if (p == head && tryAcquire(arg))), the thread pointed to by the current node can acquire the lock . Conversely, if the acquisition of the lock fails, it enters the waiting state. The overall schematic is as follows:

The overall schematic diagram of the spin acquisition lock.png

Successful lock acquisition, dequeue operation

The logic for dequeuing the node that acquires the lock is:

//队列头结点引用指向当前节点
setHead(node);
//释放前驱节点
p.next = null; // help GC
failed = false;
return interrupted;

The setHead() method is:

private void setHead(Node node) {
        head = node;
        node.thread = null;
        node.prev = null;
}

Set the current node as the head node of the queue through the setHead() method, and then set the next field of the previous head node to null and the pre field to null, that is, disconnected from the queue, and there is no reference to facilitate the GC. Memory is reclaimed. The schematic diagram is as follows:

The current node refers to the thread to acquire the lock, and the current node is set to the queue head node.png

Then when the acquisition of the lock fails, the shouldParkAfterFailedAcquire() method and the parkAndCheckInterrupt() method are called to see what they have done. The source code of the shouldParkAfterFailedAcquire() method is:

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.
         */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

The main logic of the shouldParkAfterFailedAcquire() method is to compareAndSetWaitStatus(pred, ws, Node.SIGNAL)use CAS to set the node status from INITIAL to SIGNAL, indicating that the current thread is blocked. When the compareAndSetWaitStatus setting fails, it means that the shouldParkAfterFailedAcquire method returns false, and then it will continue to retry in the for (;;) infinite loop in the acquireQueued() method, until the compareAndSetWaitStatus sets the node status bit to SIGNAL and shouldParkAfterFailedAcquire returns true before executing the method parkAndCheckInterrupt () method, the source code of this method is:

private final boolean parkAndCheckInterrupt() {
        //使得该线程阻塞
		LockSupport.park(this);
        return Thread.interrupted();
}

The key to this method is to call the LookSupport.park() method (LookSupport will be discussed in a later article), which is used to block the current thread. So it should be clear here that acquireQueued() mainly accomplishes two things during the spin process:

  1. If the predecessor node of the current node is the head node and can obtain the synchronization state, the current thread can obtain the lock and the method execution ends and exits ;
  2. If the lock acquisition fails, first set the node state to SIGNAL, and then call the LookSupport.park method to block the current thread .

After the above analysis, the acquisition process of the exclusive lock, that is, the execution flow of the acquire() method is shown in the following figure:

Exclusive lock acquisition (acquire() method) flowchart.png

3.2 Release of exclusive lock (release() method)

The release of exclusive locks is relatively easy to understand. Let's take a look at the source code without talking nonsense:

public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
}

The logic of this code is easier to understand. If the synchronization state is released successfully (tryRelease returns true), the code in the if block will be executed. When the head node pointed to by head is not null and the state value of the node is not 0 The unparkSuccessor() method will be executed. The source code of the unparkSuccessor method:

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)
		//后继节点不为null时唤醒该线程
        LockSupport.unpark(s.thread);
}

For the key information of the source code, please refer to the comments. First, the successor node of the head node is obtained. When the successor node is used, the LookSupport.unpark() method will be called, which will wake up the thread wrapped by the successor node of the node. Therefore, each time the lock is released, the thread referenced by the node's successor node in the queue will be awakened, which further proves that the process of acquiring the lock is a FIFO (first-in, first-out) process.

Now we have finally gnawed on a hard bone. By learning the source code, we have deeply learned the process of acquiring and releasing exclusive locks and the synchronization queue. To sum up:

  1. If the thread fails to acquire the lock, the thread is encapsulated into a Node for the enqueue operation. The core methods are addWaiter() and enq(), and enq() completes the initialization of the head node of the synchronization queue and the retry of the CAS operation failure ;
  2. Thread acquiring a lock is a spinning process. If and only if the predecessor node of the current node is the head node and successfully obtains the synchronization state, the node is dequeued, that is, the thread referenced by the node acquires the lock, otherwise, when the condition is not met, the thread is dequeued. The LookSupport.park() method will be called to block the thread ;
  3. When the lock is released, the successor node will be awakened;

In general: when obtaining the synchronization state, AQS maintains a synchronization queue, and the thread that fails to obtain the synchronization state will join the queue for spin; the condition for removing the queue (or stopping the spin) is that the precursor node is the head node and Successfully obtained sync status. When releasing the synchronization state, the synchronizer will call the unparkSuccessor() method to wake up the successor node.

Exclusive lock feature learning

3.3 Interrupt acquisition lock (acquireInterruptibly method)

We know that lock has some more convenient features than synchronized, such as being able to respond to interrupts and waiting for timeouts. Now we still use the method of learning the source code to see how to respond to interrupts. The method lock.lockInterruptibly() can be called for a responsive interruptible lock; and the bottom layer of this method will call the acquireInterruptibly method of AQS. The source code is:

public final void acquireInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (!tryAcquire(arg))
		//线程获取锁失败
        doAcquireInterruptibly(arg);
}

The doAcquireInterruptibly method is called after failing to get the synchronization state:

private void doAcquireInterruptibly(int arg)
    throws InterruptedException {
	//将节点插入到同步队列中
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            //获取锁出队
			if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
				//线程中断抛异常
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

Please see the comments for key information. It is very easy to read this code now :), which is almost the same as the acquire method logic. The only difference is that when parkAndCheckInterrupt returns true, that is, when the thread is blocked, the thread is interrupted, and the code throws an interrupted exception. .

3.4 Timeout waiting to acquire locks (tryAcquireNanos() method)

By calling lock.tryLock(timeout, TimeUnit), the effect of waiting for the lock to be acquired over time is achieved. This method will return in three cases:

  1. Within the timeout period, the current thread successfully acquired the lock;
  2. The current thread was interrupted within the timeout period;
  3. If the timeout expires and the lock has not been acquired, return false.

We still learn how the bottom layer is implemented by reading the source code. This method will call the AQS method tryAcquireNanos(). The source code is:

public final boolean tryAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    return tryAcquire(arg) ||
		//实现超时等待的效果
        doAcquireNanos(arg, nanosTimeout);
}

Obviously, this source code finally relies on the doAcquireNanos method to achieve the effect of timeout waiting. The source code of this method is as follows:

private boolean doAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    if (nanosTimeout <= 0L)
        return false;
	//1. 根据超时时间和当前时间计算出截止时间
    final long deadline = System.nanoTime() + nanosTimeout;
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
			//2. 当前线程获得锁出队列
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return true;
            }
			// 3.1 重新计算超时时间
            nanosTimeout = deadline - System.nanoTime();
            // 3.2 已经超时返回false
			if (nanosTimeout <= 0L)
                return false;
			// 3.3 线程阻塞等待 
            if (shouldParkAfterFailedAcquire(p, node) &&
                nanosTimeout > spinForTimeoutThreshold)
                LockSupport.parkNanos(this, nanosTimeout);
            // 3.4 线程被中断抛出被中断异常
			if (Thread.interrupted())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

The program logic is shown in the figure:

Timeout waiting to acquire locks (doAcquireNanos() method)

The program logic is basically the same as the exclusive lock can respond to interrupted acquisition. The only difference is that after the acquisition of the lock fails, in the processing of the timeout time, in the first step, the theoretical deadline will be calculated according to the current time and the timeout time. , for example, the current time is 8h10min and the timeout time is 10min, then according to the deadline = System.nanoTime() + nanosTimeoutcalculation, the system time when the timeout time is reached is 8h 10min+10min = 8h 20min. Then deadline - System.nanoTime()it can be judged according to whether it has timed out. For example, if the current system time is 8h 30min, it has obviously exceeded the theoretical system time of 8h 20min. deadline - System.nanoTime()The calculated value is a negative number, which will naturally return false between the If judgments in step 3.2. . If it has not timed out, that is, if the if in step 3.2 is judged to be true, it will continue to execute step 3.3 to block the current thread through LockSupport.parkNanos . At the same time, the detection of interruption is added in step 3.4. If it is detected to be interrupted, it will be thrown directly and interrupted. abnormal.

4. Shared lock

4.1 Acquisition of shared locks (acquireShared() method)

After talking about the implementation of exclusive locks by AQS, let's continue to take a look at how shared locks are implemented? The acquisition method of the shared lock is acquireShared, and the source code is:

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

The logic of this source code is easy to understand. In this method, the tryAcquireShared method will be called first. The return value of tryAcquireShared is an int type. When the return value is greater than or equal to 0, the end of the method indicates that the lock has been successfully acquired. Otherwise, it indicates that the synchronization state has been acquired. Failure means that the referenced thread fails to acquire the lock, and the doAcquireShared method will be executed. The source code of this method is:

private void doAcquireShared(int arg) {
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
					// 当该节点的前驱节点是头结点且成功获取同步状态
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

Wouldn't it be easy to see this code now? The logic is almost the same as the acquisition of an exclusive lock. The condition for exiting during the spin process is that the predecessor node of the current node is the head node and the return value of tryAcquireShared(arg) is greater than or equal to 0, and the synchronization state can be successfully obtained .

4.2 Release of shared locks (releaseShared() method)

The release of the shared lock will call the method releaseShared in AQS:

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

When the synchronization state is successfully released, tryReleaseShared will continue to execute the doReleaseShared method:

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

This method is a bit different from the exclusive lock release process. In the process of shared lock release, for concurrent components that can support simultaneous access by multiple threads, it is necessary to ensure that multiple threads can safely release the synchronization state. The CAS used here It is guaranteed that when the CAS operation fails to continue, it will be retried in the next loop.

4.3 Interruptible (acquireSharedInterruptibly() method), timeout waiting (tryAcquireSharedNanos() method)

The implementation of interruptible lock and timeout waiting is almost the same as the implementation of exclusive lock interruptable lock acquisition and timeout waiting. The specifics will not be described. If you understand the above content, your understanding of this part will come naturally.

Through this article, I have deepened the understanding of the underlying implementation of AQS, and laid a foundation for understanding the implementation principles of concurrent components. There is no end to learning, and continue to cheer :); If you think it is good, please give it a thumbs up, hehe.

references

The Art of Java Concurrent Programming

Guess you like

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