9.线程间协作:wait/notify
1.wait/notify的作用与用法
1.1 wait
一个线程因其执行目标动作所需的保护条件未满足而被暂停的过程就被称为等待(Wait)。一个线程更新了系统的状态,使得其他线程所需的保护条件得以满足的时候唤醒那些被暂停的线程的过程就被称为通知 (Notify)。Object.wait()/Object.wait(long) 以及Object.notify()/Object.notifyAII()可用于实现等待和通知:Object.wait()的作用是使其执行线程被暂停(其生命周期状态变更为WAITING), 该方法可用来实现等待;Object.notify()的作用是唤醒一个被暂停的线程,调用该方法可实现通知。相应地,Object.wait() 的执行线程就被称为等待线程;Object.notify()的执行线程就被称为通知线程。由于Object类是 Java 中任何对象的父类,因此使用Java中的任何对象都能够实现等待与通知。
使用Object.wait()实现等待的模板代码:
//在调用wait方法前获得相应对象的内部锁
synchroized(someObject){
while(保护条件不成立){
//调用Object.wait()暂停当前线程
someObjec.wait();
}
//代码执行到这里说明保护条件已经满足
//执行目标动作
doAction();
}
someObject.wait()会以原子操作的方式使其执行线程(当前线程)暂停并使该线程释放其持有的 someObject 对应的内部锁。当前线程被暂停的时候其对 someObject.wait() 的调用并未返回。其他线程在该线程所需的保护条件成立的时候执行相应的 notify 方法,即 someObject.notify() 可以唤醒someObject上的一个(任意的)等待线程。被唤醒的等待线程在其占用处理器继续运行的时候,需要再次申请 someObject 对应的内部锁。被唤醒的线程在其再次持有 someObject对应的内部锁的情况下继续执行someObject.wait() 中剩余的指令,直到 wait 方法返回 。
等待线程在其被唤醒、继续运行到其再次持有相应对象的内部锁的这段时间内,由于其他线程可能抢先获得相应的内部锁并更新了相关共享变量而导致该线程所需的保护条件又再次不成立, 因此 Object. wait()调用返回之后我们需要再次判断此时保护条件是否成立。所以,对保护条件的判断以及 Object.wait()调用应该放在循环语句之中,以确保目标动作只有在保护条件成立的情况下才能够执行!
Object.wait(long)允许我们指定一个超时时间(单位为亳秒)。如果被暂停的等待线程在这个时间内没有被其他线程唤醒,那么 Java 虚拟机会自动唤醒该线程 。
1.2 notify
使用 Object.notify()实现通知,其代码模板如下伪代码所示:
synchronized(someObject){
//更新等待线程的保护条件涉及的共享变量
updateSharedState();
//唤醒其他线程
someObject.notify();
}
Object.notify()的执行线程持有的相应对象的内部锁只有在Object.notify()调用所在的临界区代码执行结束后才会被释放,而Object.notify()本身并不会将这个内部锁释放。因此,为了使等待线程在其被唤醒之后能够尽快再次获得相应的内部锁,我们要尽可能地将 Object.notify()调用放在靠近临界区结束的地方。调用 Object.notify()所唤醒的线程仅是相应对象上的一个任意等待线程,所以这个被唤醒的线程可能不是我们真正想要唤醒的那个线程 。 因此,有时候我们需要借助Object.notify() 的兄弟—-Object.notifyAll(), 它可以唤醒相应对象上的所有等待线程。
2. wait/notify的开销及问题
- 过早唤醒问题。这种等待线程在其所需的保护条件并未成立的情况下被唤醒的现象就被称为过早唤醒 (Wakeup too soon) 。过早唤醒使得那些本来无须被唤醒的等待线程也被唤醒了,从而造成资源浪费。过早唤醒问题可以利用 JDKl.5 引入的 java.util.concurrent.locks.Condition 接口来解决。
- 信号丢失问题。如果等待线程在执行 Object.wait()前没有先判断保护条件是否已然成立,那么有可能出现这种情形——通知线程在该等待线程进入临界区之前就已经更新了相关共享变量,使得相应的保护条件成立并进行了通知,但是此时等待线程还没有被暂停,自然也就无所谓唤醒了。这就可能造成等待线程直接执行Object.wait()而被暂停的时候,该线程由于没有其他线程进行通知而一直处于等待状态。 信号丢失的另外一个原因是在应该调用 Object.notifyAll() 的地方却调用了Object.notify() 。总的来说,信号丢失本质上是一种代码错误,而不是 Java 标准库 API自身的问题 。
- 欺骗性唤醒问题。等待线程也可能在没有其他任何线程执行Object.notify()/notifyAII() 的情况下被唤醒。 原因是由于操作系统。只要我们将对保护条件的判断和Object.wait()调用行放在一个循环语句之中,欺骗性唤醒就不会对我们造成实际的影响 。
- 上下文切换问题。wait/notify的使用可能导致较多的上下文切换。
3.Object.notify()/notifyAII()的选用
Object.notify()唤醒的是其所属对象上的—个任意等待线程。Object.notify()本身在唤醒线程时是不考虑保护条件的。Object.notifyAll()方法唤醒的是其所属对象上的所有等待线程。使用 Object.notify()替代Object.notifyAll()时需要确保以下两个条件同时得以满足:
- 一次通知仅需要唤醒至多—个线程。
- 相应对象上的所有等待线程都是同质等待线程 。所谓同质等待线程指这些线程使用同一个保护条件,并且这些线程在 Object. wait()调用返回之后的处理逻辑一致。