在 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);
//因为可能有线程在等待,所以需要通知它们
notify();
}
public String take() throws InterruptedException {
while (buffer.isEmpty()) {
wait();
}
return buffer.remove();
}
}
这段代码中主要有两个方法:save() 和 take()。save() 方法负责将数据添加到 buffer 中,并调用 notify() 方法来唤醒因为buffer为空而等待的线程。
take() 方法则检查 buffer 是否为空。如果为空,则调用wait()方法进入等待状态;如果不为空,则从 buffer 中获取一个数据项。
这是一个典型的生产者-消费者模式,关于生产者-消费者问题将在后续的文章中详细探讨。
然而,这段代码没有受到 synchronized 关键字的保护,可能会出现以下情况:
-
首先,消费者线程调用take方法,在take方法中通过buffer.isEmpty()判断buffer是否为空,如果为空,线程要调用wait方法进入等待状态,但是如果线程在调用wait方法之前就被调度器挂起了,此时方法wait还未执行。
-
与此同时,生产者线程开始运行,并执行 save 方法。它向 buffer 中添加数据,并调用 notify 方法。然而,由于消费者线程的 wait 方法还未执行,因此notify 调用没有任何效果,因为没有任何线程在等待唤醒。
-
接着,之前被调度器挂起的消费者线程恢复执行,并调用 wait 方法,进入等待状态。错过了先前的唤醒,在这种情况下,消费者线程会继续等待,直到生产者线程再次调用 notify 或 notifyAll,才能唤醒消费者线程。
你可以使用两个线程分别调用这两个方法,一个模拟生产者线程,一个模拟消费者线程:
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 方法需要在循环中使用?
为了回答这个问题,我们先来了解一个概念:虚假唤醒。
“虚假唤醒”(spurious wakeup)是多线程编程中一个重要的概念,它指的是一个线程在调用 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 类的方法。