
理解Java中的wait()、notify()和监视器锁
在java并发编程中,wait()、notify()和notifyall()是object类提供的核心方法,用于线程间的协作。它们允许一个线程在特定条件不满足时暂停执行(wait()),并在条件满足时被其他线程唤醒(notify()或notifyall())。然而,这些方法的正确使用有一个严格的前提条件:调用这些方法的线程必须持有目标对象的监监视器锁(monitor lock)。
监视器锁是Java实现线程同步的一种机制,通常通过synchronized关键字来获取。当一个线程进入一个synchronized方法或synchronized代码块时,它就会尝试获取指定对象的监视器锁。如果锁已被其他线程持有,当前线程就会阻塞,直到获取到锁为止。
IllegalMonitorStateException的根源
当一个线程尝试调用一个对象的wait()、notify()或notifyAll()方法,但它并未持有该对象的监视器锁时,Java虚拟机就会抛出IllegalMonitorStateException。这通常是由于对同步机制的误解或不当使用造成的。
让我们通过一个具体的例子来分析这个问题。假设我们有一个Server类,其中包含一个daten_ablegen方法,用于处理客户端数据存储。
错误示例代码:
立即学习“Java免费学习笔记(深入)”;
public class Server {
private boolean sicherungswunsch = false;
private int anzahlClients = 0;
private final int maxClients = 5;
// 假设Client是一个简单的POJO类,包含一个ID
static class Client {
int ID;
public Client(int id) { this.ID = id; }
}
public synchronized void daten_ablegen(Client c) throws InterruptedException {
System.out.println("Client " + c.ID + " will Daten ablegen");
// 当条件不满足时,客户端线程进入等待状态
while (sicherungswunsch || (anzahlClients >= maxClients)) {
System.out.println("Client " + c.ID + " sleeps.");
// 错误:在此处调用了客户端对象c的wait()方法
c.wait();
}
anzahlClients++;
System.out.println("当前有 " + anzahlClients + " Clients 正在处理数据。");
// ... 执行数据存储逻辑 ...
}
public static void main(String[] args) {
Server server = new Server();
// 模拟多个客户端并发调用daten_ablegen
for (int i = 0; i < 3; i++) {
final int clientId = i;
new Thread(() -> {
try {
server.daten_ablegen(new Client(clientId));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println("Client " + clientId + " interrupted.");
}
}).start();
}
}
}在上述代码中,daten_ablegen方法被声明为synchronized。这意味着当一个线程调用此方法时,它会获取Server对象实例(即this)的监视器锁。然而,在while循环内部,代码尝试调用c.wait(),其中c是一个Client对象。
问题在于:当前线程持有的是Server对象的锁,而不是Client对象c的锁。根据wait()方法的使用规则,线程必须持有调用wait()方法的对象的锁。因此,当条件不满足,并且有多个线程尝试调用c.wait()时,就会抛出IllegalMonitorStateException。
wait()方法的正确使用姿势
要正确使用wait()方法,关键在于确保调用wait()的线程持有的是同一个对象的监视器锁。在我们的示例中,由于daten_ablegen方法是synchronized的,它锁定了Server对象(this)。因此,wait()方法也应该在Server对象上调用。
修正后的示例代码:
public class Server {
private boolean sicherungswunsch = false;
private int anzahlClients = 0;
private final int maxClients = 2; // 调整maxClients以便更容易触发等待
static class Client {
int ID;
public Client(int id) { this.ID = id; }
}
public synchronized void daten_ablegen(Client c) throws InterruptedException {
System.out.println("Client " + c.ID + " will Daten ablegen");
// 线程必须持有调用wait()方法的对象的监视器锁。
// 在此方法中,synchronized关键字锁定的是当前对象实例(即this)。
// 因此,wait()方法也必须在当前对象实例上调用。
while (sicherungswunsch || (anzahlClients >= maxClients)) {
System.out.println("Client " + c.ID + " sleeps, waiting for Server resources.");
this.wait(); // 正确:在当前Server对象上调用wait()
// 或者直接 wait(); 因为在非静态方法中,wait() 默认就是 this.wait()
}
anzahlClients++;
System.out.println("Client " + c.ID + " 开始处理。当前有 " + anzahlClients + " Clients 正在处理数据。");
// 模拟数据处理
Thread.sleep(1000);
anzahlClients--;
System.out.println("Client " + c.ID + " 处理完成。剩余 " + anzahlClients + " Clients 正在处理数据。");
// 处理完成后,唤醒其他等待的线程
this.notifyAll(); // 唤醒所有等待Server锁的线程
}
// 模拟一个方法来改变sicherungswunsch或maxClients,从而唤醒等待线程
public synchronized void releaseResources() {
System.out.println("Server释放资源,通知等待中的客户端。");
this.sicherungswunsch = false;
this.notifyAll(); // 唤醒所有等待Server锁的线程
}
public static void main(String[] args) {
Server server = new Server();
// 模拟多个客户端并发调用daten_ablegen
for (int i = 0; i < 5; i++) {
final int clientId = i;
new Thread(() -> {
try {
server.daten_ablegen(new Client(clientId));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println("Client " + clientId + " interrupted.");
}
}, "Client-Thread-" + clientId).start();
}
// 模拟一段时间后释放资源
try {
Thread.sleep(5000);
server.releaseResources();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}在修正后的代码中,我们将c.wait()改为了this.wait()。这样,当线程在daten_ablegen方法中等待时,它持有的是Server对象的锁,并且在Server对象上调用wait(),符合了wait()方法的使用规范。当Server对象的条件(如anzahlClients或sicherungswunsch)发生变化时,可以通过调用this.notify()或this.notifyAll()来唤醒等待的线程。
注意事项与最佳实践
- 锁定对象的一致性: wait()、notify()和notifyAll()必须在线程持有其监视器锁的对象上调用。这意味着它们必须出现在synchronized方法内部,或者synchronized(object)代码块内部,并且object必须是调用wait()等方法的对象。
- 使用while循环检查条件: 线程被notify()或notifyAll()唤醒后,并不意味着它等待的条件就一定满足。存在“虚假唤醒”(spurious wakeup)的可能性,或者在被唤醒后,其他线程可能已经改变了条件。因此,总是应该在一个while循环中检查条件,而不是if语句,以确保在条件真正满足时才继续执行。
-
notify() vs notifyAll():
- notify():随机唤醒一个等待在目标对象监视器上的线程。如果多个线程等待相同的条件,只有一个会被唤醒,可能导致其他线程无限期等待。
- notifyAll():唤醒所有等待在目标对象监视器上的线程。通常更安全,因为它可以确保所有相关线程都有机会重新检查条件。在许多生产者-消费者或资源池场景中,notifyAll()是更好的选择。
- 避免死锁: 不当的同步策略可能导致死锁。在设计并发程序时,应仔细考虑锁的获取顺序和释放时机。
- 处理InterruptedException: wait()方法会抛出InterruptedException,这意味着当线程在等待时被中断,它将停止等待并抛出此异常。应该妥善处理此异常,通常是重新设置中断标志Thread.currentThread().interrupt(),并根据业务逻辑决定是退出还是重试。
总结
IllegalMonitorStateException是Java并发编程中一个常见的错误,它明确指出线程在没有持有监视器锁的情况下尝试调用wait()、notify()或notifyAll()。理解synchronized关键字如何获取锁,以及wait()等方法对锁的严格要求,是编写健壮、高效Java并发程序的关键。通过确保在调用这些方法时,线程持有的是正确对象的监视器锁,并遵循最佳实践,可以有效避免此类并发问题,提升程序的稳定性和可靠性。











