在jdk1.5以后并发包中提供了Lock接口,Condition接口与Lock配合使用可以实现等待/通知模式,在此之前是使用定义在Object对象上的一组监视器方法,主要包括:wait()、wait(long timeout)、notify()以及notifyAll()方法,这些方法synchronized结合使用,也可以实现等待/通知。
Object的监视器方法与Condition接口的对比如下(图片截取自Java并发编程的艺术)
Condition Demo
public class ConditionTest {
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void conditionWait() throws InterruptedException {
lock.lock();
try {
System.out.println(Thread.currentThread());
condition.await();
System.out.println("await");
} finally {
lock.unlock();
}
}
public void conditionSignal() throws InterruptedException {
lock.lock();
try {
System.out.println(Thread.currentThread());
condition.signal();
System.out.println("siganl");
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
ConditionTest conditionTest = new ConditionTest();
new Thread(new Runnable() {
@Override
public void run() {
try {
conditionTest.conditionWait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}) {
public String toString() {
return getName();
}
}.start();
new Thread(new Runnable() {
@Override
public void run() {
try {
conditionTest.conditionSignal();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}) {
public String toString() {
return getName();
}
}.start();
}
}
运行结果:
从上面的demo比较容易得出:
1、一般都会将Condition对象作为成员变量。
2、Thread-1调用await()方法后当前线程会释放锁并等待。
3、线程Thread-1调用signal()方法通知Thread-0,Thread-0从await()返回,在返回前已经获取到了锁。
上面demo中lock.newCondition() 其实返回的是Condition的一个实现:AQS中的ConditionObject
public class ConditionObject implements Condition, java.io.Serializable {
private static final long serialVersionUID = 1173984872572414699L;
//这里Condition维护了自己的等待队列
/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;
...省略后续的代码...
一个ConditionObject包含一个等待队列,ConditionObject包括首节点和尾节点,等待队列是一个FIFO队列,如果一个线程调用Condition.await()方法,那么该线程将会释放锁,构造成节点加入等待队列并进入等待状态,并将该节点从尾部加入等待队列。节点的定义复用了同步器中节点的定义,也就是说,同步队列和等待队列中节点类型都是同步器的静态内部类AbstractQueuedSynchronizer.Node。
等待队列的基本结构如图,同步队列的结构图可以参见AQS与简介源码分析
Condition接口提供了如下方法:
public interface Condition {
//当前线程进入等待直到被通知或者中断
void await() throws InterruptedException;
//跟上面的区别是对中断不敏感
void awaitUninterruptibly();
//当前线程进入等待状态直到被通知,返回值表示剩余时间,如果在nanosTimeout纳秒之前被唤醒
//那么返回值就是nanosTimeout-实际时间,如果返回值是0或者是负数表示已经超时
long awaitNanos(long nanosTimeout) throws InterruptedException;
//当前线程进入等待状态直到被通知,中断或者经过指定的等待时间,在等待时间之前被唤醒返回true,时间到了则返回false,可以自定义超时时间的单位
boolean await(long time, TimeUnit unit) throws InterruptedException;
//当前线程进入等待状态直到被通知,中断或者到某个时间,如果没有到指定时间就被通知,返回true,否则到达指定时间返回false
boolean awaitUntil(Date deadline) throws InterruptedException;
//唤醒一个等待在Condition上的线程,该方法从等待方法返回前必须获得与Contion相关联的锁
void signal();
//唤醒所有等待在Condition上的线程,能够从等待方法返回前必须获得与Contion相关联的锁
void signalAll();
}
等待
调用Condition的await()方法(或者以await开头的方法),会使当前线程进入等待队列并释放锁,同时线程状态变为等待状态。当从await()方法返回时,当前线程一定获取了Condition相关联的锁。
如果从队列(同步队列和等待队列)的角度看await()方法,当调用await()方法时,相当于同步队列的首节点(获取了锁的节点)移动到Condition的等待队列中。
public final void await() throws InterruptedException {
//如果线程被中断则抛出异常
if (Thread.interrupted())
throw new InterruptedException();
//将当前线程构造成节点加入到等待队列节点中
Node node = addConditionWaiter();
//释放当前线程占有的锁
int savedState = fullyRelease(node);
int interruptMode = 0;
//判断当前节点是否在AQS队列中,如果不在就继续挂起
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
//如果中断则退出循环
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//竞争尝试获取锁,如果没有获取到则继续阻塞等待被唤醒再次竞争锁
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
//清理Contidion维护的等待队列
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
final boolean isOnSyncQueue(Node node) {
//如果node节点的状态为CONDITION,表示节点状态正常没有被唤醒,此刻节点还没有进入AQS队列中
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
//如果当前节点的下一个节点不为空的话,那么该节点肯定在AQS队列中
if (node.next != null) // If has successor, it must be on queue
return true;
//循环遍历,返回ture表示当前节点在AQS对列中
return findNodeFromTail(node);
}
通知
调用Condition的signal()方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移到同步队列中。
public final void signal() {
//判断当前线程是否为拥有锁的独占式线程,如果不是则抛出异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//排队唤醒,最开始唤醒Condition维护的队列中的头节点
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
private void doSignal(Node first) {
do {
//如果头节点的下一个节点为null,则将头节点和其尾节点置null
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
final boolean transferForSignal(Node node) {
//cas设置node节点从contion状态改为初始
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
//通过自旋的方式将节点加入到AQS队列中
//等待队列中的头节点线程安全地移动到同步队列。当节点移动到同步队列后,当前线程再使用LockSupport唤醒该节点的线程。
Node p = enq(node);
int ws = p.waitStatus;
//如果当前节点的上个节点的waitStatus大于1则说明该节点所对应的线程等待超时或者被中断需要从同步队列中取消等待,如果CAS修改失败则直接唤醒
//if语句一般走不到这里,唤醒的操作一般在lock.unlock()中
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
总结
Demo中的具体流程如下:
1、Thread-1调用lock.lock()获取到锁,内部调用acquireQueued方法,线程被加入到AQS的同步队列中。
2、Thread-1调用await方法时,锁释放,该线程锁构成的节点从AQS的同步队列中移除,通过addConditionWaiter()方法加入到Condition的等待队列中,等待着被通知的信号。
3、Thread-1释放锁唤醒tread-2获取到锁,加入到AQS的同步队列中,处理业务
4、Tread-2调用signal方法,Condition的等待队列中只有Thread-1一个节点,通过调用enq(Node node)方法加入到AQS的同步队列中,此时Thread-1 并没有被唤醒,唤醒的操作是在finall块中的lock.unlock()中。
5、Tread-2调用lock.unLock()方法,释放锁,Thread-1被唤醒并获取到锁从await()方法返回继续处理业务。
6、Thread-1调用unlock释放锁,结束整个流程。
参考文章:
Doug Lea:《Java并发编程实战》
方腾飞、魏鹏、程晓明:《并发编程的艺术》