浅析AQS (二)--condition的实现

212 阅读6分钟

在前一章节中,我们简单分析过aqs中加锁以及阻塞的流程,这一章我们来分析一下condition条件阻塞工具的实现

什么是condition

condition是作为条件阻塞器,通过调用await,signal和signalAll方法来阻塞和唤醒线程,可以横向对比的是Object对象的wait,notify以及notifyAll方法,值得注意的是,与Object的wait需要跟synchronized结合使用一样,condition也需要跟锁结合使用,比如ReenTrantLock中的newCondition方法就是创建一个全新的条件阻塞器,而调用await方法也需要通过lock进行加锁才可以正常使用.

condition.await与Object.wait的区别

  • 首先Object相关的阻塞方法都是通过本地方法实现的,而condition的阻塞和唤醒方法都是通过java调用来实现的,其次就是每个Object只能绑定一个阻塞器,即synchronized所绑定的对象,只有通过调用该对象的wait和notify方法才能实现阻塞以及唤醒,并且notify会在调用wait方法的线程中随机挑选一个唤醒
  • 而一个lock可以创建多个condition,例如ReentrantLock中的newCondition方法每次调用都会返回一个新的条件阻塞器,这样做的好处是,调用condition方法的signal只会唤醒当前condition调用await方法阻塞的线程,利用这种模式可以实现阻塞队列,如经典的ArrayBlockingQueue就是利用ReenTrantLock创建了两个condition控制队列空时的阻塞以及队列满时的阻塞

await方法的实现

老规矩先上源码

public final void await() throws InterruptedException {
    		//检测线程是否中断
            if (Thread.interrupted())
                throw new InterruptedException();
    		//添加一个condition的waiter
            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);
        }

await的实现并不难理解,而操作的Node节点与AQS中的node节点是一个对象,通过标记node的waitStatus变量来判断当前node的状态,我们再来看看addConditionWaiter的实现

private Node addConditionWaiter() {
        Node t = lastWaiter;
        //如果尾部的等待node被取消了,则遍历取消所有的被取消的节点
        if (t != null && t.waitStatus != Node.CONDITION) {
            unlinkCancelledWaiters();
            t = lastWaiter;
        }
    	//创建一个condition状态的node节点
        Node node = new Node(Thread.currentThread(), Node.CONDITION);
    	//如果尾结点是空证明是一个空队列,将头结点设置为当前节点,否则将当前节点插入当前尾节点的后面
        if (t == null)
            firstWaiter = node;
        else
            t.nextWaiter = node;
        lastWaiter = node;
        return node;
}

private void unlinkCancelledWaiters() {
            Node t = firstWaiter;
            Node trail = null;
    		//遍历取消所有节点状态不是condition的节点
            while (t != null) {
                Node next = t.nextWaiter;
                if (t.waitStatus != Node.CONDITION) {
                    t.nextWaiter = null;
                    if (trail == null)
                        firstWaiter = next;
                    else
                        trail.nextWaiter = next;
                    if (next == null)
                        lastWaiter = trail;
                }
                else
                    trail = t;
                t = next;
            }
        }

在这个方法中,值得注意的是通过condition维护的队列,与aqs中排队的队列是两个完全不同的队列,condition的队列维护在condition对象中,通过firstWaiter和lastWaiter变量来维护队列的头与尾,我们继续往下看fullyRelease方法

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

fullyRelease方法可见是直接释放当前独占锁,在java中目前只有ReenTrantLock以及ReentrantReadWriteLock,实现了newCondition方法,所以共享锁是不允许condition阻塞的,

继续向下看isOnSyncQueue方法,顾名思义该方法是判断当前node是否在同步队列总

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

首先是校验当前节点的状态,如果节点状态还是condition那么一定没有插入队列中,而同样node.prev前面节点如果为空自然是也没有插入队列的,后续判断node.next同样是判断后续有没有等待节点,这里值得注意的是,node.next是同步队列节点的下一个节点,而condition阻塞队列的节点为nextWaiter不要弄混了,

如果这两步判断没有成功的话,说明当前节点的prev节点不为空,而next节点为空,而node.prev节点不为空,,但是还没有在队列上,因为有可能cas失败,所以要从尾部遍历一遍确定在没在节点中.

如果在同步队列中则调用acquireQueued尝试获取锁或者排队,接下来就是判断是否打断等流程,后续不在赘述,接下来我们康康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 (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;
		//将当前线程插入队列,并返回node节点前面的节点,
        Node p = enq(node);
        int ws = p.waitStatus;
    	//修改前一个节点的状态为signle以便被唤醒
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            //如果线程被取消了,或者将waitstatus修改失败的话,说明当前线程已经被取消了
            LockSupport.unpark(node.thread);
        return true;
    }

首先调用signal时一定是要以独占锁的模式调用,否则会抛出异常,然后将当前等待节点后移,并且将当前的节点插入阻塞队列中,不需要唤醒线程因为调用signal时一定是已经被某一线程获取了锁,而当调用release时会释放锁并且自动调用后续的锁

那么这里有一个问题就是为什么会在这里调用一次unpark,就算不调用,等到下次唤醒的时候,也会清除掉被取消的节点,这里我查阅资料发现,这次唤醒主要是提升性能,在这里唤醒一次,将前面取消的节点都删除,以便下次唤醒不需要在删除节点.这里加不加这个唤醒逻辑上是一样的

我们再来看看signalAll方法

      public final void signalAll() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignalAll(first);
        }

      private void doSignalAll(Node first) {
            lastWaiter = firstWaiter = null;
            do {
                Node next = first.nextWaiter;
                first.nextWaiter = null;
                transferForSignal(first);
                first = next;
            } while (first != null);
        }

这里实现与signal几乎相同,只不过一个是将first节点插入队列,而signalAll方法则是将后续队列全部插入同步队列中

到这里我们就已经将condition的实现完全理清了,后续我们也会再分析利用condition来实现的同步阻塞队列ArrayBlockingQueue 欢迎大家关注我的公众号

小阿宅Java