Condition的用法
上一篇文章分析了ReentrantLock抢锁、线程入队、释放锁的过程,这篇文章接着来看条件队列的应用。AbstractQueuedSynchronizer中的Condition表示条件队列,我们可以通过Condition实现线程通信,我们希望挂起线程,在满足某种条件时唤醒线程,例如很常见的实现生产-消费者模式,就可以通过条件队列实现。首先基于ReentrantLock来看一下条件队列的简单用法。
class ConditionTest {
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
void awaitTest() {
try {
lock.lock();
System.out.println(Thread.currentThread().getName()+"线程挂起,等待被唤醒---");
condition.await();
System.out.println(Thread.currentThread().getName()+"线程被唤醒继续执行---");
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
void signalTest() {
try {
lock.lock();
condition.signal();
System.out.println(Thread.currentThread().getName()+"唤醒条件队列中的线程");
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
public static void main(String[] args){
JucApplicationTests tests = new JucApplicationTests();
new Thread(tests::awaitTest).start();
new Thread(tests::awaitTest).start();
new Thread(tests::signalTest).start();
new Thread(tests::signalTest).start();
}
}
使用condition时必须获取相应的锁,也就是说await和signal操作是依赖ReentrantLock的,不管是挂起还是唤醒操作,都必须先持有锁。从语义上来说这点和Object中wait、notify、notifyAll很像,必须获取监视器锁才能进行操作。
我们来看一下ConditionObject类
Condition condition = lock.newCondition();
final ConditionObject newCondition() {
return new ConditionObject();
}
public class ConditionObject implements Condition, java.io.Serializable {
private static final long serialVersionUID = 1173984872572414699L;
/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;
/**
* Creates a new {@code ConditionObject} instance.
*/
public ConditionObject() { }
public final void signal() {
......
}
public final void signalAll() {
......
}
public final void await() throws InterruptedException {
......
}
......
}
- ConditionObject中firstWaiter和lastWaiter分别代表条件队列的首尾节点,结合Node中nextWaiter属性构成单向链表,也就是我们说的条件队列。
- ConditionObject可以new多个,即可以同时存在多个条件队列。
- 条件队列和阻塞队列的节点都是Node实例,当有线程调用condition.signal会唤醒条件队列的队头,并转移到阻塞队列的队尾。
- 线程在调用condition.await方法后,会将自己包装成一个Node节点并加入条件队列的队尾,同时挂起线程。
- 条件队列与阻塞队列的区别:我们在分析ReentrantLock时提到阻塞队列的概念,线程在抢锁失败后,会进入阻塞队列排队等待,直到被前置节点唤醒。而条件队列是线程持有锁的前提下,线程执行到await进入条件队列挂起;条件队列中的线程将一直挂起直到有线程调用signal。
条件队列入队
首先看一下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状态不是Node.CONDITION,说明取消排队了
if (t != null && t.waitStatus != Node.CONDITION) {
// 如果条件队列队尾节点取消排队了,将其移除队列,并向后遍历直到获取正常等待的节点
unlinkCancelledWaiters();
t = lastWaiter;
}
//节点入队并成为新的队尾,初始化状态是 Node.CONDITION
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
/**
* 遍历条件队列并将取消排队的节点踢出去 t.waitStatus != Node.CONDITION
*/
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;
if (next == null)
lastWaiter = trail;
}
else
trail = t;
t = next;
}
}
线程唤醒
while (!isOnSyncQueue(node)) {
//线程在这里挂起,等待节点被唤醒
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
通过上面的代码我们知道当线程执行到LockSupport.park(this)时挂起等待被唤醒,为了便于理解,我们先看唤醒线程的方法。
/**
* 唤醒操作其实就是将条件队列中的节点转移到阻塞队列
*/
public final void signal() {
//执行signal的线程必须是持有当前独占锁
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
/**
* 条件队列从前往后找到第一个需要被转移的节点,这里都是一些单向链表的操作
*/
private void doSignal(Node first) {
do {
//first节点要出队了,如果没有nextWaiter了,则将lastWaiter也设置为空
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
//出队
first.nextWaiter = null;
//这里循环,如果first转移不成功,则转移下一个,以此类推
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
/**
* 这个方法中将节点转移到阻塞队列
*/
final boolean transferForSignal(Node node) {
/*
* 将waitStatus设置为0
*/
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
/*
* 将节点转移到阻塞队列队尾,返回的p是阻塞队列中的前驱节点
*/
Node p = enq(node);
int ws = p.waitStatus;
/**
* ws > 0 说明前驱节点取消了排队,唤醒当前node对应的线程
* ws <= 0 则将前驱节点状态设置为Node.SIGNAL,如果CAS失败则唤醒当前线程
*/
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
注意看ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)这个条件,正常情况下前驱节点在排队中ws=0,并且CAS成功状态变成Node.SIGNAL;那什么情况会唤醒线程呢?前驱节点取消排队或者CAS修改前驱节点状态失败,会唤醒线程。
检查中断状态
while (!isOnSyncQueue(node)) {
//线程在这里挂起,等待节点被唤醒
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
被唤醒的线程从await方法中LockSupport.park处继续执行,checkInterruptWhileWaiting这个方法在线程被唤醒后就会执行,因为线程可能是被signal唤醒的,也可能是发生了中断被唤醒,所以while循环退出循环有两种情况:
1.线程发生了中断,要区分是在await期间中断还是signal后中断的,并将节点从条件队列转移到阻塞队列
2.没有发生中断,signal后等待节点被转移到阻塞队列,然后退出循环
/**
* 检查中断状态,如果
* 1.如果线程没有发生中断返回0
* 2.线程发生中断并且是在await期间发生的,需要抛出异常 THROW_IE
* 3.如果线程在signal之后发生中断,则需要重新设置中断状态 REINTERRUPT
*/
private int checkInterruptWhileWaiting(Node node) {
return Thread.interrupted() ?
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
}
/**
* 只有中断状态才会调用此方法,判断节点是在await期间转移的还是signal后
*/
final boolean transferAfterCancelledWait(Node node) {
//signal方法会将waitStatus设置为0
//这个CAS成功说明waitStatus还是Node.CONDITION,说明中断是在await期间发生的
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
//即使被中断了,依然转移到阻塞队列
enq(node);
return true;
}
/*
* signal之后发生了中断,自旋等待节点转移到阻塞队列
*/
while (!isOnSyncQueue(node))
Thread.yield();
return false;
}
所以条件队列中线程即使发生了中断,不管是在await期间还是signal之后,节点都会转移到阻塞队列
条件队列节点向阻塞队列转移有两种情况:一是条件队列中的节点通过signal正常转移到阻塞队列;也可能是发生了中断,节点被唤醒后发现自己不是被signal的,也会主动进入阻塞队列中,不过中断状态interruptMode会被记录。PS:后面代码会对中断状态进行响应处理
获取独占锁
我们继续await方法的代码,在前面分析while循环结束后,节点已经进入阻塞队列,准备获取锁。
public final void await() throws InterruptedException {
...
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//acquireQueued 自旋获取锁
//线程将阻塞获取锁,这个方法返回的时候,代表线程已经获取锁了
//这个方法会返回线程是否被中断,如果线程在signal后中断,设置REINTERRUPT
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
//signal中会有node.nextWaiter != null的处理
//但如果await期间发生了中断,条件队列是没有signal代码的处理,节点没有出队,那么就在这里进行出队处理
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
//根据interruptMode处理中断状态
reportInterruptAfterWait(interruptMode);
}
处理中断状态
对中断的两种情况进行处理:
THROW_IE:在await期间线程发生了中断,需要抛出异常,如果不在await期间响应中断,可以使用awaitUninterruptibly方法
REINTERRUPT:代表在await期间没有发生中断,而是在signal后线程发生了中断,重新设置中断状态。
private void reportInterruptAfterWait(int interruptMode)
throws InterruptedException {
if (interruptMode == THROW_IE)
throw new InterruptedException();
else if (interruptMode == REINTERRUPT)
selfInterrupt();
}
不处理中断
可以看到这个方法与await方法不同在不对中断进行响应处理了,条件队列中的线程即使发生了中断依然等待转移到阻塞队列,获取锁后只是重新设置了中断状态而已。
public final void awaitUninterruptibly() {
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
boolean interrupted = false;
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
//这里没有break了,保存了一下interrupted
if (Thread.interrupted())
interrupted = true;
}
if (acquireQueued(node, savedState) || interrupted)
//重新设置中断标识
selfInterrupt();
}
响应中断
我们顺着中断的思路继续思考,假如线程在await期间发生了中断,会重新设置中断标识,之后节点还是会转移到阻塞队列等待获取独占锁,那么线程后面获取了锁后会对中断状态进行什么处理?答案就在acquireQueued方法中:acquireQueued方法返回的是boolean值,返回true表示线程是否发生了中断
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
//能执行到这里说明线程是被中断唤醒的
selfInterrupt();
}
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);
}
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
线程从 LockSupport.park(this);唤醒后只是判断了一下中断状态,如果中断了设置interrupted=true,便进入了下一次for循环,直到拿到锁,将interrupted返回出去,所以只要线程等待锁期间发生过中断。interrupted就等于true。
acquire方法判断返回值为true,会执行selfInterrupt方法,设置了中断状态,至于怎么样响应中断,交给调用者来处理。也就是说,acquire方法本身是不响应中断的,举例线程A在等待获取锁,线程B对其进行了中断,线程A不会立即响应中断,继续等待锁,不过会记录了中断状态,以便调用者进行处理。
AQS中也提供了响应中断的方法acquireInterruptibly,还是上面的场景,不同的是线程A被中断后会取消抢锁,立即抛出InterruptedException 异常。
超时机制
ConditionObject中还有提供带超时机制的await方法,逻辑都差不多,我们选一个来分析
public final boolean await(long time, TimeUnit unit)
throws InterruptedException {
long nanosTimeout = unit.toNanos(time);
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
//计算超时时间
final long deadline = System.nanoTime() + nanosTimeout;
boolean timedout = false;
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
//表示时间已到
if (nanosTimeout <= 0L) {
//将节点转移到阻塞队列去,如果转移成功,返回true表示是超时触发的
//如果返回false表示调用过signal,节点已经被转移了,那就不存在超时了
timedout = transferAfterCancelledWait(node);
break;
}
//spinForTimeoutThreshold=1000L 小于一毫秒就不再parkNanos了
if (nanosTimeout >= spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
//超时时间与当前时间差值
nanosTimeout = deadline - System.nanoTime();
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null)
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
return !timedout;
}
带超时的await通过parkNanos来休眠指定时间,线程被唤醒后检查是否调用过signal,调用过没有超过,否则就是超时了,超时后节点转移到阻塞队列。
总结:
- Condition条件队列是一个单向链表结构,调用await方法的线程会被阻塞执行,包装成节点并加入链表尾部。
- 线程调用signal其实就是将条件队列节点向阻塞队列转移的过程,无论是否发生中断,节点都会转移到阻塞队列,然后阻塞等待获取锁。
- await方法是默认响应中断的,条件队列中的线程如果在await期间被中断,会设置中断状态,并在获取锁后抛出异常。而lock方法是不响应中断的,如果希望响应中断可以使用lockInterruptibly。