Java concurrent package source code learning: CLH synchronization queue and synchronization resource acquisition and release

The learning objectives of this article

  • Review the structure of the CLH synchronization queue.
  • Learn the process of acquiring and releasing exclusive resources.

CLH queue structure

I'm in the Java Concurrent Package Source Learning Series: AbstractQueuedSynchronizer#Synchronous Queue and Node node. I have roughly introduced the structure of CLH. This article mainly analyzes the related operations of the synchronous queue, so I will review it here:

AQS uses the built-in FIFO to synchronize the two-way queue to complete the queuing of the resource acquisition thread, internally through the node head [actually a virtual node, the real first thread is in the position of head.next] and tail to record the head and tail elements of the queue , The queue element type is Node.

 

 

  • If the current thread fails to acquire the synchronization state (lock), AQS will construct a node (Node) and add it to the synchronization queue, and block the current thread at the same time.
  • When the synchronization state is released, the thread in the node will be awakened to make it try to obtain the synchronization state again.

Next, we will use AQS to exclusively acquire and release resources to explain the workflow of the built-in CLH blocking queue in detail, and then look down.

Resource acquisition

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

  • tryAcquire(int) is a hook method provided by AQS to subclasses. Subclasses can customize the way to obtain resources exclusively. If the acquisition succeeds, it returns true, and if it fails, it returns false.
  • If the tryAcquire method succeeds in acquiring the resource, it returns directly, and the failed transformation will execute the logic of acquireQueued(addWaiter(Node.EXCLUSIVE), arg)). We can split it into two steps: addWaiter(Node.EXCLUSIVE ): Wrap the thread into an exclusive node and add it to the queue. acquireQueued(node,arg): If the current node is the first waiting node, that is, head.next, try to acquire resources. If the method returns true, it will enter the logic of selfInterrupt() and block.

Next, let's take a look at the two methods addWaiter and acquireQueued.

Enter Node addWaiter (Node mode)

Determine the exclusive or shared mode according to the incoming mode parameter, create a node for the current thread, and join the team.

// 其实就是把当前线程包装一下,设置模式,形成节点,加入队列
	private Node addWaiter(Node mode) {
        // 根据mode和thread创建节点
        Node node = new Node(Thread.currentThread(), mode);
        // 记录一下原尾节点
        Node pred = tail;
        // 尾节点不为null,队列不为空,快速尝试加入队尾。
        if (pred != null) {
            // 让node的prev指向尾节点
            node.prev = pred;
            // CAS操作设置node为新的尾节点,tail = node
            if (compareAndSetTail(pred, node)) {
                // 设置成功,让原尾节点的next指向新的node,实现双向链接
                pred.next = node;
                // 入队成功,返回
                return node;
            }
        }
        // 快速入队失败,进行不断尝试
        enq(node);
        return node;
    }

A few points to note:

  • The operation of joining the queue is actually to pack the thread into a Node node through the specified mode. If the end node of the queue is not null, use CAS to try to quickly join the end of the queue.

There are two reasons for the failure of fast enrollment:

  • The queue is empty, that is, it has not been initialized yet.
  • CAS failed to set the tail node.
  • After the first quick entry failure, it will go to the enq(node) logic and keep trying until the setting is successful.

Constant test Node enq (final Node node)

private Node enq(final Node node) {
        // 自旋,俗称死循环,直到设置成功为止
        for (;;) {
            // 记录原尾节点
            Node t = tail;
            // 第一种情况:队列为空,原先head和tail都为null,
            // 通过CAS设置head为哨兵节点,如果设置成功,tail也指向哨兵节点
            if (t == null) { // Must initialize
                // 初始化head节点
                if (compareAndSetHead(new Node()))
                    // tail指向head,下个线程来的时候,tail就不为null了,就走到了else分支
                    tail = head;
            // 第二种情况:CAS设置尾节点失败的情况,和addWaiter一样,只不过它在for(;;)中
            } else {
                // 入队,将新节点的prev指向tail
                node.prev = t;
                // CAS设置node为尾部节点
                if (compareAndSetTail(t, node)) {
                    //原来的tail的next指向node
                    t.next = node;
                    return t;
                }
            }
        }
    }

The process of enq is the process of optional setting of the end of the queue. If the setting is successful, it will return. If the setting fails, try to set it all the time. The idea is that I can always wait for the day when the setting succeeds.

We can also find that the head is initialized lazily. When the first node tries to enqueue, the head is null. At this time, new Node() is used to create a node that does not represent any thread, as the virtual head node, and We need to note that its waitStatus is initialized to 0, which is instructive for our subsequent analysis.

If CAS failed and repeated attempts, let him continue CAS.

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

Dequeue void setHead(Node)

The CLU synchronization queue follows the FIFO. After the thread of the first node releases the synchronization state, the next node is awakened. The operation of dequeuing the head node of the team is actually just to point the head pointer to the node that will be dequeued.

private void setHead(Node node) {
        // head指针指向node
        head = node;
        // 释放资源
        node.thread = null;
        node.prev = null;
    }

boolean shouldParkAfterFailedAcquire(Node,Node)

/**
     * 走到这有两种情况 1.node不是第一个节点 2.tryAcquire争夺锁失败了
     * 这里就判断 如果当前线程争锁失败,是否需要挂起当前这个线程
     *
     * 这里pred是前驱节点, node就是当前节点
     */
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        // 前驱节点的waitStatus
        int ws = pred.waitStatus;
        // 前驱节点为SIGNAL【-1】直接返回true,表示当前节点可以被直接挂起
        if (ws == Node.SIGNAL)
            return true;
        // ws>0 CANCEL 说明前驱节点取消了排队
        if (ws > 0) {
            // 下面这段循环其实就是跳过所有取消的节点,找到第一个正常的节点
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            // 将该节点的后继指向node,建立双向连接
            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.
             * 官方说明:走到这waitStatus只能是0或propagate,默认情况下,当有新节点入队时,waitStatus总是为0
             * 下面用CAS操作将前驱节点的waitStatus值设置为signal
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        // 返回false,接着会再进入循环,此时前驱节点为signal,返回true
        return false;
    }

There are three cases of waitStatus for the precursor node:

The waiting state will not be Node.CONDITION because it is used in ConditonObject

  1. ws==-1, which is Node.SIGNAL, which means that the current node node can be directly suspended. When the pred thread releases the synchronization state, the node thread will be awakened.
  2. ws> 0, which is Node.CANCELLED, indicating that the precursor node has cancelled the queue [may be timed out, may be interrupted], you need to find the precursor node that has not been cancelled before, and keep searching until it is found.
  3. ws == 0 or ws == Node.PROPAGATE:
  4. By default, when a new node enters the queue, waitStatus is always 0. Use the CAS operation to set the waitStatus value of the precursor node to signal. The next time it comes in, it will go to the first branch.
  5. When the lock is released, the ws status of the node occupying the lock will be updated to 0.

PROPAGATE indicates that in the shared mode, the predecessor node will not only wake up the successor node, but may also wake up the successor.

We can find that this method will not return true the first time you walk in. The reason is that when the condition of returning true is the state of the precursor node is SIGNAL, and the SIGNAL has not been set for the precursor node the first time, only after the CAS has set the state, the second entry will return true.

So what is the meaning of SIGNAL?

Reference here: Concurrent Programming-Detailed AQS CLH 锁# Why does AQS need a virtual head node waitStatus is abbreviated as ws here, and each node has a ws variable to indicate the status of the node. When initialized, it is 0. If it is cancelled, the signal is -1. If the state of a node is signal, then when the node releases the lock, it needs to wake up the next node. Therefore, before each node sleeps, if the ws of the predecessor node is not set to signal, it will never be awakened. Therefore, we will find that when the ws of the current drive node above is 0 or propagate, use the cas operation to set ws to signal, so that the previous node can notify itself when the lock is released.

boolean parkAndCheckInterrupt ()

private final boolean parkAndCheckInterrupt() {
        // 挂起当前线程
        LockSupport.park(this);
        return Thread.interrupted();
    }

After the shouldParkAfterFailedAcquire method returns true, this method will be called to suspend the current thread.

The thread suspended by the LockSupport.park(this) method can be awakened in two ways: 1. by unpark() 2. by interrupt().

Note that Thread.interrupted() here will clear the interrupt flag bit.

void cancelAcquire(node)

When the above tryAcquire fails to acquire the lock, it will come to this method.

private void cancelAcquire(Node node) {
        // Ignore if node doesn't exist
        if (node == null)
            return;
		// 将节点的线程置空
        node.thread = null;

        // 跳过所有的取消的节点
        Node pred = node.prev;
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;

        // predNext is the apparent node to unsplice. CASes below will
        // fail if not, in which case, we lost race vs another cancel
        // or signal, so no further action is necessary.
        // 这里在没有并发的情况下,preNext和node是一致的
        Node predNext = pred.next;

        // Can use unconditional write instead of CAS here. 可以直接写而不是用CAS
        // After this atomic step, other Nodes can skip past us.
        // Before, we are free of interference from other threads.
        // 设置node节点为取消状态
        node.waitStatus = Node.CANCELLED;

        // 如果node为尾节点就CAS将pred设置为新尾节点
        if (node == tail && compareAndSetTail(node, pred)) {
            // 设置成功之后,CAS将pred的下一个节点置为空
            compareAndSetNext(pred, predNext, null);
        } else {
            // If successor needs signal, try to set pred's next-link
            // so it will get one. Otherwise wake it up to propagate.
            int ws;
            if (pred != head && // pred不是首节点
                ((ws = pred.waitStatus) == Node.SIGNAL || // pred的ws为SIGNAL 或 可以被CAS设置为SIGNAL
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                pred.thread != null) { // pred线程非空
                // 保存node 的下一个节点
                Node next = node.next; 
                // node的下一个节点不是cancelled,就cas设置pred的下一个节点为next
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next);
            } else {
                // 上面的情况除外,则走到这个分支,唤醒node的下一个可唤醒节点线程
                unparkSuccessor(node);
            }

            node.next = node; // help GC
        }
    }

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

  • tryRelease(int) is a hook method provided by AQS to subclasses. Subclasses can customize the way to release resources exclusively. The release succeeds and returns true, otherwise it returns false.
  • The unparkSuccessor(node) method is used to wake up the next thread that can be awakened in the waiting queue, not necessarily the next node next, for example, it may be in a canceled state.
  • The ws of head must not be equal to 0, why? When a node tries to suspend itself, it will set the predecessor node to SIGNAL -1. Even if it is the first node to join the queue, after failing to acquire the lock, the ws set by the virtual node will be set to SIGNAL, and this The judgment is to prevent multiple threads from repeatedly releasing. Then we can also see the operation of setting ws to 0 when releasing.

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

Original link: https://www.cnblogs.com/summerday152/p/14244324.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/112341438