1
头图

Guide :

In our daily development, we often encounter concurrent scenarios. In the Java language system, we will think of ReentrantLock, CountDownLatch, Semaphore and other tools, but do you know their internal implementation principles? These tools are very similar, and the bottom layer is based on AbstractQueuedSynchronizer (AQS). Today we will learn the internal principles of AQS together. As the saying goes, knowing yourself and knowing your enemy will win a hundred battles. If we understand the principle, we can do more with less when developing with such tools.

Text|Xia Fuli Senior Development Engineer of NetEase Zhiqi

1. AQS execution framework

The following figure is the general execution framework of AQS:

 title=

Through the above picture, you can understand the general execution process of AQS. ReentrantLock, CountDownLatch, and Semaphore are all encapsulated in this process.

Take ReentrantLock as an example, ReentrantLock is an exclusive lock, can be a fair lock or an unfair lock, can be well understood through this picture.

ReentrantLock is an exclusive lock, which means that only one thread can successfully try to acquire resources at the same time. Others that fail to acquire will be queued at the tail of the blocking queue.

A fair lock means that threads acquire resources in strict accordance with the order of blocking queues, on a first-come-first-served basis, and are not allowed to jump in the queue. As shown below:

 title=

instead of fair locks may have the possibility of queue jumping. For example, if the head node above is woken up and is about to try to acquire resources, then a thread also tries to acquire resources. It is possible that the new thread acquires resources successfully, but the head node fails to acquire resources. This is an unfair lock.

In the ReentrantLock source code, it can be seen that when an unfair lock tries to acquire resources, will not consider whether the blocking queue is empty, and will directly occupy resources if it can acquire resources successfully. If the acquisition fails, it will join the blocking queue. code show as below:

final boolean nonfairTryAcquire(int acquires) {    final Thread current = Thread.currentThread();    int c = getState();    if (c == 0) {        if (compareAndSetState(0, acquires)) {            setExclusiveOwnerThread(current);            return true;        }    }    else if (current == getExclusiveOwnerThread()) {        int nextc = c + acquires;        if (nextc < 0) // overflow            throw new Error("Maximum lock count exceeded");        setState(nextc);        return true;    }    return false;}

When a fair lock tries to acquire resources, it will determine whether the blocking queue is empty (the key difference from an unfair lock), as follows:

static final class FairSync extends Sync {    private static final long serialVersionUID = -3000897897090466540L;
    final void lock() {        acquire(1);    }
    /**     * Fair version of tryAcquire.  Don't grant access unless     * recursive call or no waiters or is first.     */    @ReservedStackAccess    protected final boolean tryAcquire(int acquires) {        final Thread current = Thread.currentThread();        int c = getState();        if (c == 0) {            if (!hasQueuedPredecessors() &&                compareAndSetState(0, acquires)) {                setExclusiveOwnerThread(current);                return true;            }        }        else if (current == getExclusiveOwnerThread()) {            int nextc = c + acquires;            if (nextc < 0)                throw new Error("Maximum lock count exceeded");            setState(nextc);            return true;        }        return false;    }}

2. Detailed explanation of AQS implementation principle

AQS knows from the name that it is an abstract class, and defines the process of the above picture through the template method. The definitions of "resource occupation" and "resource release" are handed over to specific subclasses to define and implement.

For shared locks, subclasses need to implement the following two methods:

protected int tryAcquireShared(int arg);protected boolean tryReleaseShared(int arg);
  • tryAcquireShared: Shared method. arg is the number of lock acquisitions, tries to acquire resources; the return value is:

    Negative numbers indicate failure;

    0 means success, but no remaining available resources;

    A positive number indicates success and there are remaining resources;

<!---->

  • tryReleaseShared: Shared method. arg is the number of times to release the lock, tries to release the resource, if the subsequent waiting node is allowed to wake up after the release, it returns True, otherwise it returns False;

For exclusive locks, subclasses need to implement three methods:

protected boolean tryAcquire(int arg)protected boolean tryRelease(int arg)protected boolean isHeldExclusively()
  • isHeldExclusively: this thread is exclusively holding resources. It needs to be implemented only when using Condition;
  • tryAcquire: exclusive way. arg is the number of times to acquire the lock, try to acquire the resource, return True if it succeeds, and False if it fails;
  • tryRelease: exclusive way. arg is the number of times to release the lock, try to release the resource, return True if it succeeds, and False if it fails;

get resource

First, let's look at the process of acquiring resources with exclusive locks.

In AQS, the core code for acquiring resources for exclusive locks is as follows:

public final void acquire(int arg) {    // 当 tryAcquire 返回 true 就说明获取到锁了,直接结束。    // 反之,返回 false 的话,就需要执行后面的方法。    if (!tryAcquire(arg) &&        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))        selfInterrupt();}

If the tryAcquire of the subclass returns true, it means that the lock is acquired successfully and ends directly.

As long as the tryAcquire method of the subclass returns false, it means that the acquisition of the lock fails, and you need to add yourself to the queue.

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;    // 如果 tail 节点不是 null,就将新节点的 pred 节点设置为 tail 节点。    // 并且将新节点设置成 tail 节点。    if (pred != null) {        node.prev = pred;        if (compareAndSetTail(pred, node)) {            pred.next = node;            return node;        }    }    // 如果 tail 节点是  null,或者 CAS 设置 tail 失败。    // 在 enq 方法中处理    enq(node);    return node;}

If the tail node is null, or the CAS fails to set tail, the tail node will be added by spinning.

private Node enq(final Node node) {    for (;;) {        Node t = tail;        // 如果 tail 是 null,就创建一个虚拟节点,同时指向 head 和 tail,称为 初始化。        if (t == null) { // Must initialize            if (compareAndSetHead(new Node()))                tail = head;        } else {// 如果不是 null            // 和 上个方法逻辑一样,将新节点追加到 tail 节点后面,并更新队列的 tail 为新节点。            // 只不过这里是死循环的,失败了还可以再来 。            node.prev = t;            if (compareAndSetTail(t, node)) {                t.next = node;                return t;            }        }    }}

What is the logic of the enq method? When tail is null (no queue is initialized), the queue needs to be initialized. If CAS fails to set tail, it will also go here. You need to set tail cyclically in the enq method. until successful.

The above process is represented by a diagram as follows:

After adding yourself to the blocking queue (note that the addWaiter method returns the current node), execute the acquireQueued() method to suspend the thread corresponding to the current node. The source code is as follows:

// 这里返回的节点是新创建的节点,arg 是请求的数量final boolean acquireQueued(final Node node, int arg) {    boolean failed = true;    try {        boolean interrupted = false;        for (;;) {            // 找上一个节点            final Node p = node.predecessor();            // 如果上一个节点是 head ,就尝试获取锁            // 如果 获取成功,就将当前节点设置为 head,注意 head 节点是永远不会唤醒的。            if (p == head && tryAcquire(arg)) {                setHead(node);                p.next = null; // help GC                failed = false;                return interrupted;            }            // 在获取锁失败后,就需要阻塞了。            // shouldParkAfterFailedAcquire ---> 检查上一个节点的状态,如果是 SIGNAL 就阻塞,否则就改成 SIGNAL。            if (shouldParkAfterFailedAcquire(p, node) &&                parkAndCheckInterrupt())                interrupted = true;        }    } finally {        if (failed)            cancelAcquire(node);    }}

This method has two logics:

  • How to suspend yourself?
  • What do you do when you wake up?

Answer the second question first: what do you do after being awakened?

Try to get the lock. After success, set yourself as head and disconnect from next.

Look at the first question: how to suspend yourself?

The specific logic is in the shouldParkAfterFailedAcquire method:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {    int ws = pred.waitStatus;    //  如果他的上一个节点的 ws 是 SIGNAL,他就需要阻塞。    if (ws == Node.SIGNAL)        // 阻塞        return true;    // 前任被取消。跳过前任并重试。    if (ws > 0) {        do {            // 将前任的前任 赋值给 当前的前任            node.prev = pred = pred.prev;        } while (pred.waitStatus > 0);        // 将前任的前任的 next 赋值为 当前节点        pred.next = node;    } else {         // 如果没有取消 || 0 || CONDITION || PROPAGATE,那么就将前任的 ws 设置成 SIGNAL.        // 为什么必须是 SIGNAL 呢?        // 答:希望自己的上一个节点在释放锁的时候,通知自己(让自己获取锁)        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);    }    // 重来    return false;}

The main logic of this method is to change the state of the front node to SIGNAL and tell him: remember to wake me up when you release the lock. Among them, if the predecessor node is canceled, it will be skipped. Then when the front node releases the lock, it will definitely wake up this node.

The above is the process of acquiring resources for exclusive locks. The process of acquiring resources for shared locks is similar, but there will be slight differences. The core code is as follows:

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

This method also contains two logics:

  • How to suspend yourself?
  • What do you do when you wake up?

There is no difference between how to suspend yourself and an exclusive lock. What to do after wakes up is the key to distinguish it from exclusive locks: If the current node wakes up and acquires the lock, it will wake up the next node. After the next node wakes up, it will continue to wake up the next node, thereby waking up all threads waiting for the shared lock. The core code is as follows:

private void setHeadAndPropagate(Node node, int propagate) {        Node h = head; // Record old head for check below        // 将自己设置为头结点        setHead(node);        if (propagate > 0 || h == null || h.waitStatus < 0 ||            (h = head) == null || h.waitStatus < 0) {            Node s = node.next;            if (s == null || s.isShared())                // 唤醒下一个节点                doReleaseShared();        }    }

release resources

The logic of acquiring resources is discussed above, so how to release resources?

Similarly, let's first look at the release logic of exclusive locks:

public final boolean release(int arg) {    if (tryRelease(arg)) {        Node h = head;        // 所有的节点在将自己挂起之前,都会将前置节点设置成 SIGNAL,希望前置节点释放的时候,唤醒自己。        // 如果前置节点是 0 ,说明前置节点已经释放过了。不能重复释放了,后面将会看到释放后会将 ws 修改成0.        if (h != null && h.waitStatus != 0)            unparkSuccessor(h);        return true;    }    return false;}

It can be seen from the judgment of this method that head must not be equal to 0. why? As mentioned in the process of obtaining resources above: before a node tries to suspend itself, it will set the front node to SIGNAL -1. Even if it is the first node to join the queue, it will also set the initialization node after it fails to acquire the lock. ws is set to SIGNAL.

And this judgment is also to prevent multiple threads from repeatedly releasing, then after releasing the lock, the ws state will definitely be set to 0. Prevent repeated operations. code show as below:

private void unparkSuccessor(Node node) {    int ws = node.waitStatus;    if (ws < 0)        // 将 head 节点的 ws 改成 0,清除信号。表示,他已经释放过了。不能重复释放。        compareAndSetWaitStatus(node, ws, 0);
    Node s = node.next;    // 如果 next 是 null,或者 next 被取消了。就从 tail 开始向上找节点。    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);}

Remember what the logic looks like after waking up? When explaining resource acquisition above, it was mentioned:

After the thread wakes up, it tries to get the lock. If the lock is successful, it sets itself as head, and disconnects the previous head from itself.

final boolean acquireQueued(final Node node, long arg) {    boolean failed = true;    try {        boolean interrupted = false;        //唤醒之后再次进行for循环,尝试获取锁,获取成功则将自己设置为头结点        for (;;) {            final Node p = node.predecessor();            if (p == head && tryAcquire(arg)) {                setHead(node);                p.next = null; // help GC                failed = false;                return interrupted;            }            if (shouldParkAfterFailedAcquire(p, node) &&                parkAndCheckInterrupt())                interrupted = true;        }    }

Let's take a look at the release logic of the shared lock. The code is as follows:

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

The doReleaseShared() code is as follows:

private void doReleaseShared() {        for (;;) {            Node h = head;            if (h != null && h != tail) {                int ws = h.waitStatus;                if (ws == Node.SIGNAL) {                // 将 head 节点的 ws 改成 0,清除信号。表示,他已经释放过了。不能重复释放。                    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;        }    }

Likewise, what to do after waking up?

After the thread wakes up, it tries to take the lock. If the lock succeeds, it sets itself as head, disconnects the connection between the previous head and itself, and wakes up the next node. The code is as follows:

private void doAcquireShared(long arg) {    final Node node = addWaiter(Node.SHARED);    boolean failed = true;    try {        boolean interrupted = false;        //唤醒后再次for循环,尝试获取锁,获取锁成功,则设置自己为头结点,        //并唤醒下一个结点,从而一直传播下去,将所有等待共享锁的线程唤醒        for (;;) {            final Node p = node.predecessor();            if (p == head) {                long 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;        }    }}

In order to prove that the shared lock wakes up one by one, we use a demo to verify, the sample code is as follows:

CountDownLatch countDownLatch = new CountDownLatch(1);
        Thread t1 = new Thread(() -> {            try {                TimeUnit.SECONDS.sleep(1);                countDownLatch.await();                System.out.println("线程1被唤醒了");            } catch (InterruptedException e) {                e.printStackTrace();            }        });
        Thread t2 = new Thread(() -> {            try {                TimeUnit.SECONDS.sleep(2);                countDownLatch.await();                System.out.println("线程2被唤醒了");            } catch (InterruptedException e) {                e.printStackTrace();            }        });
        Thread t3 = new Thread(() -> {            try {                TimeUnit.SECONDS.sleep(3);                countDownLatch.await();                System.out.println("线程3被唤醒了");            } catch (InterruptedException e) {                e.printStackTrace();            }        });
        t1.start();        t2.start();        t3.start();
        TimeUnit.SECONDS.sleep(4);        countDownLatch.countDown();    }

In the above example code, threads 1, 2, and 3 are added to the blocking queue in order. When the main thread calls countDown(), thread 1 will be woken up at this time, and after thread 1 wakes up, thread 2 will be woken up, and thread 2 will wake up thread 3. It can be seen from the running results that this is indeed the case:

线程1被唤醒了
线程2被唤醒了
线程3被唤醒了

This proves the above inference.

3. Summary

When the exclusive lock and the shared lock fail to acquire resources, they will add themselves to the tail of the blocking queue, and set the ws of the previous node to SINGAL, and tell him: remember to wake me up when releasing the lock.

The difference between an exclusive lock and a shared lock is that after the node is woken up, the exclusive lock thread will not wake up the next node (the lock must be released actively to wake up, for example, when using ReentrantLock, the release() method must be called to actively release the lock).

For shared locks, as long as a node is woken up, it will continue to wake up the next node, and the next node will wake up the next node, thereby awakening all threads waiting for the shared lock.

Author introduction

Xia Fuli, senior development engineer of Netease Zhiqi, mainly responsible for the research and development of Netease Qiyu online intelligent customer service


网易数智
619 声望140 粉丝

欢迎关注网易云信 GitHub: