JUC--AQS源码分析(三)阻塞和唤醒线程

1 概述

上一篇文章 JUC–AQS源码分析(二)同步状态的获取与释放,我们学习到了同步状态的获取与释放的源码,并且对线程的阻塞和唤醒有了一个初步的了解,这里我们进行深一步的分析。

2 阻塞

我们知道在获取线程同步状态失败的时候,会将线程加入到CLH同步队列,并且进行自旋等待。而在自旋等待方法acquireQueued中我们可以看见需要再次进行获取同步状态,如果获取同步状态失败则需要判断当前线程是否能够被阻塞,并且进行线程阻塞后检测中断状态。

//如果线程能够被阻塞就阻塞线程并且返回线程的中断状态。
if (shouldParkAfterFailedAcquire(p, node) &&
    parkAndCheckInterrupt())
    interrupted = true;

接下来我们来学习shouldParkAfterFailedAcquire和parkAndCheckInterrupt着两个方法到底是怎么回事。

2.1 shouldParkAfterFailedAcquire判断是否阻塞

首先直接上源码,进行源码分析。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        
        //前驱节点状态
        int ws = pred.waitStatus;

        //状态为SIGNAL,表示当前节点处于等待状态,直接返回true。
        if (ws == Node.SIGNAL)
            return true;

        //状态大于0,则为CANCEL,表示该节点已经超时,或者被中断,需要CLH队列中删除。
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;

        //前驱节点状态为CONDITION、PROPAGATE
        } else {
            

            //CAS设置状态为SIGNAL
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

上面这段代码主要的作用就是检查当前线程是否需要被阻塞,具体规则如下:

(1)如果前驱节点状态为SIGNAL,则表明当前线程需要被阻塞,则直接返回true。

(2)如果前驱节点状态为CANCEL,则表明该节点的前驱节点已经超时或者被中断,需要从同步队列中删除,直到回溯到前驱节点的状态<=0,返回false。

(3)如果前驱节点非SIGNAL,非CANCEL,则通过CAS方式将前驱节点设置成SIGNAL,返回false。

如果shouldParkAfterFailedAcquire返回true,则调用parkAndCheckInterrupt阻塞线程。

2.2 parkAndCheckInterrupt阻塞线程 

首先还是直接上源码。

private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

从上面的源码我们可以看出parkAndCheckInterrupt做的事就相对简单多了,直接阻塞当前线程,并且返回一个线程中断状态就行了。

3 唤醒

当线程释放同步状态后,就需要唤醒该线程的后继节点。调用unparkSuccessor方法。

private void unparkSuccessor(Node node) {
        //当前节点状态
        int ws = node.waitStatus;
        //当前状态 < 0 则设置为 0,删除当前节点
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        //当前节点的后继节点
        Node s = node.next;
        //后继节点为null或者其状态 > 0 (超时或者被中断了)
        if (s == null || s.waitStatus > 0) {
            s = null;
            //从tail节点来找可用节点
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        //唤醒后继节点
        if (s != null)
            LockSupport.unpark(s.thread);
    }

为何是从tail尾节点开始,而不是从node.next开始呢?由于node.next仍然可能会存在null或者取消了,所以采用tail回溯办法找第一个可用的线程。

上面就是对AQS阻塞和唤醒线程的学习,后面用一篇博文学习LockSupport。

    原文作者:JUC
    原文地址: https://blog.csdn.net/ONROAD0612/article/details/81772747
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞