目录
一 监视器锁(Synchorized)和Condition
二 sync queue 和 condition queue的不同
三 sync queue 和 condition queue的联系
四 await源码解析
addConditionWaiter解析
fullyRelease解析
signallAll()
signall()
await后被唤醒的处理
中断发生时,线程没被signal/signalAll唤醒过
中断发生时,线程被signal/signalAll唤醒过---不是在抢锁中发生的中断
中断发生时,线程被signal/signalAll唤醒过---在抢锁中发生的中断
await总结
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();
}
}
}
在 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从条件队列中唤醒出队时,会去抢锁,即线程从条件队列出队时,是没有获取到锁的。
同步队列和条件队列实际上就是同一个Node类,只是用到的属性有差异。当调用条件队列的signal/signalAll方法时,会将条件队列中的一个或所有线程唤醒,被唤醒的线程会去争抢锁,如果争锁失败,会被添加到同步队列的末尾继续等待。需要注意的是,哪怕是通过signalAll的方式唤醒线程,也不是将这整个条件队里直接添加到同步队列的末尾,是一个一个转移过去的。我猜想的原因是,条件队列中的节点转移到同步队列中的过程,需要先断开原来的nextWaiter属性,赋值上新的pre,next属性,所以只能条件队列中一个一个节点的转移,不能直接转移整个条件队列。
await源码大体流程如图4.1,已经在代码中给出了相应注释,接下来会进一步进行分析说明。
图 4.1
源码如图4.2:
图 4.2
注意,在addConditionWaiter中和之前的await,并没有加锁或者CAS的操作,这是因为,在调用await方法的时候,一定是持有锁的,所以不需要做通不处理,不存在并发。
很明显的从源码中能够得知,在初始化条件队列时,是直接将firstWaiter和lastWaiter指向新建节点就行了。但是在同步队列中,我之前在另一篇文章中分析过了,同步队里的头结点是一个dummy哑节点。
在条件队里中有新的节点加入时,只修改了前驱节点的nextWaiter,是一个单向队里。而同步队列时一个双向队列,通过prev和next进行前后节点的双向关联。
需要特别说明的是,如果节点在加入条件队里时,发现当前队里的尾节点已经取消等待了,这个时候我们就不应该连接在这个尾节点的末尾,需要通过unlinkCancelledWaiters来删除前面所有已经取消等待的节点,然后再加入。unlinkCancelledWaiters的源码如4.3所示,是一个很基本的链表操作,就不细说了。
图 4.3
在成功加入条件队里末尾后,通过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的线程本身得现持有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节点唤醒。但是唤醒后,不代表就能获取到锁,锁还是需要去争抢的,抢不到锁就继续被挂起。
弄懂signalAll方法后,signal就很简单了,源码如下:
图 4.9
signal实际上就是唤醒添加队列中第一个没有被cancelled的节点,并将这个节点放入到同步队列中。 在这里用了transerForSignal的返回值,transerForSignal返回true就表示节点已经加入到同步队列中了,然后结束循环。
被唤醒后的代码部分如图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()退出时需要自我中断。
首先分析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
回到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方法中会进行自我中断。
这里单独把这个拿出来将,是在看源码时想到,会不会存在被唤醒后,又没进入到同步队列,也没发生中断的情况,称为虚假唤醒,如图4.17:
图 4.17
其他的和 中断发生时,线程被signal/signalAll唤醒过---不是在抢锁中发生的中断类似了。
1.进入await时需持有锁;
2.离开await时也需持有锁;
3.调用await会将线程保证成node放入条件队列,并释放持有的锁后,在条件队里中挂起知道发生中断或被signal/signalAll唤醒;
4.线程在条件队列中被中断或唤醒均为进入到同步队列中去抢锁;
5.根据中断发生在唤醒前还是唤醒后,会分别抛出中断异常和自我中断记录;
6.线程持有锁,从await方法返回。