Java之AQS(二)

AQS


前言

在前一篇文章,我们分析了AQS入队的方式( addWaiter)及入队后,对队列中每个节点进行自旋+CAS获取独占锁的操作(acquireQueued)。而本文将分析Condition下的队列,又称为条件队列。

AQS中的队列分为同步队列和条件队列两种,这两种队列即相互独立又藕断丝连。 本文接下来将分析条件队列是如何新建、又是如何和同步队列藕断丝连的。


如何使用

按照"国际惯例",这里先给出一个例子:在例子中,我们通过lock.newCondition()创建了一个conditionNull 的队列,表示队列LinkedList为空的条件队列。

当我们试图调用take方法的时候,如果LinkedList为空,我们的conditionNull.await()试图将当前线程挂起。

当我们调用put方法的时候,我们又试图通过条件队列conditionNull.signal();唤醒先前调用take方法时,发现队列元素为空而被阻塞的线程。

这里可能就会有人有疑问了,我在执行put操作时,已经是占有锁了,此时我唤醒别的线程,还没释放自己占用的锁,其它线根本就不可能占据。那唤醒的线程到底何去何从呢?这里的的"去",指的就是同步队列了,"从"指的是就是我们的条件队列。

cpp 复制代码
 /***
 *
 * @Author:fsn
 * @Date: 2020/4/5 16:32
 * @Description
 */


public class BlockingQueue<E> {
    private Logger log = LoggerFactory.getLogger(BlockingQueue.class);
    private LinkedList<E> linkedList = new LinkedList<>();
    private Lock lock = new ReentrantLock();
    private Condition conditionNull = lock.newCondition();

    public void put(E e) {
        if (e == null || e.equals("")) {
            throw  new RuntimeException("添加的元素不能为空");
        }
        lock.lock();
        linkedList.add(e);
        conditionNull.signal();
        lock.unlock();
    }

    public E take() {
        lock.lock();
        if (linkedList.isEmpty()) {
            try {
                log.info("线程名称 {} . 当前元素为空", Thread.currentThread().getName());
                conditionNull.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        E e = linkedList.remove();
        lock.unlock();
        return e;
    }
}

await

从上述例子中,我们可以知道,条件队列的创建的奥妙就在于await方法中。那await到底做了件什么事?如下源码所示,

cpp 复制代码
public final void await() throws InterruptedException {
            // 如果当前线程被中断过, 则直接抛出中断异常
            if (Thread.interrupted())
                throw new InterruptedException();
            // 封装当前线程, 并扔到条件队列中
            Node node = addConditionWaiter();
            // 完全释放当前线程占用的锁, 并保存释放前(即当前)的锁状态
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            // 如果当前节点(封装好的线程)不在同步队列中
            // 说明还没有被signal过
            while (!isOnSyncQueue(node)) {
                // 挂起
                LockSupport.park(this);
                // 讲道理, 上面执行后不应该执行到这里的
                // 当如果线程被中断了、或者被signal了, 
                // 则会跑到这里
                // 检查唤醒的原因, 如果是中断则跳出循环
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            // 下面这部分代码先不用管
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

addConditionWaiter

上述源码中,addConditionWaiter的作用是封装当前线程, 并扔到条件队列中,它的源码如下所示, 这里我们可以回顾上一篇文章分析addWaiter的时候,它是需要依赖自旋+CAS来入队的,这里之所以不用,是因为进入await的时候就需要事先加锁了,这里可能有巨细的人说,那我偏偏不加呢?还有一个问题,为什么这里需要判断如果尾结点不为null且不为等待状态,就清除被cancel节点呢?这里可以接着往下看~

回顾一下AQS(一)中提到的waitStatus,等待状态的取值情况如下所示:

  1. CANCELLED :1 表明一个等待的线程被取消了
  2. SIGNAL : -1 表明一个等待线程的下一个线程需要被唤醒
  3. CONDITION : -2 当前线程正在等待中
  4. PROPAGATE :-3 下一次的acquire方法应该被无条件的传播
  5. 0:初始值
cpp 复制代码
private Node addConditionWaiter() {
            Node t = lastWaiter;
            // If lastWaiter is cancelled, clean out.
            // 如果尾结点不为null且不为等待状态
            if (t != null && t.waitStatus != Node.CONDITION) {
                // 遍历链表清除被cancel的节点
                unlinkCancelledWaiters();
                t = lastWaiter;
            }
            // 包装当前线程、扔进条件队列
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            if (t == null)
                // 初始化
                firstWaiter = node;
            else
                // 通过nextWaiter进行连接
                t.nextWaiter = node;
            lastWaiter = node;
            // 返回包装好的节点
            return node;
        }

fullyRelease

进入条件队列后,执行这行代码 int savedState = fullyRelease(node);我们需要释放当前线程的锁,注意这里是完全释放的意思,对于重入锁来说,无论你重入几次,在这里都要全部释放,并且 释放前需要保存状态,以便恢复的时候使用。

cpp 复制代码
final int fullyRelease(Node node) {
        boolean failed = true;
        try {
            // 释放之前保存状态
            int savedState = getState();
            if (release(savedState)) {
                failed = false;
                return savedState;
            } else {
                throw new IllegalMonitorStateException();
            }
        } finally {
            if (failed)
                node.waitStatus = Node.CANCELLED;
        }
    }

我们可以看看release的逻辑,这里又会调用tryRelease方法,而AQS中是没有对这方法进行实现,以重入锁为例,它重写了这个方法:

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

OK,看到这句代码,我觉得我们可以回答上面提到的第一个问题了, Thread.currentThread() != getExclusiveOwnerThread())即addConditionWaiter没有通过CAS,也能让程序正常运行,如果你不在外面加锁,在await方法中,会释放线程占有的锁,而真正执行释放操作的代码逻辑中会判断线程是否为独占的线程,否则就抛出异常IllegalMonitorStateException。

而fullyRelease代码模块中,执行finally 代码块时, node.waitStatus = Node.CANCELLED这句代码将被执行,也就是它会将当前线程的等待状态设置为取消,而AQS中并没有立刻清除这些状态为取消的节点,而是等到下次调用addConditionWaiter方法时,判断尾部节点是否为等待状态,不是的话就遍历链表清除被cancel的节点。

cpp 复制代码
  protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

isOnSyncQueue

接下来就是下面这段代码了,interruptMode 可以先别管,我们先分析isOnSyncQueue方法,它会判断当前线程是否在同步队列。

cpp 复制代码
 			int interruptMode = 0;
            // 如果当前节点(封装好的线程)不再同步队列中
            // 说明还没有被signal过, signal的线程会移动到同步队列中
            while (!isOnSyncQueue(node)) {
                // 挂起
                LockSupport.park(this);
                // 讲道理, 上面执行后不应该执行到这里的
                // 当如果线程被中断了、或者被signal了, 
                // 则会跑到这里
                // 检查唤醒的原因, 如果是中断则跳出循环
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }

isOnSyncQueue源码比较简单,关键的遍历逻辑放在findNodeFromTail了方法了.

😉 findNodeFromTail 😃 重点来了,在findNodeFromTail代码中,我们的等待队列和同步队列第一次出现出现联系了。

cpp 复制代码
final boolean isOnSyncQueue(Node node) {
        if (node.waitStatus == Node.CONDITION || node.prev == null)
            return false;
        if (node.next != null) // If has successor, it must be on queue
            return true;
        return findNodeFromTail(node);
    }

 /**
     * Returns true if node is on sync queue by searching backwards from tail.
     * Called only when needed by isOnSyncQueue.
     * @return true if present
     */
    private boolean findNodeFromTail(Node node) {
        Node t = tail;
        for (;;) {
            if (t == node)
                return true;
            if (t == null)
                return false;
            t = t.prev;
        }
    }

findNodeFromTail代码中通过一个死循环,从尾部向前遍历同步队列,寻找节点。这个同步队列就是我们在分析addWaiter的时候已经讲过,它会通过自旋的方式确保每个线程都能入队。那问题来了,我们的节点什么时候会进入到同步队列中呢?


signal

为了回答上个问题,我们可以先来看看signal方法,它的源码如下所示:它先判断当前线程是否具有锁即独占线程,然后将firstWaiter赋给first,其中firstWaiter 就是我们 addConditionWaiter方法创建条件队列的时候,初始化节点后所赋值的节点。

cpp 复制代码
 public final void signal() {
            
            // 是否为独占线程
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                // 唤醒第一个节点
                doSignal(first);
        }

doSignal

唤醒之前,我们需要重置一下firstWaiter 节点,具体的唤醒操作放在了transferForSignal。

cpp 复制代码
 /**
         * Removes and transfers nodes until hit non-cancelled one or
         * null. Split out from signal in part to encourage compilers
         * to inline the case of no waiters.
         * @param first (non-null) the first node on condition queue
         */
        private void doSignal(Node first) {
            do {
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                first.nextWaiter = null;
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }

transferForSignal

终于到了具体的唤醒步骤了,在这个步骤,我们企图通过compareAndSetWaitStatus方法,将节点对应的线程从等到状态恢复到初始状态0(该状态处于唤醒线程、但还没有参与到锁竞争的状态中),如果设置为初始状态不成功,说明该线程被取消(中断)了。

否则,我们将进入enq队列,关于enq队列,我们已经在上篇文章中陈述过,它通过自旋+CAS的方式确保进入同步队列能够成功。

进入同步队列不就意味着你能立刻得到锁,所以有了接下来的判断,这里单独拎出来,根据前面对等待状态的描述,大于0的状态只有cancel一种,而compareAndSetWaitStatus则是试图将唤醒的线程的状态通过CAS转为SIGNAL,该状态表示的意思一个等待线程的下一个线程需要被唤醒。

而关于这句代码 Node p = enq(node) 我们要注意的是,它返回的是当前节点的前驱节点。这也就解释了接下来的操作。

cpp 复制代码
  if (ws > 0 || 
  !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
cpp 复制代码
 final boolean transferForSignal(Node node) {
        /*
         * If cannot change waitStatus, the node has been cancelled.
         */
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;

        /*
         * Splice onto queue and try to set waitStatus of predecessor to
         * indicate that thread is (probably) waiting. If cancelled or
         * attempt to set waitStatus fails, wake up to resync (in which
         * case the waitStatus can be transiently and harmlessly wrong).
         */
        Node p = enq(node);
        int ws = p.waitStatus;
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }

这句代码中,我们通过判断前驱节点等待状态是否大于0(取消)或者尝试将前驱节点设置为SIGNAL状态(同步队列中的节点靠前驱节点去唤醒),当ws大于0或者设置SIGNAL不成功,可以将前驱视为被取消了,此时我们可以LockSupport唤醒当前线程了。

cpp 复制代码
 if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))

说到这里,我们可以回答这个问题了 我们的节点什么时候会进入到同步队列中呢?当某个ConditionObject对象调用signal()方法的时候(signalAll差不多),条件队列中的节点会进入到同步队列中,但真正执行进入的操作还是在enq方法。


总结

本文分析了同步队列的节点如何创建条件队列的,又是如何唤醒等待队列,而被唤醒的等待队列节点又是如何跑到同步队列的。

同步队列即CLH队列、双向队列,根据FIFO的原则,结合上一篇文章分析的addWaiter方法,我们可以知道头结点总是虚拟节点,头结点之后的节点会先获取独占锁,获取之后,如果此时来一个ConditionObject对象用了 await方法。那么会在对应的条件队列后边插入一个节点、并释放当前的独占锁。

如果获取独占锁的线程里,一个ConditionObject对象用了 signal方法,那么此时会将对应的条件队列的对头移至同步队列的队尾处。


结束语

似乎还有什么没有讲到的样子。。如果你已察觉到,恭喜盲生发现了华点了!就是下面这坨还没有说到,欢迎收看Java之AQS(三)

cpp 复制代码
  // 讲道理, 上面执行后不应该执行到这里的
                // 当如果线程被中断了、或者被signal了, 
                // 则会跑到这里
                // 检查唤醒的原因, 如果是中断则跳出循环
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            // 下面这部分代码先不用管
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
相关推荐
尘浮生6 分钟前
Java项目实战II基于微信小程序的电影院买票选座系统(开发文档+数据库+源码)
java·开发语言·数据库·微信小程序·小程序·maven·intellij-idea
不是二师兄的八戒29 分钟前
本地 PHP 和 Java 开发环境 Docker 化与配置开机自启
java·docker·php
爱编程的小生41 分钟前
Easyexcel(2-文件读取)
java·excel
带多刺的玫瑰1 小时前
Leecode刷题C语言之统计不是特殊数字的数字数量
java·c语言·算法
计算机毕设指导62 小时前
基于 SpringBoot 的作业管理系统【附源码】
java·vue.js·spring boot·后端·mysql·spring·intellij-idea
Gu Gu Study2 小时前
枚举与lambda表达式,枚举实现单例模式为什么是安全的,lambda表达式与函数式接口的小九九~
java·开发语言
Chris _data2 小时前
二叉树oj题解析
java·数据结构
牙牙7052 小时前
Centos7安装Jenkins脚本一键部署
java·servlet·jenkins
paopaokaka_luck2 小时前
[371]基于springboot的高校实习管理系统
java·spring boot·后端
以后不吃煲仔饭2 小时前
Java基础夯实——2.7 线程上下文切换
java·开发语言