深入浅出AQS条件队列以及阻塞队列BlockingQueue

文章目录

  • 前言
  • AQS中的条件队列
  • BlockingQueue的结构
  • ArrayBlockingQueue源码
    • 生产者
      • put
      • notFull.await()
    • 消费者
      • take
  • 图解
  • 总结

前言

之前讲过独占共享模式下Node节点的waitStatus信号量还有一个CONDITION = -2;没有说,并且AQS中还有一个ConditionObject内部类没有提到和条件队列下使用到的一些方法

AQS中的条件队列

static final class Node {
        /**
         *  标记节点为独占模式
         */
        static final Node EXCLUSIVE = null;
   
        /**
         * 出现异常,中断引起的,需要废弃的node即节点. 中断一般是手动,程序异常通常是代码运行中问出题
         * 在同步队列中等待的线程等待超时或者被中断,需要从同步队列中取消等待
         * */
        static final int CANCELLED =  1;
      
        /**
         *  可被唤醒
         *  同步队列需要通道的状态
         *  后继节点的线程处于等待状态,而当前的节点如果释放了同步状态或者被取消,
         *  将会通知后继节点,使后继节点的线程得以运行。
         */
        static final int SIGNAL    = -1;
        /**
         *  条件等待
         *  条件队列需要用到的状态
         *  节点在等待队列中,节点的线程等待在Condition上,当其他线程对Condition调用了signal()方法后,
         *  该节点会从等待队列中转移到同步队列中,加入到同步状态的获取中
         */
        static final int CONDITION = -2;
         /**
         * 标记当前节点的信号量状态 (1,0,-1,-2,-3)5种状态
         * 使用CAS更改状态,volatile保证线程可见性,高并发场景下,
         * 即被一个线程修改后,状态会立马让其他线程可见。
         */
        volatile int waitStatus;
          /**
         * 节点同步状态的线程
         */
        volatile Thread thread;
  		/**
         *  PS:条件队列Node节点的下一个指针(单向链表)头尾指针在@See ConditionObject
         *      构建条件队列使用到的参数,条件队列必须是独占的
         *
         * 等待队列中的后继节点,如果当前节点是共享的,那么这个字段是一个SHARED常量,
         * 也就是说节点类型(独占和共享)和等待队列中的后继节点共用同一个字段。
         */
        Node nextWaiter;
}
	/**
	* ConditionObject 的作用是构建条件队列的头尾指针,以及两个重要的方法
	*/
	public class ConditionObject implements Condition {
 
        /**
         * 条件队列头指针
         */
        private transient Node firstWaiter;
       
        /**
         * 条件队列尾部指针
         */
        private transient Node lastWaiter;
		//条件队列的唤醒
		public final void signal() {}
     	//条件队列的阻塞
        public final void await() throws InterruptedException {}
		 
		//其他一些用到的重要方法
		final boolean transferForSignal(Node node)
     }

       

上面是构建条件队列和使用条件队列的关键处,条件队列的作用简单说可以是用来作为CLH队列的中间过度的,如果阻塞队列满了,就没办法put了,那么put的线程就会先放入条件队列阻塞。当被触发条件调用signal后,条件队列的节点全部移动到CLH队列,并且唤醒。

BlockingQueue的结构

通过构造器,认识阻塞队列的内部结构

 public ArrayBlockingQueue(int capacity, boolean fair) {
        if (capacity <= 0)
            throw new IllegalArgumentException();
        this.items = new Object[capacity];  // 阻塞队列容量大小
        lock = new ReentrantLock(fair);     // lock锁对象->clh队列
        notEmpty = lock.newCondition();     //条件对象->条件队列
        notFull =  lock.newCondition();     //条件对象-条件队列
    }

用消费者和生产者的角色描述简单的执行流程

深入浅出AQS条件队列以及阻塞队列BlockingQueue_第1张图片

ArrayBlockingQueue源码

生产者

put

/**
     * 生产者put
     * @param e
     * @throws InterruptedException
     */
    public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        // 获取锁,如果中断抛出异常
        lock.lockInterruptibly();
        try {
            // 容量满了
            while (count == items.length)
                //由于满足条件无法在放入元素,当前线程入条件队列,并且释放锁,然后唤醒CLH队列,然后当前线程阻塞,直到当前节点被加入到同步队列中,然后死循环在等待获取锁哪里
                notFull.await();
            // 放入item数组,唤醒消费者条件队列的线程
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }

notFull.await()

  /**
         * 加入条件队列等待,条件队列入口
         */
        public final void await() throws InterruptedException {
            //中断状态检测,如果当前线程被中断则直接抛出异常
            if (Thread.interrupted())
                throw new InterruptedException();
            // 构建waitStatus=Node.condition的节点并且加入条件队列
            Node node = addConditionWaiter();
            //释放掉put,take加的锁,唤醒CLH队列第一个节点
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            // 判断节点是否在同步队列?不再则一直阻塞,或者中断退出循环。 这里是因为items容器每次添加删除完都会调用ConditionObject.signal方法会把条件队列的节点添加到CLH队列中。
            while (!isOnSyncQueue(node)) {
                // 节点在条件队列中会一直阻塞在这里
                LockSupport.park(this);
                //如果是中断唤醒的也跳出循环
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            //走到这里说明节点已经条件满足被加入到了同步队列中或者中断了
            //这个方法很熟悉吧?就跟独占锁调用同样的获取锁方法,从这里可以看出条件队列只能用于独占锁,这里加完锁,在外面unlock释放
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            //走到这里说明已经成功获取到了独占锁,接下来就做些收尾工作
            //删除条件队列中被取消的节点
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            //根据不同模式处理中断
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

消费者

take

public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        // 加锁
        lock.lockInterruptibly();
        try {
            // 容器空了,消费者线程无法消费
            while (count == 0)
                // 和put一样同一个await方法,里面入条件队列,释放掉这里加的锁,然后阻塞在while循环,等待enqueue里的signal方法唤醒去获取锁
                notEmpty.await();
            return dequeue();
        } finally {
            // 释放锁
            lock.unlock();
        }
    }
 private E dequeue() {
        // 返回items末尾元素
        // assert lock.getHoldCount() == 1;
        // assert items[takeIndex] != null;
        final Object[] items = this.items;
        @SuppressWarnings("unchecked")
        E x = (E) items[takeIndex];
        items[takeIndex] = null;
        if (++takeIndex == items.length)
            takeIndex = 0;
        count--;
        if (itrs != null)
            itrs.elementDequeued();
        // 把生产者(notFull)这条条件队列放到CLH队列中
        notFull.signal();
        return x;
    }
 public final void signal() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            // 拿到条件队列头指针
            Node first = firstWaiter;
            if (first != null)
                // 从头唤醒条件队列(入CLH队列,unpark线程),然后在await中了while循环判断中满足条件跳出循环。
                doSignal(first);
        }
        
 		/**
         * 该方法就是把一个有效节点从条件队列中删除并加入同步队列
         * 如果失败则会查找条件队列上等待的下一个节点直到队列为空
         * @param 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) {
        //修改节点状态,这里如果修改失败只有一种可能就是该节点被取消,具体看上面await过程分析
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;
        //该方法很熟悉了,跟独占锁入队方法一样,不赘述
        Node p = enq(node);
        //注:这里的p节点是当前节点的前置节点
        int ws = p.waitStatus;
        //如果前置节点被取消或者修改状态失败则直接唤醒当前节点
        //此时当前节点已经处于同步队列中,唤醒会进行锁获取或者正确的挂起操作
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }

图解

太大了,不好截取
https://www.processon.com/diagraming/5f5e3f28f346fb47ca9fa7bf

总结

前言和AQS的条件队列的结构,差不多是一个简单面上流程的总结,再复杂点,多线程运行环境下的流程,可以看下上面图解。我觉得条件队列是AQS中最难的一块,因为很多代码涉及到多个线程切换的场景,但是学好了很有用,生产消费场景基本都是用这一套思路。

你可能感兴趣的:(JUC,java,多线程,并发编程,分布式,队列)