AQS源码解析---Condition

目录

一 监视器锁(Synchorized)和Condition

二 sync queue 和 condition queue的不同

三 sync queue 和 condition queue的联系

四 await源码解析 

 addConditionWaiter解析

 fullyRelease解析

signallAll()

signall() 

await后被唤醒的处理

中断发生时,线程没被signal/signalAll唤醒过

 中断发生时,线程被signal/signalAll唤醒过---不是在抢锁中发生的中断

中断发生时,线程被signal/signalAll唤醒过---在抢锁中发生的中断

await总结


一 监视器锁(Synchorized)和Condition

        Condition的await/signal是用于替换监视器锁中的wait/notify机制,它们的对比如表1.1所示:

                                                                         表1.1

监视器锁condition
void wait()void await()
void wait(long timeout)long awaitNanos(long nanosTimeout)
void wait(long timeout, int nanos)boolean await(long time, TimeUnit unit)
void notify()void signal()
void notifyAll()void signalAll()
---void awaitUninterruptibly()
---boolean awaitUntil(Date deadline)

         debug用的示例代码来自官方的demo代码,是一个典型的生产---消费模型:

class BoundedBuffer {
    final Lock lock = new ReentrantLock();
    final Condition notFull = lock.newCondition();
    final Condition notEmpty = lock.newCondition();

    final Object[] items = new Object[100];
    int putptr, takeptr, count;

    public void put(Object x) throws InterruptedException {
        lock.lock();
        try {
            while (count == items.length)
                notFull.await(); 
            items[putptr] = x;
            if (++putptr == items.length) putptr = 0;
            ++count;
            notEmpty.signal(); 
        } finally {
            lock.unlock();
        }
    }

  
    public Object take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0)
                notEmpty.await(); 
            Object x = items[takeptr];
            if (++takeptr == items.length) takeptr = 0;
            --count;
            notFull.signal();
            return x;
        } finally {
            lock.unlock();
        }
    }
}

二 sync queue 和 condition queue的不同

        在 AQS源码解析---独占锁获取 一文中提到了Node队列,为了和condition用到得队列形成对比,这里将上诉文章中提到的Node队列称为同步队列,如图1.1所示:

图 1.1 

         在上诉同步队列中,通过prev,next来串联节点。其实在同步队列中还有一个nextWaiter属性,同步队列因为没用到这个属性,就没画在上诉图中。nextWaiter在同步队列中将会用到,且是一个很重要的属性。

        这里以官方示例demo为例,notFull和notEmpty的条件队列如图1.2所示:

图 1.2

         每一个条件队列时相互独立的。在demo示例中,调用notFull.await或notEmpty.await会将当前线程包装成Node放入对应条件队列的末尾。在条件队列中,通过nextWaiter将这个链表串联起来,在条件队里中,未使用到prev和next这两个属性。条件队里实际上用到得属性就thread、waitStatus和nextWaiter这三个。

        waitStatus共有4个值,分别是1(CANCELLD)、-1(SIGNAL)、-2(CONDITION)、-3(PROPAGATE)。在条件队列中,涉及到的waitStatus的值为-2,表示线程处于等待状态。如果waitStatus为非-2状态,将表示该线程需要从条件队列中移除,不再等待。

        同步队列和条件队列在出入队时,锁的状态也有所差异。同步队列是等待锁的队列,当线程没获取到锁后,才会被包装成Node放入同步队列中,线程从同步队列中出队,是在获取到锁后才能出队

        而条件队列正好相反,当调用await方法时,该线程一定是获取到lock锁的。即在进入条件队里那一个刻,线程是获取到锁的;进入到队列后,线程被挂起,释放锁。当线程被signal/signalAll从条件队列中唤醒出队时,会去抢锁,即线程从条件队列出队时,是没有获取到锁的

三 sync queue 和 condition queue的联系

        同步队列和条件队列实际上就是同一个Node类,只是用到的属性有差异。当调用条件队列的signal/signalAll方法时,会将条件队列中的一个或所有线程唤醒,被唤醒的线程会去争抢锁,如果争锁失败,会被添加到同步队列的末尾继续等待。需要注意的是,哪怕是通过signalAll的方式唤醒线程,也不是将这整个条件队里直接添加到同步队列的末尾,是一个一个转移过去的。我猜想的原因是,条件队列中的节点转移到同步队列中的过程,需要先断开原来的nextWaiter属性,赋值上新的pre,next属性,所以只能条件队列中一个一个节点的转移,不能直接转移整个条件队列。

四 await源码解析 

         await源码大体流程如图4.1,已经在代码中给出了相应注释,接下来会进一步进行分析说明。

图 4.1

 addConditionWaiter解析

        源码如图4.2:

图 4.2

         注意,在addConditionWaiter中和之前的await,并没有加锁或者CAS的操作,这是因为,在调用await方法的时候,一定是持有锁的,所以不需要做通不处理,不存在并发。

         很明显的从源码中能够得知,在初始化条件队列时,是直接将firstWaiter和lastWaiter指向新建节点就行了。但是在同步队列中,我之前在另一篇文章中分析过了,同步队里的头结点是一个dummy哑节点。

        在条件队里中有新的节点加入时,只修改了前驱节点的nextWaiter,是一个单向队里。而同步队列时一个双向队列,通过prev和next进行前后节点的双向关联。

        需要特别说明的是,如果节点在加入条件队里时,发现当前队里的尾节点已经取消等待了,这个时候我们就不应该连接在这个尾节点的末尾,需要通过unlinkCancelledWaiters来删除前面所有已经取消等待的节点,然后再加入。unlinkCancelledWaiters的源码如4.3所示,是一个很基本的链表操作,就不细说了。

图 4.3

 fullyRelease解析

        在成功加入条件队里末尾后,通过fullyRelease方法来释放当前线程持有的锁,源码如图4.4所示:

 图 4.4

         在释放锁前,先通过getState()获取了state的值,这个state的值是当前线程重入的次数。

这里调用了release方法来释放锁,这个方法这里不详细讲了,在我之前分析AQS锁的时候有详细的分析。在这个release里面还会调用tryRealse方法,在这个方法里会执行c = getState() - releases,这里的releases就是通过getState()获取的state的值。因此对于可重入锁而言,fullyRelease会一次释放掉锁,不管重入了多少次,因为这里传入的就是所有的重入次数,相减直接为0。如图4.5:

图 4.5 

         fullyRelease是由可能抛出监视器异常的,具体是从tryRelease中抛出的,当当前线程不是持有锁的线程的时候,会抛出该异常。也就是说,实际上在调用await()方法的时候,可能并不是持有锁的线程去持有的,在调用fullyRelease之前是没有检查当前线程是否是持有锁的线程的。之前有说过,进入到await()中的就是持有锁的线程嘛,我理解在极端情况下,会存在不是持有锁的线程。但是在await()方法中做了一个兜底的处理,如果抛出了这个监视器异常,在fullyRelease的finally中会将该节点标记为CANCELLED。还记得,在addConditionWaiter中在添加新节点前都会检查尾节点是否是CANCELLED状态的原因了

         在通过fullyRelease释放掉锁后,就会去挂起当前线程。在挂起当前线程的时候,还通过isOnSyncQueue()来确保当前线程不在同步队列中。这里有个疑问,要加入到条件队列中的线程为什么会有可能在同步队列中呢?需要从signal相关方法中找到答案。下面先分析signalAl()和signal()方法。

signallAll()

        调用signallAll的线程本身得现持有lock锁,signallAll调用后,会唤醒条件队列中的线程,然后这些被唤醒的线程会去争抢锁。源码如图4.6:

图 4.6

         接下来进入doSignalAll方法,如图4.7:

图 4.7

         该方法首先将条件队里的首末位清空,然后通过循环将条件队列中的每个一节点拿出来通过transerForSignal将节点放入到同步队列的末尾。transerForSignal源码如图4.8所示:

图 4.8        

        通过CAS成功修改状态后,会调用end方法将该节点加到同步队列的末尾。end方法在以前的AQS源码分析中有讲。这个end方法返回的是node节点加入到同步队列后的前驱节点,然后将这个前驱节点的状态变为SIGNAL,我们知道,在同步队列中,节点状态为SIGNAL表示下一个节点需要唤醒。

        注意这里,如果node的前驱节点p被取消,或者设置成SIGNAL状态失败。那么就直接将node节点唤醒。但是唤醒后,不代表就能获取到锁,锁还是需要去争抢的,抢不到锁就继续被挂起。

signall() 

        弄懂signalAll方法后,signal就很简单了,源码如下:

图 4.9

        signal实际上就是唤醒添加队列中第一个没有被cancelled的节点,并将这个节点放入到同步队列中。 在这里用了transerForSignal的返回值,transerForSignal返回true就表示节点已经加入到同步队列中了,然后结束循环。

await后被唤醒的处理

        被唤醒后的代码部分如图4.10红色框所示:

 

图 4.10

        被唤醒后的处理设计到不同情况下的中断处理,这里先给一个结论,再进一步讨论,便于大家理解:

        1.被中断唤醒的线程,和被正常操作唤醒的线程一样,会离开条件队里进入到同步队列中;

        2.acquireQueued方法是不对中断进行响应的,只是简单记录中断的状态,交给上层调用函数去处理。

        3.如果中断发生时,当前线程没有被signal/signalAll唤醒过 ,需要中断该线程,导致正常在条件队里中等待的状态被中断,并放入到同步队列中进行抢锁。这个时候会抛出InterruptedException,表示当前线程因为中断而被唤醒。

        4.如果中断发生时,当前线程被signal/signalAll唤醒过,说明这个中断来的太迟了,线程已经从条件队列中被唤醒了。这个时候会忽略这个中断,但是在await()调用返回后,会补一下这个中断。因为在通过Thread.intreeupt方法检查中断是否发生时,会将中断标志清除,因此需要补一下这个中断;

        5.图4.10中的interruptMode可能有3个值:0表示没有中断发生;THROW_IE表示在await()退出时需要抛出InterruptedException;REINTERRUPT表示,在await()退出时需要自我中断。

中断发生时,线程没被signal/signalAll唤醒过

         首先分析checkInterruptWhileWaiting(node))方法,源码如图4.11:

图4.11

         这里我们假设的是中断发生时,线程没有被唤醒过,所以这里的Thread.intreeupted会返回ture,那么需要分析transferAfterCancelledWait(node)方法,源码如图4.12所示:

 图 4.12

         一个节点的状态如何是CONDITION,说明它还没被唤醒,因此在我们当前的假设下,这里的CAS操作会成功,会将节点状态变为0,然后调用end方法将节点放入到同步队列中的末尾处,transferAfterCancelledWait(node)方法返回true。一层一层回到await方法中,这个时候,interruptMode为THROW_IE,此时将会调出break循环,如图4.13:

图 4.13

         接下来会进入到acquireQueued中进行争抢锁,这里传入的savedState是当前锁的可重入次数,这个在前面有讲到。也就是说,之前在fullyRelease中释放了多少可重入锁,这里就要重新再次获取多少可重入锁。

        acquireQueued方法再之前的AQS源码分析中有讲,大概意思就是在该方法中,会阻塞式的获取锁,获取到锁就退出,获取不到就阻塞,直到获取成功才退出。且会返回当前线程的中断状态。

        我们假设在acquireQueued方法中获取到了锁,但是此时的interruptMode为THROW_IE,这里会进入到if(node.nextWaiter != null)这个判断,这里要说下,此时这个node.nextWaiter必定不会null。在图4.12中调用end方法将节点加入到同步队列后,并没有断裂掉和之前队列中的联系。这里直接通过unlinkCacenlledWaiters将条件队列中所有被取消的节点都从队列中移除掉。

        因为interruptMode为THROW_I,最后会进入到reportInterruptAfterWait方法中,如图4.14所示:

图 4.14

 中断发生时,线程被signal/signalAll唤醒过---不是在抢锁中发生的中断

        回到transferAfterCancelledWait(Node node)方法,中断发生在唤醒后,那么这里将直接进入到while循环。

图 4.15 

         这里会一直在while循环中,等到该节点成功加入到同步队列中为止才会跳出循环。假设当前线程为A,A被唤醒后检查到发生中断来到这个循环中。此时有另一个线程B在之前调用了signal/signalAll方法,并会把节点加入到同步队列中。加入成功后,在某个时刻,回到线程A,A在这个while循环中就会检查到该节点已经加入到同步队列中,跳出循环并,返回false。、        

         transferAfterCancelledWait返回false后, checkInterruptWhileWaiting方法将返回REINTERRUPT。同样的,返回后,依然会进入到acquireQueued中去抢锁,直到抢到锁为止。

注意,因为这个时候的node节点是通过上面假设的B线程通过signal/signalAll方法唤醒的,会将node的nextWaiter设置为null。因此此时不会再执行unlinkCancelledWaiters方法了。

图 4.16

        最后在reportInterruptAfterWait方法中会进行自我中断。

中断发生时,线程被signal/signalAll唤醒过---在抢锁中发生的中断

        这里单独把这个拿出来将,是在看源码时想到,会不会存在被唤醒后,又没进入到同步队列,也没发生中断的情况,称为虚假唤醒,如图4.17:

图 4.17

        其他的和 中断发生时,线程被signal/signalAll唤醒过---不是在抢锁中发生的中断类似了。

await总结

1.进入await时需持有锁;

2.离开await时也需持有锁;

3.调用await会将线程保证成node放入条件队列,并释放持有的锁后,在条件队里中挂起知道发生中断或被signal/signalAll唤醒;

4.线程在条件队列中被中断或唤醒均为进入到同步队列中去抢锁;

5.根据中断发生在唤醒前还是唤醒后,会分别抛出中断异常和自我中断记录;

6.线程持有锁,从await方法返回。

        

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值