关于线程之间的通信,有:
- Object的wait(),wait(long timeout),wait(long timeout, int nanos)与notify(),notifyAll()几个方法实现等待/通知机制
- Lock体系中的配合Condition实现也能够完成,其中底层使用的LockSupport的park方法。
前者是java底层级别的,后者是语言级别,有着较高的可控性和拓展性,它们的区别也有:
- Condition能够支持不响应中断,而通过使用Object方式不支持;
- Condition能够支持多个等待队列(new 多个Condition对象),而Object方式只能支持一个;
- Condition能够支持超时时间的设置,而Object不支持
参照Object的wait和notify/notifyAll方法,Condition也提供了同样的方法:
针对Object的wait方法
- void await() throws InterruptedException:当前线程进入等待状态,如果其他线程调用condition的signal或者signalAll方法并且当前线程获取Lock从await方法返回,如果在等待状态中被中断会抛出被中断异常;
- long awaitNanos(long nanosTimeout):当前线程进入等待状态直到被通知,中断或者超时;
- boolean await(long time, TimeUnit unit)throws InterruptedException:同第二种,支持自定义时间单位
- boolean awaitUntil(Date deadline) throws InterruptedException:当前线程进入等待状态直到被通知,中断或者到了某个时间
针对Object的notify/notifyAll方法
- void signal():唤醒一个等待在condition上的线程,将该线程从等待队列中转移到同步队列中,如果在同步队列中能够竞争到Lock则可以从等待方法中返回。
- void signalAll():与1的区别在于能够唤醒所有等待在condition上的线程
探究Condition源码:
Condition是一个interface,里面包含await、await(timeout)、signal、signalAll的方法,其实现是在AQS中ConditionObject,跟Node一样是AQS的内部类。它需要和Lock一块使用,创建的方式是Lock.newCondition(),AQS内部有个双向队列,拿不到锁的会从尾巴处接入,队列每个Node都有prev和next,队列叫同步队列;而Condition也有队列,为等待队列,但其实单向队列,不过同样是使用尾插法,每个Node只会和后一个的Node关联,不会和前面的Node有关联。
condition的单向队列如下:
condition有两个变量,类似AQS的head和tail,它的是firstWaiter、lastWaiter。
await()
首先看condition.await(),在于让出锁,使自身阻塞:
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
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);
}
private Node addConditionWaiter() {
Node t = lastWaiter;
// 如果lastWaiter被取消了,将其清除
if (t != null && t.waitStatus != Node.CONDITION) {
// 遍历整个条件队列,将已取消的所有节点清除出列
unlinkCancelledWaiters();
// t重新赋值一下,因为last可能改变了
t = lastWaiter;
}
//注意这里,node在初始化的时候,会指定ws为CONDITION
Node node = new Node(Thread.currentThread(), Node.CONDITION);
// t == null 表示队列此时为空,初始化firstWaiter
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;// 入队尾
lastWaiter = node;// 将尾指针指向新建的node
return node;
}
private void unlinkCancelledWaiters() {
Node t = firstWaiter;
// trail这里表示取消节点的前驱节点
Node trail = null;
// t会从头到尾遍历这个单链表
while (t != null) {
// next用于保存下一个
Node next = t.nextWaiter;
// 如果发现当前这个节点 不是 condition了, 那么考虑移除它
// 下面是单链表的移除节点操作 简单来说就是 trail.next = t.next
if (t.waitStatus != Node.CONDITION) {
t.nextWaiter = null;
// 说明first就是不是condition了
if (trail == null)
firstWaiter = next;
else
//trail.next = t.next
trail.nextWaiter = next;
// trail后面没东西,自然trail就是lastWaiter了
if (next == null)
lastWaiter = trail;
}
// 当前节点是一直跟到不是condition节点的上一个
else
trail = t;
// 向后遍历 t = t.next
t = next;
}
}
一来见到如果是interrupted,就扔异常,没什么好说,在看addConditionWaiter(),里面的代码,注意Node的waitStatus是condition的,前面说的AQS双向队列是exclusive的,还有双向队列使用的prev和next,这里用的是nextWaiter,这点注意区别。该方法很简单,就是初始化等待队列,如果已经有了等待队列,就尾巴接入新的Node,返回这个新Node出去,同时有unlinkCancelledWaiters()将已取消的所有节点清除出列,以上执行完之后,接着执行 fullyRelease(node) 。
final int fullyRelease(Node node) {
boolean failed = true;
try {
// 获取当前的state值,重入次数
int savedState = getState();
// 释放N = savedState资源
if (release(savedState)) {
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
// 如果获取失败,将会将节点设置为取消状态,并抛出异常
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
执行tryRelease(),该方法使用Lock重写的tryRelease
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
以上的fullRelease(Node),是将当前线程,即这个Node占有的锁释放,注意是全数释放,因为reentrantLock是可重入锁,返回是获取锁的次数,当再次唤醒的时候,就能还原了,最后unpark唤醒双向同步队列的下一个节点,自己就进入condition的等待队列。所以其实这2个队列的关系如下:
所以,其实等待队列中的Node是同步队列中转移过来的,如果不先调用lock.lock(),而直接condition.await(),tryRelease()判断不是当前线程持有锁,就扔出异常,由外面包裹的catch捕获, 最后置Node为cancel状态,它仍然在等待队列中,所以unlinkCancelledWaiters()方法有存在的必要。
跳到外面,
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);
}
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;
/*
* node.prev can be non-null, but not yet on queue because
* the CAS to place it on queue can fail. So we have to
* traverse from tail to make sure it actually made it. It
* will always be near the tail in calls to this method, and
* unless the CAS failed (which is unlikely), it will be
* there, so we hardly ever traverse much.
*/
return findNodeFromTail(node);
}
先看isOnSyncQueue()中的第一个判断条件:如果是condition,或node.prev==null,condition是进入等待单向队列的状态、node.prev==null即持有锁,ok,这样就不在同步队列了,但有无情况是ws不等于condition呢?有可能是被signal了,signal就是将等待队列的节点移动回同步队列。回到同步队列是尾接法,prev就不是null,何况这里的操作并不是原子操作,有可能出现这种情况。说到尾插法,next应该就是null,但前面已经将锁释放了,就意味别的线程也能抢锁的,有可能有另外新的线程尾插了,最后兜底的还是findNodeFromTail(),直接在同步队列中,从尾巴开始往头部顺着找。
回归正题,如果不在同步队列,出来就park,interrupt和signal都会打断park,它就醒来,检查有无被打断,
private int checkInterruptWhileWaiting(Node node) {
return Thread.interrupted() ?
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
}
final boolean transferAfterCancelledWait(Node node) {
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
enq(node);
return true;
}
/*
* If we lost out to a signal(), then we can't proceed
* until it finishes its enq(). Cancelling during an
* incomplete transfer is both rare and transient, so just
* spin.
*/
while (!isOnSyncQueue(node))
Thread.yield();
return false;
}
如果被打断了:有2种方式打断:interrupt 和 unpark(由signal触发),进入transferAfterCancelledWait()方法:
- interrupt打断:这里也分为2种,因为这里要判断ws。
在signal之前,那没人改变ws,它依然是condition,cas成功就enq(node)尾接双向同步队列,返回true,得到的异常结果是 THROW_IE = -1 ,跳出while循环。
在signal之后,改变了ws变为0,返回false,跳出为REINTERRUPT = 1,也跳出while循环。
查看跳出循环后执行的代码:
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
acquireQueued(node,savedSate)这个在AQS解析过,就是进入同步双向队列,这个方法怎么都要执行,没有拿到锁就park等待,拿到锁就出队返回,这里只看返回true或false,但不重要,这里的判断条件是 && ,只看interruptMode ,中间这步不看,还是清理cancelNode,看最后,是哪样就返回哪样,selfInterrupt()的话,就是interrupt是signal之后执行,这里只是还原用户操作,和AQS那里是一致。
private void reportInterruptAfterWait(int interruptMode)
throws InterruptedException {
if (interruptMode == THROW_IE)
throw new InterruptedException();
else if (interruptMode == REINTERRUPT)
selfInterrupt();
}
由上可得,await的模型如下:
signal()
该方法是单向等待队列 出队第一个Node。
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
private void doSignal(Node first) {
do {
// firstWaiter 指向first的下一个
if ( (firstWaiter = first.nextWaiter) == null)
// 如果first是最后一个且要被移除了,就将last置null
lastWaiter = null;
// first断绝与条件队列的连接
first.nextWaiter = null;
// fisrt转移失败,就看看后面是不是需要的
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
可见signal 是从firstWaiter开始的,即单向等待队列头部,直接看关键方法 transferForSignal(Node node):
final boolean transferForSignal(Node node) {
/*
* CAS操作尝试将Condition的节点的ws改为0
* 如果失败,意味着:节点的ws已经不是CONDITION,说明节点已经被取消了
* 如果成功,则该节点的状态ws被改为0了
*/
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
/*
* 通过enq方法将node自旋的方式加入同步队列队尾
* 这里放回的p是node在同步队列的前驱节点
*/
Node p = enq(node);
int ws = p.waitStatus;
// ws大于0 的情况只有 cancenlled,表示node的前驱节点取消了争取锁,那直接唤醒node线程
// ws <= 0 会使用cas操作将前驱节点的ws置为signal,如果cas失败也会唤醒node
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
// 自旋的方式入队
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
// 返回的是node的前驱节点
return t;
}
}
}
}
node是单向等待队列首节点,cas设置ws为0,失败的话,node的ws已经不是condition,回到外面继续while循环,继续单向等待队列的下一个。enq(Node) 还是将node转移到双向同步队列中,这里必须非常注意,enq(Node)返回的是参数Node的前驱节点,之后判断ws,如果ws是>0,即是cancel状态,直接唤醒即可,这个如果false,后面得true才能出队了,先看判断条件, !compareAndSetWaitStatus(p, ws, Node.SIGNAL)就帮帮前驱节点的ws置为-1,Signal排队状态,如果失败就直接唤醒node,醒来就回到之前的以下代码,checkInterruptWhileWaiting里面如果没有被打断的话,返回0,继续while判断,但此时node已经在同步双向队列中了,跳出while循环,还是acquireQueued(node, savedState)继续排队,它不是第一个排队的,拿不到锁,又是park继续阻塞,最后让它醒了,执行acquireQueued()方法余下的代码,拿到锁就继续业务代码, 以下用@包裹的方法有处理异常的,上面已经说过,不再重复,node的nextWaiter==null,也不需要清理,结束方法。
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);
@@@@
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
signalAll()
就是将单向等待队列的Node按顺序转移到双向同步队列中罢了,这里不展开。
private void doSignalAll(Node first) {
lastWaiter = firstWaiter = null;
do {
Node next = first.nextWaiter;
first.nextWaiter = null;
transferForSignal(first);
first = next;
} while (first != null);
}
结论:
调用condition的signal的前提条件是当前线程已经获取了lock,该方法会使得等待队列中的头节点即等待时间最长的那个节点移入到同步队列,而移入到同步队列后才有机会使得等待线程被唤醒,即从await方法中的LockSupport.park(this)方法中返回,从而才有机会使得调用await方法的线程成功退出。
signal执行示意图如下图:
参考资料:Condition中await与signall