JUC核心控件AQS源码解析第二部分(Condition条件队列)

AQS的条件队列是由AQS的内部类ConditionObject实现的,ConditionObject实现了Condition接口
而阻塞队列ArrayBlockingQueue是基于ConditionObject的
ConditionObject的实例要通过ReentrantLock产生

拿ArrayBlockingQueue的构造函数举例,可以看到最终是调用ReentrantLock的内部类Sync的newCondition()方法产生一个ConditionObject实例

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

public Condition newCondition() {
    
    
        return sync.newCondition();
 }

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

条件队列的作用是可以拿来实现生产者-消费者模型,简单看下ArrayBlockingQueue是怎样通过条件队列实现生产者-消费者模型的

//生产
 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();
        }
    }

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(); //生产了一个,发出队列不空的信号唤醒因为队列为空而被挂起的线程
    }

//消费
public E take() throws InterruptedException {
    
    
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
    
    
            while (count == 0)
            //队列空了,就先不消费,把线程挂起,等待其他线程发出队列不空的信号把线程唤醒
                notEmpty.await();
            return dequeue(); //消费一个
        } finally {
    
    
            lock.unlock();
        }
}

private E dequeue() {
    
    
        // 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.signal();
        return x;
    }

ConditionObject

简单了解了ArrayBlockingQueue之后就可以看回ConditionObject了

public class ConditionObject implements Condition, java.io.Serializable {
    
    
            private static final long serialVersionUID = 1173984872572414699L;
            /** 条件队列队头 */
            private transient Node firstWaiter;
            /** 条件队列队尾 */
            private transient Node lastWaiter;
 }

1、await()方法,把线程入条件队列,然后挂起线程

public final void await() throws InterruptedException {
    
    
               //先判断线程是否被中断了,被中断了就抛出中断异常
                if (Thread.interrupted())
                    throw new InterruptedException();
                 //线程入条件队列   
                Node node = addConditionWaiter(); 
                //完全释放锁
                int savedState = fullyRelease(node);
                //0 : 未中断
                //-1 : 表示在Condition队列挂起期间,接收到中断信号,即signal前被中断
                //1 : 在Condition队列挂起期间,未接收到中断信号,但是迁移到阻塞队列之后,接收到过中断信号,即signal后被中断,表示中断信号来得太晚了
                int interruptMode = 0;
                //不在同步队列while就为true
                while (!isOnSyncQueue(node)) {
    
    
                  //挂起线程
                    LockSupport.park(this);
                    //来到这个if表示线程已经被唤醒了,这个线程的前驱节点释放锁时才会唤醒这个线程,一般不是在signal()方法唤醒
                    //被唤醒后判断被挂起的时候是否被中断了
                    if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                        break;
                }
                //来到这里表明线程的已经被唤醒了,并且已经在同步队列等待获取锁了
                if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                    interruptMode = REINTERRUPT;
         // 考虑下 node.nextWaiter != null 条件什么时候成立呢?
        // 其实是node在条件队列内时 如果被外部线程 中断唤醒时,会加入到阻塞队列,但是并未设置nextWaiter = null。
                if (node.nextWaiter != null) // clean up if cancelled
                //清理条件队列里取消排队的节点
                    unlinkCancelledWaiters();
                 //处理中断状态,
                if (interruptMode != 0)
                    reportInterruptAfterWait(interruptMode);
            }

 private Node addConditionWaiter() {
    
    
                Node t = lastWaiter; //旧尾节点
                // 如果旧尾节点取消了排队就清理移出队列,被中断了
                if (t != null && t.waitStatus != Node.CONDITION) {
    
    
                    unlinkCancelledWaiters();
                    t = lastWaiter;
                }
                //把线程封装成Node
                Node node = new Node(Thread.currentThread(), Node.CONDITION);
                //队列为空
                if (t == null) 
                    firstWaiter = node;
                //旧尾节点指向新尾节点    
                else
                    t.nextWaiter = node;
                //更新尾节点    
                lastWaiter = node;
                return node;
}

//检查是否发生了中断
private int checkInterruptWhileWaiting(Node node) {
    
    
                return Thread.interrupted() ?
                    (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
                    0;
}

//这个方法只有线程被中断了才会调用,用于判断中断是否发生在signal之前,true表示
final boolean transferAfterCancelledWait(Node node) {
    
    
         //如果更新成功表示node还在条件队列里,因为signal把节点转到同步队列要把ws改为0
         //ws原来为0这个条件就不成立
            if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
    
    
            //即使被中断了取消了条件队列的排队,node还是会入同步队列,因为最终的目的都是为了获取锁
                enq(node);
                // true:表示是在条件队列内被中断的,即signal前
                return true;
            }
            // 执行到这里有几种情况?
          // 1.当前node已经被外部线程调用 signal 方法将其迁移到 阻塞队列内了。
            // 2.当前node正在被外部线程调用 signal 方法将其迁移至 阻塞队列中进行中状态,这种情况就要自旋等待直到node已经被迁移到阻塞队列
            while (!isOnSyncQueue(node))
                Thread.yield();
            return false;
}

//移除取消排队的节点
private void unlinkCancelledWaiters() {
    
    
                Node t = firstWaiter; //头结点
                Node trail = null;
                while (t != null) {
    
    
                    Node next = t.nextWaiter;//当前节点的下一节点
                    //说明当前节点取消了排队
                    if (t.waitStatus != Node.CONDITION) {
    
    
                    //清空后继节点
                        t.nextWaiter = null;
                        //还没遇到正常的节点,即还没遇到在排队的节点
                        if (trail == null)
                        //更新头结点
                            firstWaiter = next;
                        else
                        //正在排队的节点指向当前节点的下一节点,因为当前节点取消了排队,要移除出队列
                            trail.nextWaiter = next;
                        // 条件成立:当前节点为队尾节点了,更新lastWaiter 指向最后一个正常节点 就Ok了
                        if (next == null)
                            lastWaiter = trail;
                    }
                    //来到这里说明t是正在排队的节点,没被取消,赋值给trail
                    else
                        trail = t;
                    //下一节点赋值给t
                    t = next;
                }
}

//此方法是在await()的最后判断的
private void reportInterruptAfterWait(int interruptMode)
    throws InterruptedException {
    
    
    // 条件成立:说明在条件队列内发生过中断,此时await方法抛出中断异常
    if (interruptMode == THROW_IE)
        throw new InterruptedException();

    // 条件成立:说明在条件队列外发生的中断,此时设置当前线程的中断标记位 为true
    // 中断处理 交给 你的业务处理。只是跟你说发生了中断,不过要你自己去处理 如果你不处理,那什么事 也不会发生了...
    else if (interruptMode == REINTERRUPT)
        selfInterrupt();
}

整个await()方法的大概流程是:
1、先判断调用方法的线程有没被中断,被中断了就抛中断异常
2、把线程包装成Node节点加入条件队列
3、完全解锁
4、把线程挂起等待唤醒
5、如果被唤醒了就判断在条件队列等待的时候有没发生中断,有就退出循环,没有就判断是否在同步队列里,是就退出循环
6、因为已经在同步队列里,所以就可以竞争锁,如果竞争成功就判断是否是在条件队列等待时发生了中断
7、因为如果线程是因为中断被唤醒的话还没把条件队列的后继节点清空,所以要清空一下
8、最后处理中断,如果是在条件队列等待时发生了中断就抛出中断异常,如果不是条件队列等待时发生了中断就只是把中断标志设为true表示已经发生了中断不过要调用者自己去处理中断

2、isOnSyncQueue()方法,判断node是否在同步队列里

final boolean isOnSyncQueue(Node node) {
    
    
            //node.waitStatus为CONDITION表示node在条件队列里
            //prev不为null也不能说node在阻塞队列里,因为enq()方法先执行node.prev = t,执行之后node.prev不为空,但是还没cas更新尾节点,只有cas更新了尾节点node才入同步队列成功
            if (node.waitStatus == Node.CONDITION || node.prev == null)
                return false;
           //cas更新了尾节点node.next不为空,表明node一成功入队了
            if (node.next != null) // If has successor, it must be on queue
                return true;
         //从尾部开始找node是否在同步队列,node可能在执行signal,还没把node转移到同步队列
            return findNodeFromTail(node);
        }

3、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) {
    
    
            /*
             * 更新失败表示取消了排队返回false
             */
            if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
                return false;

           //节点入同步队列,p是旧尾节点,即node的前驱节点
            Node p = enq(node);
            //前驱节点ws
            int ws = p.waitStatus;
            //ws>0表示前驱节点取消了排队,直接唤醒当前线程 
            //cas更新失败表示前驱已经被其他线程中断了然后修改了ws,被中断了会出队,所以这种情况也是直接唤醒当前线程
            if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
                LockSupport.unpark(node.thread);
            return true;
        }

整个signal()方法的大概流程是:
1、判断是否持有锁,否的话就抛IllegalMonitorStateException
2、把条件队列的头结点转移到同步队列,如果头结点取消了排队就转移头结点的下一个节点
3、调用enq()方法把头结点从条件队列转移到同步队列
4、转移到同步队列后判断前驱节点是否取消了排队,是的话就直接唤醒当前节点
5、把前驱节点的ws更新为-1表示前驱结点释放锁时要唤醒当前节点,如果更新失败表明前驱节点被其他线程中断了然后修改了ws,被中断了会出队,这种情况也是直接唤醒当前线程
6、因为当前节点的唤醒需要依靠前驱结点的ws为-1时才能唤醒当前节点,如果前驱节点的ws不为-1,那么就要靠自己唤醒自己,如果不靠自己的话要继续往前找一个可以把ws更新为-1的节点

おすすめ

転載: blog.csdn.net/weixin_43478710/article/details/121515990