11.2.2 Object类的wait、notify和notifyAll方法
使用synchronized关键词主要用来实现线程之间的互斥,即同一时刻只有一个线程允许执行特定的代码。通过互斥的方式来保证多个线程访问共享变量时的正确性。除了互斥访问之外,线程之间也需要通过协作的方式来完成某些任务。比如在典型的生产者-消费者场景中,生产者线程在缓冲区已满时需要等待,等消费者线程从缓冲区中取走数据之后,再进行生产;消费者线程在缓冲区已空时需要等待,等生产者线程向缓冲区中放入数据之后,再进行消费。这种协作方式可以抽象为等待-通知机制。在线程运行时,可能需要满足某些逻辑条件才能继续进行。当线程所要求的条件不满足时,线程进入等待状态,等待由于其他线程的运行而使条件得到满足;其他线程则负责在合适的时机发出通知来唤醒处于等待状态的线程,表明等待线程所要求的条件已经满足。这是一种在多线程程序中经常会遇到的典型场景,即判断是否满足条件之后再选择继续运行。可以用代码清单11-3中的while循环和volatile变量来处理这个场景。但是这种做法的本质是让线程处于忙等待的状态,并通过轮询的方式来判断条件是否满足。处于忙等待状态的线程仍然需要占用CPU时间,会对性能造成影响。更好的做法是使用Object类提供的wait、notify和notifyAll方法。
由于wait方法是在Object类中定义的,因此可以调用任何Java对象的wait方法。使用wait方法的关键在于理解调用wait方法的含义。Java中的每个对象除了有与之关联的监视器对象之外,还有一个与之关联的包含线程的等待集合。在调用wait方法时,该方法调用的接收者所关联的监视器对象是所使用的监视器对象,同时wait方法所影响的是执行wait方法调用的当前线程。成功调用wait方法的先决条件是当前线程获取到监视器对象上的锁。如果没有锁,则直接抛出java.lang.IllegalMonitorStateException异常,wait方法调用失败;如果有锁,那么当前线程被添加到对象所关联的等待集合中,并释放其持有的监视器对象上的锁。当前线程被阻塞,无法继续执行,直到被从对象所关联的等待集合中移除。
由于wait方法的成功调用需要当前线程持有监视器对象上的锁,因此wait方法的调用需要放在使用synchronized关键词声明的方法或代码块中。当执行wait方法时,当前线程已经进入了synchronized关键词所声明的互斥块中,已经持有所需的锁。在synchronized方法或代码块中使用的监视器对象必须是wait方法调用的接收者所关联的监视器对象。
通过调用wait方法进入的等待状态分成无超时和有超时两种。如果线程处于有超时的等待状态,那么线程除了可以被主动唤醒而离开等待状态之外,设定的超时时间过去后也会自动离开等待状态。在设定超时时间时可以指定毫秒数和纳秒数。
wait方法的作用是使当前线程进入等待状态,对应的notify和notifyAll方法用来通知线程离开等待状态。调用一个对象的notify方法会从该对象关联的等待集合中选择一个线程来唤醒。被唤醒的线程可以和其他线程竞争运行的机会。与notify方法相对应的notifyAll方法会唤醒对象关联的等待集合中的所有线程。而notify方法所唤醒的线程的选择由虚拟机实现来决定,不能保证一个对象所关联的等待集合中的线程按照所期望的顺序被唤醒。很可能一个线程被唤醒之后,发现它所要求的条件并没有满足,而重新进入等待状态,而真正需要被唤醒的线程却仍然处于等待集合中。因此,当等待集合中可能包含多个线程时,一般使用notifyAll方法。不过notifyAll方法会导致线程在没有必要的情况下被唤醒,之后又马上进入等待状态,因此会造成一定的性能影响,不过可以保证程序的正确性。与wait方法相同,notify和notifyAll方法在调用时都要求当前线程拥有方法调用接收者所关联的监视器对象上的锁。当线程被唤醒之后,由于在调用wait方法时已经释放了之前所持有的监视器对象上的锁,线程需要重新竞争锁来获得继续运行wait方法调用完成之后的代码的机会。
需要注意的一个情况是,处于某个对象所关联的等待集合中的线程可能被意外唤醒。这种唤醒不是以开发人员可以预期的方式发生的,而是由底层操作系统和虚拟机内部实现所产生的非正常行为。这种意外的唤醒无法避免,需要开发人员来处理。以生产者-消费者场景为例,当缓冲区已满时,生产者线程处于等待状态。消费者线程在从缓冲区中取走数据之后,唤醒生产者线程。但是,当生产者线程被唤醒时,并不意味着缓冲区肯定是不满的,因为生产者线程有可能在缓冲区已满时被意外唤醒。因此,通常要把wait方法的调用包含在一个循环中。循环的条件是线程可以继续执行需要满足的逻辑条件。如果线程继续执行的逻辑条件不满足,那么线程应该再次调用wait方法来重新进入等待状态。代码清单11-7给出了wait方法的一般使用方式。
代码清单11-7 把wait方法调用置于循环之中
synchronized(obj){
while(/逻辑条件不满足/){
obj.wait();
}
//条件满足
}