在理解了线程之间可能存在相互冲突,以及怎样避免冲突之后,下一步就是学习怎样使线
程之间相互协作。这种协作关键是通过线程之间的握手来进行的,这种握手可以通过
Object的方法wait( )和notify()来安全的实现。
调用sleep()的时候锁并没有被释放,理解这一点很重要。另一方面,wait( )方法的
确释放了锁,这就意味着在调用wait()期间,可以调用线程中对象的其他同步控制方法。
当一个线程在方法里遇到了对wait()的调用的时候,线程的执行被挂起,对象上的锁被
释放。
有两种形式的wait( )。第一种接受毫秒作为参数,意思与 sleep()方法里参数的意思
相同,都是指“在此期间暂停” 。不同之处在于,对于 wait():
1.在 wait( )期间锁是释放的。
2.你可以通过notify( )、notifyAll(),或者时间到期,从wait( )中恢
复执行。
第二种形式的wait( )不要参数;这种用法更常见。wait( )将无限等待直到线程接收到
notify( )或者notifyAll( )消息。
wait( ), notify( ),以及notifyAll( )的一个比较特殊的方面是这些方法是基类
Object的一部分,而不是像Sleep( )那样属于Thread的一部分。尽管开始看起来有点
奇怪,仅仅针对线程的功能却作为通用基类的一部分而实现,不过这是有道理的,因为这
些功能要用到的锁也是所有对象的一部分。所以,你可以把wait( )放进任何同步控制方
法里,而不用考虑这个类是继承自Thread还是实现了Runnable接口。实际上,你只能在
同步控制方法或同步控制块里调用wait(), notify( )和notifyAll()(因为不用
操作锁,所以sleep()可以在非同步控制方法里调用)。如果你在非同步控制方法里调用
这些方法,程序能通过编译,但运行的时候,你将得到IllegalMonitorStateException
异常,伴随着一些含糊的消息,比如“当前线程不是拥有者”。消息的意思是,调用 wait( ),
notify( )和notifyAll( )的线程在调用这些方法前必须“拥有”(获取)对象的锁。
你能够让另一个对象执行这种操作以维护其自己的锁。要这么做的话,你必须首先得到对
象的锁。比如,如果你要在对象x上调用notify( ),那么你就必须在能够取得x的锁的同
步控制块中这么做:
synchronized(x) {
x.notify();
}
特别地,当你在等待某个条件,这个条件必须由当前方法以外的因素才能改变的时候(典
型地,这个条件被另一个线程所改变),就应该使用wait( )。你也不希望在线程里测试
条件的时候空等;这也称为“忙等”,它会极大占用CPU时间。所以wait( )允许你在等待
外部条件的时候,让线程休眠,只有在收到notify( )或notifyAll()的时候线程才唤
醒并对变化进行检查。所以,wait()为在线程之间进行同步控制提供了一种方法。
例如,考虑一个餐馆,有一个厨师和一个服务员。服务员必须等待厨师准备好食物。当厨
师准备好食物的时候,他通知服务员,后者将得到食物然后继续等待。这是一个线程协作
的极好的例子:厨师代表了生产者,服务员代表了消费者。WaitPerson(服务员)必须知道自己所工作的Restaurant(餐馆),因为他们必须从餐馆
的“订单窗口”取出订单restaurant.order。在run( )中,WaitPerson调用wait( )
进入等待模式,停止线程的执行直到被Chef(厨师)的notify( )方法所唤醒。因为是很
简单的程序,我们知道只有一个线程在等待WaitPerson对象的锁:即WaitPerson线程
自己。正因为这个原因,使得调用notify()是安全的。在更复杂的情况下,多个线程可
能在等待同一个特定的锁,所以你不知道哪个线程被唤醒。解决方法是调用notifyAll( ),
它将唤醒所有等待这个锁的线程。每个线程必须自己决定是否对这个通知作出反应。
注意对wait( )的调用被包装在一个while( )语句里,它在测试的正是等待的条件。开
始看起来可能很奇怪--如果你在等一个订单,那么一旦你被唤醒,订单必须是可用的,
对不对?问题是在多线程程序里,一些别的线程可能在WaitPerson苏醒的同时冲进来抢
走订单。唯一安全的方法就是对于wait()总是使用如下方式:
while(conditionIsNotMet)
wait( );
这可以保证在你跳出等待循环之前条件将被满足,如果你被不相干的条件所通知(比如
notifyAll( )),或者在你完全退出循环之前条件已经被改变,你被确保可以回来继续
等待。
一个Chef对象必须知道他/她工作的餐馆(这样可以通过restaurant.order下订单)和
取走食物的WaitPerson,这样才能在订单准备好的时候通知WaitPerson。在这个简化
过的例子中,由Chef产生订单对象,然后通知WaitPerson订单已经准备好了。
请注意对notify()的调用必须首先获取WaitPerson对象的锁。WaitPerson.run( )
里对wait( )的调用将自动释放这个锁,所以这是可能的。因为要调用notify( )必须获
取锁,这就能保证如果两个线程试图在同一个对象上调用notify( )时不会互相冲突。
上面的例子中,一个线程只有一个单一的地点来存储某个对象,这样另一个线程就可以在
以后使用这个对象。然而,在一个典型的生产者-消费者实现中,你要使用先进先出的队
列来存放被生产和消费的对象。