线程信令
线程信令的目的是使线程能够相互发送信号。另外,线程信令使线程能够等待来自其他线程的信号。例如,线程B可能会等待线程A发出的信号,表明数据已准备好进行处理。
通过共享对象发信号
线程互相发送信号的一种简单方法是在某些共享对象变量中设置信号值。线程A可以从同步块内部将布尔成员变量hasDataToProcess设置为true,线程B可以在同步块内部读取hasDataToProcess成员变量。这是一个对象的简单示例,该对象可以保存此类信号,并提供设置和检查该信号的方法:
public class MySignal{ protected boolean hasDataToProcess = false; public synchronized boolean hasDataToProcess(){ return this.hasDataToProcess; } public synchronized void setHasDataToProcess(boolean hasData){ this.hasDataToProcess = hasData; } }
线程A和B必须具有对共享MySignal实例的引用,信令才能起作用。如果线程A和B引用了不同的MySignal实例,则它们将不会检测到彼此的信号。可以将要处理的数据放置在与MySignal实例分开的共享缓冲区中。
忙等待
要处理数据的线程B正在等待数据变得可用于处理。换句话说,它正在等待来自线程A的信号,该信号导致hasDataToProcess()返回true。这是线程B在等待此信号时正在运行的循环:
protected MySignal sharedSignal = ... ... while(!sharedSignal.hasDataToProcess()){ //do nothing... busy waiting }
注意while循环如何继续执行,直到hasDataToProcess()返回true。这称为忙等待。等待时线程正忙。
wait(),notify()和notifyAll()
繁忙等待不是在运行等待线程的计算机中非常有效地利用CPU,除非平均等待时间很小。否则,如果等待线程能够以某种方式休眠或者变为非活动状态,直到它接收到正在等待的信号,这将变得更聪明。
Java具有内置的等待机制,该机制使线程在等待信号时变为非活动状态。 java.lang.Object类定义了三种方法,wait(),notify()和notifyAll()来简化此过程。
在任何对象上调用wait()的线程将变为非活动状态,直到另一个线程在该对象上调用notify()为止。为了调用wait()或者通知,调用线程必须首先获取该对象的锁。换句话说,调用线程必须从同步块内部调用wait()或者notify()。这是MySignal的修改版本,称为MyWaitNotify,它使用wait()和notify()。
public class MonitorObject{ } public class MyWaitNotify{ MonitorObject myMonitorObject = new MonitorObject(); public void doWait(){ synchronized(myMonitorObject){ try{ myMonitorObject.wait(); } catch(InterruptedException e){...} } } public void doNotify(){ synchronized(myMonitorObject){ myMonitorObject.notify(); } } }
等待线程将调用doWait(),而通知线程将调用doNotify()。当线程在对象上调用notify()时,在该对象上等待的线程之一将被唤醒并允许执行。还有一个notifyAll()方法,它将唤醒所有在给定对象上等待的线程。
如我们所见,正在等待的线程和通知线程都从同步块中调用了wait()和notify()。这是强制性的!线程不能在未保持调用方法的对象上的锁的情况下调用wait(),notify()或者notifyAll()。如果存在,则抛出IllegalMonitorStateException。
但是,这怎么可能呢?只要等待对象在同步块内执行,它就不会保持对监视对象(myMonitorObject)的锁定吗?等待线程是否不会阻塞通知线程,使其永远不会进入doNotify()中的同步块?答案是不。线程调用wait()后,它将释放它对监视对象持有的锁定。这允许其他线程也调用wait()或者notify(),因为必须从同步块内部调用这些方法。
线程唤醒后,它将无法退出wait()调用,直到调用notify()的线程离开其同步块为止。换句话说:唤醒的线程必须先获得对监视对象的锁定,然后才能退出wait()调用,因为等待调用嵌套在一个同步块中。如果使用notifyAll()唤醒多个线程,一次只能有一个唤醒的线程可以退出wait()方法,因为每个线程必须在退出wait()之前依次获取监视对象的锁。
错过的信号
万一调用时没有线程在等待,方法notify()和notifyAll()不会将方法调用保存到它们中。该通知信号然后就丢失了。因此,如果线程在要发出信号的线程调用wait()之前调用notify(),则等待线程将丢失该信号。这可能是问题,也可能不是问题,但是在某些情况下,这可能会导致等待线程永远等待,永远不会醒来,因为错过了唤醒信号。
为避免丢失信号,应将其存储在信号类中。在MyWaitNotify示例中,通知信号应存储在MyWaitNotify实例内部的成员变量中。这是执行此操作的MyWaitNotify的修改版本:
public class MyWaitNotify2{ MonitorObject myMonitorObject = new MonitorObject(); boolean wasSignalled = false; public void doWait(){ synchronized(myMonitorObject){ if(!wasSignalled){ try{ myMonitorObject.wait(); } catch(InterruptedException e){...} } //clear signal and continue running. wasSignalled = false; } } public void doNotify(){ synchronized(myMonitorObject){ wasSignalled = true; myMonitorObject.notify(); } } }
注意,在调用notify()之前,doNotify()方法现在如何将wasSignalled变量设置为true。另外,请注意doWait()方法现在如何在调用wait()之前检查wasSignalled变量。实际上,如果在先前的doWait()调用与此调用之间未接收到任何信号,则仅调用wait()。
虚假唤醒
出于无法解释的原因,即使未调用notify()和notifyAll(),线程也可能会唤醒。这就是所谓的虚假唤醒。没有任何原因的唤醒。
如果MyWaitNofity2类的doWait()方法中发生虚假唤醒,则等待线程可能会继续处理而未收到执行该操作的适当信号!这可能会在应用程序中引起严重的问题。
为了防止虚假唤醒,请在while循环内而不是if语句内检查signal成员变量。这样的while循环也称为自旋锁。唤醒的线程一直旋转,直到旋转锁(while循环)中的条件变为false为止。这是MyWaitNotify2的修改版本,显示了此内容:
public class MyWaitNotify3{ MonitorObject myMonitorObject = new MonitorObject(); boolean wasSignalled = false; public void doWait(){ synchronized(myMonitorObject){ while(!wasSignalled){ try{ myMonitorObject.wait(); } catch(InterruptedException e){...} } //clear signal and continue running. wasSignalled = false; } } public void doNotify(){ synchronized(myMonitorObject){ wasSignalled = true; myMonitorObject.notify(); } } }
注意,wait()调用现在是如何嵌套在while循环中而不是if语句中的。如果等待线程在没有收到信号的情况下唤醒,则wasSignalled成员仍为false,而while循环将再次执行,从而使唤醒的线程返回等待状态。
多线程等待相同的信号
如果我们有多个等待的线程,而while循环也是一个不错的解决方案,这些线程都使用notifyAll()唤醒了,但是只允许其中一个继续运行。一次只有一个线程将能够获得监视对象的锁定,这意味着只有一个线程可以退出wait()调用并清除wasSignalled标志。一旦此线程退出doWait()方法中的同步块,其他线程便可以退出wait()调用并检查while循环内的wasSignalled成员变量。但是,第一个线程唤醒会清除此标志,因此其余唤醒的线程将返回等待状态,直到下一个信号到达为止。
不要在常量String或者全局对象上调用wait()
本文的早期版本具有MyWaitNotify示例类的版本,该示例类使用常量字符串("")作为监视对象。这是该示例的外观:
public class MyWaitNotify{ String myMonitorObject = ""; boolean wasSignalled = false; public void doWait(){ synchronized(myMonitorObject){ while(!wasSignalled){ try{ myMonitorObject.wait(); } catch(InterruptedException e){...} } //clear signal and continue running. wasSignalled = false; } } public void doNotify(){ synchronized(myMonitorObject){ wasSignalled = true; myMonitorObject.notify(); } } }
在空字符串或者任何其他常量字符串上调用wait()和notify()的问题是,JVM /编译器在内部将常量字符串转换为同一对象。这意味着,即使我们有两个不同的MyWaitNotify实例,它们都引用相同的空字符串实例。这也意味着在第一个MyWaitNotify实例上调用doWait()的线程有被第二个MyWaitNotify实例上的doNotify()调用唤醒的风险。
请记住,即使4个线程在同一个共享字符串实例上调用wait()和notify(),来自doWait()和doNotify()调用的信号也分别存储在两个MyWaitNotify实例中。在MyWaitNotify 1上执行doNotify()调用可能会唤醒在MyWaitNotify 2中等待的线程,但是信号只会存储在MyWaitNotify 1中。
起初,这似乎不是一个大问题。毕竟,如果在第二个MyWaitNotify实例上调用doNotify(),那么真正发生的一切就是错误地唤醒了线程A和B。唤醒的线程(A或者B)将在while循环中检查其信号,然后返回等待状态,因为在第一个MyWaitNotify实例中未在其上等待doNotify()。这种情况等同于引起虚假唤醒。线程A或者B在未发出信号的情况下唤醒。但是代码可以处理此问题,因此线程可以返回等待状态。
问题在于,由于doNotify()调用仅调用notify()而不是notifyAll(),因此即使有4个线程在同一字符串实例(空字符串)上等待,也仅唤醒了一个线程。因此,如果实际上是针对C或者D的信号唤醒了线程A或者B中的一个,则唤醒的线程(A或者B)将检查其信号,看到没有收到信号,然后返回等待状态。 C或者D都不会醒来以检查他们实际接收到的信号,因此会丢失该信号。这种情况等于前面所述的信号丢失问题。 C和D发送了一个信号,但没有响应。
如果doNotify()方法已调用notifyAll()而不是notify(),则所有等待线程已被唤醒并依次检查信号。线程A和B将回到等待状态,但是C或者D中的一个将注意到该信号并留下doWait()方法调用。 C和D中的另一个将返回等待状态,因为发现信号的线程会在退出doWait()的过程中将其清除。
然后,我们可能很想总是调用notifyAll()而不是notify(),但这在性能上是一个坏主意。当只有一个线程可以响应该信号时,没有理由唤醒所有正在等待的线程。
因此:不要将全局对象,字符串常量等用于wait()/ notify()机制。使用使用该构造唯一的对象。例如,每个MyWaitNotify3(前面部分中的示例)实例都有自己的MonitorObject实例,而不是将空字符串用于wait()/ notify()调用。