Java并发编程实战 04 | 使用WaitNotify时要注意什么?
在 Java 中,wait()、notify() 和 notifyAll() 方法在多线程编程中主要用于线程间的协作和同步。理解这些方法的使用特点对于编写稳定的多线程程序至关重要。我们将从以下三个问题入手深入探讨它们的使用:
- 为什么必须在 synchronized 代码块中使用 wait() 方法?
- 为什么 wait 方法需要在循环中使用?
- wait/notify 和 sleep 方法之间的相似点和不同点?
为什么必须在 synchronized 代码块中使用 wait() 方法?
为了找到这个问题的答案,让我们反过来思考:如果我们不要求在synchronized
代码块中使用wait
方法,会发生什么问题?让我们看看这段代码。
public class QueueDemo {
Queue<String> buffer = new LinkedList<String>();
public void save(String data) {
buffer.add(data);
// // 因为可能有线程在等待 take(),所以通知它们
notify();
}
public String take() throws InterruptedException {
while (buffer.isEmpty()) {
wait();
}
return buffer.remove();
}
}
代码中有两个方法:save 和 take。save 方法负责将数据添加到 buffer 中,并调用 notify 方法来唤醒之前等待的线程。
take 方法则检查 buffer 是否为空。如果为空,则进入等待状态;如果不为空,则从 buffer 中获取一个数据项。
这是一个典型的生产者-消费者模式,我将在后续的文章中详细探讨这一模式。
然而,这段代码没有受到 synchronized 关键字的保护,可能会出现以下情况:
-
首先,消费者线程调用take方法,在take方法中通过buffer.isEmpty()判断buffer是否为空,如果为空,线程要进入等待状态,但是如果线程在调用wait方法之前就被调度器挂起了,此时方法wait还未执行。
-
与此同时,生产者线程开始运行,并执行 save 方法。它向 buffer 中添加数据,并调用 notify 方法。然而,由于消费者线程的 wait 方法还未执行,因此notify 调用没有任何效果,因为没有任何线程在等待唤醒。
-
接着,之前被调度器挂起的消费者线程恢复执行,并调用 wait 方法,进入等待状态。错过了先前的唤醒。
虽然消费者在调用 wait 方法之前已经判断了 buffer.isEmpty 的条件,但当 wait 方法实际执行时,之前的判断结果已经过期,因为 buffer 的状态可能已经发生了变化。
这里的“判断-执行”并不是一个原子操作,中途可能被打断,这导致了线程的不安全性。在这种情况下,消费者线程可能由于错过了生产者的 notify 调用而陷入无尽的等待状态。
你可以分别调用这两个方法来模拟一个生产者线程和一个消费者线程:
public class QueueDemo2 {
Queue<String> buffer = new LinkedList<String>();
public void save(String data) {
System.out.println("Produce a data");
buffer.add(data);
notify(); // Since someone may be waiting in take()
}
public String take() throws InterruptedException {
System.out.println("Try to consume a data");
while (buffer.isEmpty()) {
wait();
}
return buffer.remove();
}
public static void main(String[] args) throws InterruptedException {
QueueDemo2 queueDemo = new QueueDemo2();
Thread producerThread = new Thread(() -> {
queueDemo.save("Hello World!");
});
Thread consumerThread = new Thread(() -> {
try {
System.out.println(queueDemo.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
});
consumerThread.start();
producerThread.start();
}
}
//输出:
Try to consume a data
Produce a data
Exception in thread "Thread-0" Exception in thread "Thread-1" java.lang.IllegalMonitorStateException
at java.lang.Object.notify(Native Method)
at thread.basic.chapter4.QueueDemo2.save(QueueDemo2.java:13)
at thread.basic.chapter4.QueueDemo2.lambda$main$0(QueueDemo2.java:28)
at java.lang.Thread.run(Thread.java:748)
java.lang.IllegalMonitorStateException
at java.lang.Object.wait(Native Method)
at java.lang.Object.wait(Object.java:502)
at thread.basic.chapter4.QueueDemo2.take(QueueDemo2.java:19)
at thread.basic.chapter4.QueueDemo2.lambda$main$1(QueueDemo2.java:33)
值得庆幸的是,你根本没有犯错的机会!因为如果 wait 方法和 notify 方法在没有被 synchronized 关键字保护的代码块中执行,Java 会直接抛出 java.lang.IllegalMonitorStateException 异常。
为了解决这个问题,我们需要对代码进行修改:
public class SyncQueueDemo2 {
Queue<String> buffer = new LinkedList<>();
public synchronized void save(String data) {
System.out.println("Produce a data");
buffer.add(data);
notify(); // Since someone may be waiting in take()
}
public synchronized String take() throws InterruptedException {
System.out.println("Try to consume a data");
while (buffer.isEmpty()) {
wait();
}
return buffer.remove();
}
public static void main(String[] args) throws InterruptedException {
SyncQueueDemo2 queueDemo = new SyncQueueDemo2();
Thread producerThread = new Thread(() -> {
queueDemo.save("Hello World!");
});
Thread consumerThread = new Thread(() -> {
try {
System.out.println(queueDemo.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
});
consumerThread.start();
producerThread.start();
}
}
//输出:
Produce a data
Try to consume a data
Hello World!
如您所见,程序成功运行,并将“Hello World!”正确打印到控制台。
为什么 wait 方法需要在循环中使用?
当线程调用 wait 方法后,有可能会发生“虚假唤醒”的情况,即线程可能在没有接收到 notify 或 notifyAll 的情况下被意外唤醒,而这是我们不希望看到的。
尽管在实际环境中发生虚假唤醒的概率很小,但程序仍然需要确保在这种情况下的正确性。因此,我们使用 while 循环结构来反复检查等待条件,从而保证线程在被唤醒时,只有在条件满足的情况下才会继续执行。
while (condition does not hold)
obj.wait();
这样,即便是被误唤醒了,也会再次检查 while 中的条件,如果条件不满足,则继续 wait,这样就杜绝了误唤醒的风险。
wait/notify 和 sleep 方法之间的相似点和不同点?
以下是 wait 方法和 sleep 方法之间的相似之处:
- 阻塞线程:wait 和 sleep 都会导致当前线程进入阻塞状态。
- 响应中断:如果在等待过程中收到中断信号,两者都会响应并抛出 InterruptedException 异常。
但是,它们之间也存在着许多不同之处:
- 使用位置不同:wait 方法必须在 synchronized 修饰的代码块或方法中使用,而 sleep 方法没有这个要求,可以在任何地方使用。
- 锁处理方式不同:当 wait 方法执行时,线程会主动释放所持有的对象锁;而 sleep 方法不会释放锁,即使它是在同步代码块中执行。
- 恢复机制不同:sleep 方法需要指定一个时间,时间到后线程会自动恢复;而 wait 方法(不带参数的情况)表示线程将永久等待,直到被中断或被其他线程唤醒。
- 所属类不同:wait 和 notify 是 Object 类的方法,而 sleep 是 Thread 类的方法。