深入理解AQS的condition队列

系列文章目录


前言

上一篇讲了CLH队列,这一篇通过ArrayBlockingQueue的源码来讲以下condition队列

一、ArrayBlockingQueue的结构

先看一下构造方法

public ArrayBlockingQueue(int capacity, boolean fair) {
        if (capacity <= 0)
            throw new IllegalArgumentException();
        this.items = new Object[capacity];
        lock = new ReentrantLock(fair);
        notEmpty = lock.newCondition();
        notFull =  lock.newCondition();
    }

有一个数组items,一个可重入锁lock,两个Condition队列notEmpty和notFull

1. 数组

items:用于存放元素。


    /** The queued items */
    final Object[] items;

我们知道,ArrayBlockingQueue是一个基于数组结构的有界阻塞队列,按FIFO排序任务。items就锁用于存放元素的数组,那FIFO怎么实现呢?通过下面两个指针来实现

takeIndex:下一个要获取的元素数组坐标位置
putIndex:下一个元素要放入的数组坐标位置


    /** items index for next take, poll, peek or remove */
    int takeIndex;

    /** items index for next put, offer, or add */
    int putIndex;

并且这两个指针是循环移动的,如果指针达到最大坐标了,又会从0开始

关于数组的还有一个重要的属性,那就是count
count:表示队列中元素的个数。ArrayBlockingQueue会有很多地方用到对count数量的判断,后面会讲到


    /** Number of elements in the queue */
    int count;

2.可重入锁

ArrayBlockingQueue的一个重要的特性是在任意时刻只有一个线程可以进行获取或者放入操作,所以就需要一个锁来实现,ArrayBlockingQueue的获取和放入操作都需要获取到这个lock才能进行


    /** Main lock guarding all access */
    final ReentrantLock lock;

3.condition队列的结构

在这里插入图片描述

condition队列是一个单向链表,在该链表中我们使用nextWaiter属性来串联链表。但是,就像在同步队列中不会使用nextWaiter属性来串联链表一样,在条件队列是中,也并不会用到prev, next属性,它们的值都为null。

ArrayBlockingQueue中定义了2个condition队列,notEmpty和notFull
notEmpty:等待获取元素的condition队列。如果items数组空了,那么获取元素的线程就会进入该condition队列
notFull:等待放入元素的condition队列。如果items数组空了,那么放入元素的线程就会进入该condition队列


    /** Condition for waiting takes */
    private final Condition notEmpty;

    /** Condition for waiting puts */
    private final Condition notFull;

我们从构造方法中的lock.newCondition()看到,这个队列就是AQS中的ConditionObject


final ConditionObject newCondition() {
            return new ConditionObject();
        }

而ConditionObject有2个属性头节点指针和尾节点指针,如上面的图片显示

        /** First node of condition queue. */
        private transient Node firstWaiter;
        /** Last node of condition queue. */
        private transient Node lastWaiter;

二、ArrayBlockingQueue主要方法

1)public boolean add(E e):放入元素,放入成功了返回true,如果items满了,抛异常
2)public boolean offer(E e):放入元素,放入成功了返回true,如果items满了,返回false
3)public void put(E e):放入元素,如果items满了,阻塞
4)public boolean offer(E e, long timeout, TimeUnit unit):放入元素,放入成功了返回true,如果items满了,等待一段时间,超时后返回false
5)public E poll():获取元素,如果items空了,返回null
6)public E take():获取元素,如果items空了,阻塞
7)public E poll(long timeout, TimeUnit unit):获取元素,如果items空了,等待一段时间,超时后返回null

三、ArrayBlockingQueue源码解读

先看放入元素的几个方法,这几个的方法都比较相似,但是只有put(E e)和offer(E e, long timeout, TimeUnit unit)用到了条件队列,我们从这两个方法进行分析

1.put(E e)

public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == items.length)
                notFull.await();
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }

1)获取可中断锁,获取不到就进入CLH队列(上一篇讲过),获取到了就往下进行
2)循环判断,如果队列满了(这里的队列是指items数组),调用notFull.await()进入notFull队列,并阻塞。唤醒后再从头判断,如果队列满了,再入notFull队列
3)如果队列没有满,调用enqueue(e)方法入队,入队会唤醒notEmpty队列
4)解锁

先看下notFull.await()方法:

1.1 notFull.await()

public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            Node node = addConditionWaiter();
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                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);
        }

1)如果线程中断,抛中断异常
2)调用addConditionWaiter()方法加入到notFull队列
如果尾节点状态为取消,那么从头开始清除所有状态为取消的节点

private Node addConditionWaiter() {
            Node t = lastWaiter;
            // If lastWaiter is cancelled, clean out.
            if (t != null && t.waitStatus != Node.CONDITION) {
                unlinkCancelledWaiters();
                t = lastWaiter;
            }
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            if (t == null)
                firstWaiter = node;
            else
                t.nextWaiter = node;
            lastWaiter = node;
            return node;
        }

3)释放锁
前面说过put方法一开始就要获取锁,执行到这里肯定已经获取到锁了。但是因为此时已经无法入队了,并且后面线程还会阻塞,所以持有锁就不合适了,需要释放掉
4)自旋检查当前节点是否在CLH队列上,如果在则退出自旋,否则阻塞,直到被signal唤醒转移到CLH队列(或者被中断唤醒,这个单独再说)
5)到这里,已经在CLH队列上了。开始竞争锁
a. 如果获取到了,又开始下一轮的入队判断while (count == items.length)
b. 获取不到,那么一直阻塞,等待其他线程释放锁后被唤醒,进行下一轮锁竞争
(阻塞过程中也会被中断唤醒,这个也单独再说)

这就是notFull.await()方法的主要逻辑,我们再看下enqueue(e)方法:

1.2 enqueue(e)

private void enqueue(E x) {
        // assert lock.getHoldCount() == 1;
        // assert items[putIndex] == null;
        final Object[] items = this.items;
        items[putIndex] = x;
        if (++putIndex == items.length)
            putIndex = 0;
        count++;
        notEmpty.signal();
    }

逻辑很简单
1)将元素放入items数组,putIndex往前移,如果移到最大下标了就从0开始(之前都介绍过,这里是具体的实现),元素个数也加1
2)调用notEmpty.signal()唤醒notEmpty队列
所以我们要具体看下调用notEmpty.signal()方法

1.3 notEmpty.signal()

public final void signal() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignal(first);
        }
private void doSignal(Node first) {
            do {
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                first.nextWaiter = null;
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }
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;
    }

1)如果头节点不为空,唤醒头节点
2)头结点出condition队列,尝试唤醒头结点,如果失败(如果头结点的waitStatus不是CONDITION状态,说明节点已取消,那么就会失败),那么继续唤醒下一个节点
3)这样直到要唤醒的下一个节点是CONDITION状态,那么就将这个节点入到CLH队列,同时将CONDITION状态设置为SIGNAL状态,等待被唤醒(上一篇讲CLH队列说过,只有SIGNAL状态才能被唤醒)

这里虽然代码少,但是流程有点复杂,需要画图来展示一下流程,我们假设condition队列中有3个节点,t1节点的ws为1(CANCEL状态),t2节点和t3节点的ws都是-2(CONDITION状态),流程图如下:
在这里插入图片描述
主要流程应该就讲清楚了,当然还有特殊的流程,比如下面的这一段代码,并发导致的特殊情况,就不深究了

if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);

2.offer(E e, long timeout, TimeUnit unit)

public boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException {

        checkNotNull(e);
        long nanos = unit.toNanos(timeout);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == items.length) {
                if (nanos <= 0)
                    return false;
                nanos = notFull.awaitNanos(nanos);
            }
            enqueue(e);
            return true;
        } finally {
            lock.unlock();
        }
    }

1)获取可中断锁,获取不到就进入CLH队列(上一篇讲过),获取到了就往下进行
2)循环判断,如果队列满了(这里的队列是指items数组),调用notFull.awaitNanos(nanos)方法进入notFull队列,并阻塞。达到超时时间后自动唤醒,然后返回false
3)如果队列没有满,调用enqueue(e)方法入队,入队会唤醒notEmpty队列
4)解锁
是不是与put方法基本一样,唯一不一样的地方就是当队列满了时,调用的是notFull.awaitNanos(nanos)方法,并且超时唤醒后直接返回false,不会像put方法一样继续入condition队列。
所以我们重点看一下notFull.awaitNanos(nanos)方法

2.1 notFull.awaitNanos(nanos)

public final long awaitNanos(long nanosTimeout)
                throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            Node node = addConditionWaiter();
            int savedState = fullyRelease(node);
            final long deadline = System.nanoTime() + nanosTimeout;
            int interruptMode = 0;
            while (!isOnSyncQueue(node)) {
                if (nanosTimeout <= 0L) {
                    transferAfterCancelledWait(node);
                    break;
                }
                if (nanosTimeout >= spinForTimeoutThreshold)
                    LockSupport.parkNanos(this, nanosTimeout);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
                nanosTimeout = deadline - System.nanoTime();
            }
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null)
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
            return deadline - System.nanoTime();
        }

与notFull.await()方法类似
1)如果线程中断,抛中断异常
2)调用addConditionWaiter()方法加入到notFull队列
3)释放锁
4)自旋检查当前节点是否在CLH队列上,如果在则退出自旋,否则
a.如果已经超时了,将节点转移到CLH队列上
b.如果没超时,并且剩余时间≥spinForTimeoutThreshold(默认1s),那么就park阻塞指定时间。如果剩余时间<spinForTimeoutThreshold,那么就不阻塞,继续自旋(因为parkNanos也是耗时间耗性能的,park时间太短了不划算)
阻塞期间,可以被signal唤醒或者被中断唤醒转移到CLH队列(这个也单独再说)
5)到这里,已经在CLH队列上了。开始竞争锁
a. 如果获取到了,又开始下一轮的入队判断while (count == items.length)
b. 获取不到,那么一直阻塞,等待其他线程释放锁后被唤醒,进行下一轮锁竞争
(阻塞过程中也会被中断唤醒,这个也单独再说)

3. 阻塞过程中被中断唤醒

前面一直留着一个问题,就是在notFull.await()和notFull.awaitNanos(nanos)方法入condition队列后被中断唤醒会怎么样,这里单独分析一下

涉及的代码如下:

while (!isOnSyncQueue(node)) {
	...
	//判断是否被中断,如果被中断,判断中断是发生在signal唤醒之前还是之后
	if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
	......
//如果前面没有中断,acquireQueued时被中断了
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;    
...
if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);                    

先来看第一个判断
如果checkInterruptWhileWaiting返回不为0(被中断了),那么就退出循环

checkInterruptWhileWaiting方法如下:

private int checkInterruptWhileWaiting(Node node) {
            return Thread.interrupted() ?
                (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
                0;
        }

判断是否被中断,如果被中断,将节点转移到CLH队列,同时判断中断是发生在signal唤醒之前还是之后
a.如果发生在signal唤醒之前,interruptMode为THROW_IE(需要抛异常)
b.如果发生在signal唤醒之后,interruptMode为REINTERRUPT(需要重新标记中断)

再来看第二个判断
如果前面在condition队列中没有中断,但是在CLH队列中竞争锁时被中断了(acquireQueued在将CLH队列那篇文章讲过,被中断后返回true),将interruptMode也设为REINTERRUPT(需要重新标记中断)

最后看第三个判断
a.如果interruptMode为THROW_IE,抛中断异常
b.如果interruptMode为REINTERRUPT,重新标记中断(因为在判断是否中断的时候将中断标记清除了)

小结
a.在condition队列阻塞过程中,被中断唤醒了,如果中断发生在signal唤醒之前,那么在转移到CLH队列后抛异常,如果中断发生在signal唤醒之后,那么在转移到CLH队列后只需要重新标记中断
b.在condition队列阻塞过程中没有被中断,而是在CLH队列中竞争锁的过程中,被中断唤醒了,只需要重新标记中断

ps:为什么这么区分,个人理解,如果中断发生在signal唤醒之后,即使没有中断节点也会因为signal而转移到CLH队列,所以只需要重新标记中断,让调用者自己处理。如果中断发生在signal唤醒之前,打乱这个阻塞-唤醒的流程,所以最后要抛异常(个人理解,可能不对,不必深究)

4.take()和poll(long timeout, TimeUnit unit)

分析完放入元素的两个方法,其实获取元素的两个方法take()和poll(long timeout, TimeUnit unit)就不展开分析了,因为流程完全一致,只不过是反着来的,队列空了获取不到元素就入notFull队列,获取到了元素就唤醒notEmpty队列。


总结

1.condition队列是一个单向队列,通过头节点指针firstWaiter和尾节点指针lastWaiter来维护
2.分析了put(E e)方法:先获取ReentrantLock,再放入元素,如果队列满了,入notEmpty队列并阻塞,直到被其他线程调用notEmpty.signal唤醒;如果队列没满,入队成功了,调用notFull.signal唤醒notFull队列
3.分析了offer(long timeout, TimeUnit unit)方法:先获取ReentrantLock,再放入元素,如果队列满了,入notEmpty队列并阻塞一段时间,直到超时或者被其他线程调用notEmpty.signal唤醒;如果队列没满,入队成功了,调用notFull.signal唤醒notFull队列

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值