在前面已经对 wait() , notify() 和 notifyAll() 进行了讲解,并得出了等待/通知机制的基本范式,接下来就对如何得到此范式做一个分析。
线程信令的目的是使线程能够相互发送信号。另外,线程信令使线程能够等待来自其他线程的信号。例如,线程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的信号,线程A能够让hasDataToProcess()
方法返回true。这是线程B在等待此信号时运行的循环:
protected MySignal sharedSignal = ...
...
while(!sharedSignal.hasDataToProcess()){
//do nothing... busy waiting
}
注意while循环如何一直执行,直到hasDataToProcess()
返回true,这称为忙等待,等待时线程正忙。
wait(), notify() and notifyAll()
等待线程忙等待运行时不能有效地利用计算机的CPU,除非平均等待时间非常短。否则,如果等待的线程能以某种方式睡眠或变为非活动状态,直到它收到它正在等待的信号,那将更加智能。
Java有一个内置的等待机制,可以让线程在等待信号时变为非活动状态。java.lang.Object类定义了三个方法,wait()
,notify()
和notifyAll()
,以方便这一点。
在任何对象上调用wait()
的线程将变为非活动状态,直到另一个线程在该对象上调用notify()
。为了调用wait()
或通知调用线程必须首先获取该对象的锁。换句话说,调用线程必须从同步块内部调用wait()
或notify()
。这是一个名为MyWaitNotify的MySignal的修改版本,它使用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() ,因为必须从synchronized块内调用这些方法。
一旦线程被唤醒,它不能立刻退出 wait() 调用,直到调用 notify() 的线程离开其synchronized块。换句话说:被唤醒的线程必须重新获取监视器对象上的锁才能退出 wait() 调用,因为等待调用嵌套在同步块中。如果使用 notifyAll() 唤醒多个线程,则一次只有一个被唤醒的线程可以退出 wait() 方法,因为每个线程必须在退出 wait() 之前依次获取监视器对象上的锁。
信号丢失
当在调用 notify() 和 notifyAll() 方法时如果没有线程在等待,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();
}
}
}
注意 doNotify() 方法现在在调用 notify() 之前将wasSignalled
变量设置为true。另外,注意 doWait() 方法现在在调用 wait() 之前检查wasSignalled
变量。事实上,如果在 doWait() 调用之前和期间没有收到信号,它只调用 wait() 。
意外唤醒
由于莫名其妙的原因,即使没有调用 notify() 和 notifyAll() ,也可以唤醒线程,这被称为虚假唤醒,线程没有任何理由的醒来。
如果在MyWaitNofity2
类的 doWait() 方法中发生虚假唤醒,则等待线程在没有收到正确的信号时也可以继续处理,这可能会导致应用程序出现严重问题。
为防止虚假唤醒,在while循环内而不是if语句内部检查信号成员变量。这样的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() 方法中的synchronized块,其他线程也可以获取锁然后退出 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 / Compiler在内部将常量字符串转换为同一对象。这意味着,即使您有两个不同的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() ,所以信号没有被改变,设置为true。这种情况等于主动的虚假唤醒。线程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() 调用。
下面是一个说明上面问题的示例:
import org.junit.Test;
import static java.lang.Thread.sleep;
public class MyWaitNotify {
private final String myMonitorObject = "";
boolean wasSignalled = false;
public void doWait() {
synchronized(myMonitorObject) {
while(!wasSignalled) {
try{
myMonitorObject.wait();
System.out.println(Thread.currentThread().getName() + " is notified");
} catch(InterruptedException e) {
e.printStackTrace();
}
}
//clear signal and continue running.
wasSignalled = false;
}
}
public void doNotify() {
synchronized(myMonitorObject) {
wasSignalled = true;
myMonitorObject.notify();
}
}
@Test
public void test() throws Exception {
MyWaitNotify myWaitNotify1 = new MyWaitNotify();
MyWaitNotify myWaitNotify2 = new MyWaitNotify();
Thread thread1 = new Thread(() -> {
myWaitNotify1.doWait();
System.out.println(Thread.currentThread().getName() + " is notified successfully");
}, "Thread-1");
Thread thread2 = new Thread(() -> {
myWaitNotify1.doWait();
System.out.println(Thread.currentThread().getName() + " is notified successfully");
}, "Thread-2");
Thread thread3 = new Thread(() -> {
myWaitNotify2.doWait();
System.out.println(Thread.currentThread().getName() + " is notified successfully");
}, "Thread-3");
Thread thread4 = new Thread(() -> {
myWaitNotify2.doNotify();
System.out.println(Thread.currentThread().getName() + " notify");
}, "Thread-4");
thread1.start();
thread2.start();
thread3.start();
//等待三个线程充分运行,即期待他们都已经在等待
sleep(1000);
thread4.start();
sleep(1000);
}
}
输出结果如下:
Thread-4 notify
Thread-1 is notified
可能需要多次运行才会出现线程1或线程2被通知的情况。
经过此章内容的讲解,相信对等待/通知的范式是如何形成的,有了一个充分的认知。