Interviewer: Tell me about the difference between AQS shared/exclusive acquisition/release of resources

The previous article introduced the dequeue and enqueue operations of AQS built-in queue nodes, as well as the detailed process of exclusive acquisition and release of shared resources. For structural integrity, this article will continue to introduce another one from the perspective of AQS: shared mode acquisition and For the details of releasing resources, this article will not analyze the implementation of specific subclasses such as ReentrantLock and ReentrantReadWriteLock, and will continue to add it later.

Exclusive access to resources

Friendly reminder: This article focuses on the characteristics of the shared mode to acquire and release resources. Many code implementations are similar in logic to the shared and exclusive types. For a clear comparison, part of the exclusive core code will be pasted here, and pay attention to understanding There are differences between shared and exclusive. For detailed analysis, please see my last article: Java Concurrent Package Source Code Learning Series: CLH Synchronous Queue and Synchronous Resource Acquisition and Release

void acquire(int arg)

public final void acquire(int arg) {
        if (!tryAcquire(arg) && // tryAcquire由子类实现,表示获取锁,如果成功,这个方法直接返回了
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 如果获取失败,执行
            selfInterrupt();
    }

boolean acquireQueued(Node, int)

// 这个方法如果返回true,代码将进入selfInterrupt()
	final boolean acquireQueued(final Node node, int arg) {
        // 注意默认为true
        boolean failed = true;
        try {
            // 是否中断
            boolean interrupted = false;
            // 自旋,即死循环
            for (;;) {
                // 得到node的前驱节点
                final Node p = node.predecessor();
                // 我们知道head是虚拟的头节点,p==head表示如果node为阻塞队列的第一个真实节点
                // 就执行tryAcquire逻辑,这里tryAcquire也需要由子类实现
                if (p == head && tryAcquire(arg)) {
                    // tryAcquire获取成功走到这,执行setHead出队操作 
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // 走到这有两种情况 1.node不是第一个节点 2.tryAcquire争夺锁失败了
                // 这里就判断 如果当前线程争锁失败,是否需要挂起当前这个线程
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            // 死循环退出,只有tryAcquire获取锁失败的时候failed才为true
            if (failed)
                cancelAcquire(node);
        }
    }

Exclusive release of resources

boolean release(int arg)

public final boolean release(int arg) {
        if (tryRelease(arg)) { // 子类实现tryRelease方法
            // 获得当前head
            Node h = head;
            // head不为null并且head的等待状态不为0
            if (h != null && h.waitStatus != 0)
                // 唤醒下一个可以被唤醒的线程,不一定是next哦
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

void unparkSuccessor(Node node)

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;
        // 如果node的waitStatus<0为signal,CAS修改为0
        // 将 head 节点的 ws 改成 0,清除信号。表示,他已经释放过了。不能重复释放。
        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.
         */
        // 唤醒后继节点,但是有可能后继节点取消了等待 即 waitStatus == 1
        Node s = node.next;
        // 如果后继节点为空或者它已经放弃锁了
        if (s == null || s.waitStatus > 0) {
            s = null;
            // 从队尾往前找,找到没有没取消的所有节点排在最前面的【直到t为null或t==node才退出循环嘛】
            for (Node t = tail; t != null && t != node; t = t.prev)
                // 如果>0表示节点被取消了,就一直向前找呗,找到之后不会return,还会一直向前
                if (t.waitStatus <= 0)
                    s = t;
        }
        // 如果后继节点存在且没有被取消,会走到这,直接唤醒后继节点即可
        if (s != null)
            LockSupport.unpark(s.thread);
    }

Shared access to resources

void acquireShared(int arg)

public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0) //子类实现
            doAcquireShared(arg);
    }

  • tryAcquireShared(int) is a hook method provided by AQS for subclasses to implement. Subclasses can customize the way to achieve shared resource acquisition. If the acquisition status fails, the return value is less than 0, a return value of zero means exclusive acquisition, and a positive return value means shared means Obtain.
  • If the acquisition fails, enter the logic of doAcquireShared(arg);.

void doAcquireShared(int arg)

Note the difference between this and the exclusive acquisition of resources acquireQueued.

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

We can see that there are several differences:

  1. When the shared resource fails, it will be packaged into a node in the SHARED mode to join the team.
  2. If the predecessor node is head, use the tryAcquireShared method to try to obtain the synchronization state, which is implemented by the subclass.
  3. If the acquisition is successful r>=0, then call setHeadAndPropagate(node, r). This method will first set the new first node, dequeue the first node, and then continuously wake up the next shared mode node to achieve synchronization. Multiple threads share acquisition.

Next we will focus on the setHeadAndPropagate method.

void setHeadAndPropagate(Node node, int propagate)

private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        // 节点出队,设置node为新的head
        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.
         */
        // 这个方法进来的时候propagate>=0
        // propagate>0表示同步状态还可以被后面的节点获取
        // h指向原先的head节点,之后h = head,h表示新的head节点
        // h.waitStatus<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();
        }
    }

  • Record the original head node first, and then set node as the new head node.
  • The original head node or the new head node waiting state is propagate or signal, and can continue to wake up downward.
  • If it is judged that the next node is a shared node, call the shared resource release method to wake up subsequent nodes.

Shared release of resources

boolean releaseShared(int arg)

public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) { // 子类实现
            doReleaseShared();
            return true;
        }
        return false;
    }

doReleaseShared()

It can be found that in the shared mode, the doReleaseShared method is called whether to acquire or release the resource. It can be seen that this method is the core method of releasing the resource in the shared mode to wake up the node. The main function is to wake up the next thread or set the propagation state .

After the subsequent thread is awakened, it will try to acquire the shared lock. If it succeeds, it will call setHeadAndPropagate to propagate the awakening. The function of this method is to ensure that the nodes in the waiting state in the queue can be awakened in the case of competition between acquire and release.

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;
                // 无论是独占还是共享,只有节点的ws为signal的时候,才会在释放的时候,唤醒后面的节点
                if (ws == Node.SIGNAL) {
                    // cas将ws设置为0,设置失败,将会继续从循环开始
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    // 唤醒后继节点,unparkSuccessor这个方法上面已经解析过
                    unparkSuccessor(h);
                }
                // 如果ws为0,则更新状态为propagate,
                // 之后setHeadAndPropagate读到ws<0的时候,会继续唤醒后面节点
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            // 如果head在这个过程中被更改了,会继续自旋
            if (h == head)                   // loop if head changed
                break;
        }
    }

This method does two things when the head node has a successor node:

  1. If the head node's waiting state is SIGNAL, the head node state is set to 0, and subsequent nodes that are not cancelled are awakened.
  2. If the waiting state of the head node is 0, set the state of the head node to PROPAGATE to ensure that the wake-up can propagate normally.

The role of setting PROPAGATE : PROPAGATE state is used in [setHeadAndPropagate](#void setHeadAndPropagate(Node node, int propagate)), when the head node state is set to PROPAGATE, subsequent nodes become the new head node. If the condition of propagate> 0 is not established, it is determined whether to wake up subsequent nodes according to whether the condition h.waitStatus <0 is established or not, that is, the wake-up action is propagated backward.

What problem is the introduction of PROPAGATE to solve?

AbstractQueuedSynchronizer source code interpretation, I strongly recommend reading this blog.

Summary of the difference between exclusive and shared

The biggest difference between shared acquisition and exclusive acquisition is whether multiple threads can acquire the synchronization state at the same time. When sharing resources, other shared accesses will be allowed at the same time. When accessing resources exclusively, other accesses are blocked at the same time.

AQS provides hook methods implemented by subclasses. The exclusive representative methods are: tryAcquire and tryRelease and isHeldExclusively methods, and the shared representative methods are: tryAcquireShared and tryReleaseShared methods.

The standard form of get operation and release operation in AQS:

boolean acquire() throws InterruptedException{
    while( 当前状态不允许获取操作 ){
        if( 需要阻塞获取请求){
            如果当前线程不在队列中,则将其插入队列
            阻塞当前线程
        }else{
            返回失败
        }
    }
    可能更新同步器的状态
    如果线程位于队列中,则将其移除队列
    返回成功
}

void release(){
    更新同步器的状态
    if( 新的状态允许某个被阻塞的线程获取成功 ){
        解除队列中一个或多个线程的阻塞状态
    }
}

Source: "The Art of Concurrent Programming" The following figure shows the process of obtaining exclusive synchronization status

 

Interviewer: Tell me about the difference between AQS shared/exclusive acquisition/release of resources

 

When a thread fails to compete for synchronization resources, they will package the thread as a node, and join the end of the CLH synchronization queue, and keep spinning, one is addWaiter (Node.EXCLUSIVE), the other is addWaiter (Node.EXCLUSIVE).

Threads in the synchronization queue will determine whether their predecessor node is the head node when spinning. If it is the head node node.predecessor() == head, they will all try to obtain the synchronization status, but:

  • After the exclusive acquisition status is successful, only one node will be dequeued.
  • After the shared state is successfully obtained, in addition to leaving one node out of the team, the following nodes will also be awakened.

After the threads execute the logic, they will release the synchronization state. After the release, the unparkSuccessor(h) will wake up a successor node that can be awakened later.

Original link: https://www.cnblogs.com/summerday152/p/14253917.html

If you think this article is helpful to you, you can follow my official account and reply to the keyword [Interview] to get a compilation of Java core knowledge points and an interview gift package! There are more technical dry goods articles and related materials to share, let everyone learn and progress together!

Guess you like

Origin blog.csdn.net/weixin_48182198/article/details/112436972