线程调用wait为什么会报错IllegalMonitorStateException?

在Java多线程编程中,wait()notify()notifyAll() 是实现线程间协作与通信的核心方法,它们允许一个线程在某个条件不满足时暂停执行,并在另一个线程改变了该条件后将其唤醒,许多初学者在初次使用这些方法时,常常会遇到一个令人困惑的运行时异常:java.lang.IllegalMonitorStateException,这个异常的出现,几乎总是因为一个根本性的原则被忽略了:调用 wait() 方法的线程必须持有该对象的监视器

线程调用wait为什么会报错IllegalMonitorStateException?

为什么会报错?——深入理解监视器锁

要理解这个错误,我们首先需要明白什么是“监视器锁”,在Java中,每一个对象都可以作为一个锁,这个锁也被称为监视器锁或内部锁。synchronized 关键字就是用来获取和释放这个锁的。

wait()notify()notifyAll() 这三个方法并非 Thread 类的方法,而是定义在 java.lang.Object 类中的 final 方法,这正是因为它们需要与任何Java对象的锁进行交互,其设计哲学是:任何对象都有潜力成为多个线程之间的同步监视器。

当一个线程想要调用一个对象的 wait() 方法时,JVM会进行检查,如果当前线程并不是这个对象监视器的所有者(即没有通过 synchronized 获取该对象的锁),JVM为了防止线程在不安全的状态下操作共享资源,会立即抛出 IllegalMonitorStateException,这是一种保护机制,确保了只有在持有锁的“安全区”内,线程才能执行等待或通知这类敏感操作。

成为对象监视器所有者的方式有两种:

  1. 执行该对象的同步(synchronized)实例方法。
  2. 进入一个 synchronized (object) { ... } 代码块,object 就是目标对象。

如果脱离了这两种同步上下文,直接调用 wait(),就等同于一个没有钥匙的人试图去操作一个需要钥匙才能使用的保险箱,结果必然是失败并报警。

错误代码示例与正确写法对比

为了更直观地理解,我们来看一个经典的“生产者-消费者”场景的简化示例。

错误的写法(会抛出异常):

线程调用wait为什么会报错IllegalMonitorStateException?

class SharedObject {
    // 共享数据
}
class Consumer implements Runnable {
    private final SharedObject sharedObject;
    public Consumer(SharedObject sharedObject) {
        this.sharedObject = sharedObject;
    }
    @Override
    public void run() {
        try {
            // 错误:在synchronized块外调用wait()
            System.out.println("消费者准备等待...");
            sharedObject.wait(); // 这里会抛出 IllegalMonitorStateException
            System.out.println("消费者被唤醒,继续执行。");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

在这个例子中,Consumer 线程直接调用了 sharedObject.wait(),但它并未持有 sharedObject 的锁,因此程序运行时会抛出异常。

正确的写法:

class SharedObject {
    // 共享数据
}
class CorrectConsumer implements Runnable {
    private final SharedObject sharedObject;
    public CorrectConsumer(SharedObject sharedObject) {
        this.sharedObject = sharedObject;
    }
    @Override
    public void run() {
        // 正确:在synchronized块内调用wait()
        synchronized (sharedObject) {
            try {
                System.out.println("消费者获取锁,准备等待...");
                sharedObject.wait(); // 正确,当前线程持有sharedObject的锁
                System.out.println("消费者被唤醒,继续执行。");
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
}

在修正后的代码中,Consumer 线程首先通过 synchronized (sharedObject) 获取了 sharedObject 的监视器锁,在此同步块内部,它成为了锁的所有者,此时调用 sharedObject.wait() 就是合法的。

wait()方法的机制与注意事项

当线程在持有锁的情况下调用 wait() 时,会发生以下几件事:

  1. 释放锁:当前线程会立即释放它所持有的对象监视器锁,这一点至关重要,它使得其他线程(例如生产者线程)有机会进入同一个同步块,并执行 notify()notifyAll()
  2. 进入等待池:线程被挂起,并放入该对象的等待池中,处于阻塞状态。
  3. 等待唤醒:线程会一直阻塞,直到其他线程调用了同一个对象的 notify()notifyAll() 方法,或者线程被中断,或者等待超时(如果使用了 wait(long timeout))。

当线程被唤醒后,它并不会立即恢复执行,它需要从等待池中移出,并与其他正在竞争该对象锁的线程一起重新尝试获取锁,只有当它成功重新获取到锁之后,wait() 方法才会返回,线程才能从之前暂停的地方继续向下执行。

一个非常重要的最佳实践是,永远将 wait() 调用放在一个 while 循环中,而不是 if 语句中

synchronized (sharedObject) {
    while (/* 条件不满足 */) {
        sharedObject.wait();
    }
    // 执行业务逻辑
}

这样做是为了处理“虚假唤醒”的情况,尽管罕见,但线程有可能在没有收到 notify()notifyAll() 的情况下被唤醒,使用 while 循环可以确保线程被唤醒后,会重新检查条件是否真正满足,从而避免逻辑错误。

线程调用wait为什么会报错IllegalMonitorStateException?

为了避免 IllegalMonitorStateException 并编写健壮的多线程代码,请遵循以下核心原则:

实践原则 解释 示例
同步上下文调用 wait(), notify(), notifyAll() 必须在 synchronized 方法或代码块内调用。 synchronized (obj) { obj.wait(); }
锁对象一致性 synchronized 锁定的对象必须是你调用 wait()/notify() 的对象。 synchronized (sharedObject) { sharedObject.wait(); }
循环等待 使用 while (condition) { wait(); } 来检查等待条件,防止虚假唤醒。 while (queue.isEmpty()) { this.wait(); }
优先使用notifyAll() 除非你非常确定只有一个等待线程,否则优先使用 notifyAll() 来避免因通知了错误的线程而导致的死锁。 this.notifyAll();

相关问答FAQs

问题1:为什么 wait(), notify(), notifyAll() 方法被定义在 Object 类中,而不是 Thread 类中?

解答: 这是因为这些方法是用于线程间对对象锁进行操作的,而任何Java对象都可以作为锁,将这些方法放在 Object 类中,意味着任何对象都可以拥有等待/通知机制,从而具备了成为同步监视器的能力,如果它们被放在 Thread 类中,就意味着锁与线程本身绑定,这将是一种非常不灵活的设计,限制了同步机制的通用性,Java的设计者们选择将锁机制与对象本身关联,使得线程可以锁定任何它们需要协调访问的对象,大大增强了多线程模型的灵活性。

问题2:调用 wait() 方法后,线程会立即释放CPU资源吗?

解答: 这个问题的描述需要更精确一些,调用 wait() 后,线程会立即释放它所持有的对象监视器锁,释放锁是这个动作最直接和最重要的后果,至于CPU资源,线程随后会从运行状态变为阻塞状态,操作系统会将其从可运行队列中移除,这意味着它放弃了继续使用CPU的资格,另一个线程(例如通过 notify() 唤醒它的线程)此时可以获取到刚刚被释放的锁,并获得CPU时间片来执行。wait() 导致释放锁,从而使得当前线程让出CPU,而其他线程得以竞争CPU和锁,这是一个连锁反应,核心是释放锁,而不是直接操作CPU。

【版权声明】:本站所有内容均来自网络,若无意侵犯到您的权利,请及时与我们联系将尽快删除相关内容!

(0)
热舞的头像热舞
上一篇 2025-10-25 02:15
下一篇 2024-11-30 20:46

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

广告合作

QQ:14239236

在线咨询: QQ交谈

邮件:asy@cxas.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信