导读:本期聚焦于白鲨创作的《在Java中使用wait时需要注意什么?线程通信常见误区说明》,敬请观看详情。为什么调用wait方法时程序会抛出IllegalMonitorStateException?为什么线程唤醒后条件还是不成立?wait和sleep看起来都能让线程暂停,实际区别到底在哪里?这篇文章围绕Java线程通信中的wait方法展开,详细讲解必须放在synchronized同步块中调用的原因,分析while循环判断条件的必要性,对比notify与notifyAll的适用场景,并给出生产者消费者模式的完整示例代码,帮助你避开线程通信开发中那些容易踩中的坑。

wait方法是Java多线程开发中最基础也最容易用错的API之一。不少人在第一次接触wait时,会把它简单理解成让线程睡一会儿,结果运行时抛出IllegalMonitorStateException,或者线程被唤醒后业务逻辑却出现了错乱。要正确使用wait,需要理解它背后的监视器锁机制、条件判断方式以及唤醒语义。本文从常见误区入手,逐一拆解wait的正确用法。

在Java中使用wait时需要注意什么?线程通信常见误区说明

误区一:不加锁就直接调用wait

wait方法并不是Thread类的方法,而是定义在Object类中的方法。它的核心作用是让当前线程释放所持有的监视器锁,并进入该对象的等待队列,直到其他线程调用同一个对象的notify或notifyAll将其唤醒。这个机制决定了wait必须在一个已经获得锁的环境下调用,否则JVM无法知道该释放哪个锁。

最常见的错误写法是这样的:

public class WrongWait {
    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        // 没有获取lock的监视器锁就直接调用wait,运行时抛异常
        lock.wait();
    }
}

上面这段代码在运行时会直接抛出java.lang.IllegalStateException的近亲:java.lang.IllegalMonitorStateException,异常信息通常是current thread is not owner。正确做法是把wait放进synchronized块中,并且synchronized锁的对象必须与调用wait的对象完全一致:

synchronized (lock) {
    while (!condition) {
        lock.wait(); // 正确:当前线程已持有lock的锁
    }
    // 条件满足后执行业务逻辑
}

还有一个隐蔽的坑:锁对象和wait对象不一致。比如写成synchronized(this)却调用otherObject.wait(),同样会抛出IllegalMonitorStateException。排查这类问题时,优先检查synchronized括号里的对象和调用wait的对象是不是同一个引用。

误区二:用if判断条件而不是while

wait被唤醒后,线程会从wait调用处继续往下执行,但此时条件不一定真的满足了。原因有两个:第一,notify只是随机唤醒一个等待线程,被唤醒的线程需要重新竞争锁,等到它真正拿到锁时,条件可能已经被其他线程改掉了,这被称为虚假唤醒之外的正常竞争失效;第二,JVM规范本身允许虚假唤醒发生,即没有任何线程调用notify,等待线程也可能自己醒来。

因此,wait的标准姿势是放在while循环中,醒来后重新检查条件:

synchronized (lock) {
    while (queue.isEmpty()) {   // 用while而不是if
        lock.wait();
    }
    // 走到这里说明队列一定不为空
    String item = queue.remove();
}

如果写成if,一旦发生条件被抢先修改或者虚假唤醒,线程会直接往下执行,取出数据时就会抛出IndexOutOfBoundsException之类的异常,或者处理到脏数据。这类问题在测试环境很难复现,往往在高并发的生产环境才爆发,排查成本非常高。JDK官方文档也明确建议wait永远放在循环中调用。

误区三:混淆wait和sleep,误用notify和notifyAll

wait和sleep都能让线程暂停,但它们有本质区别。wait是Object的方法,必须在同步块中调用,会释放锁;sleep是Thread的静态方法,可以在任意位置调用,不会释放已经持有的锁。用一个表格对比更清晰:

对比项waitsleep
所属类ObjectThread
是否释放锁释放监视器锁不释放
调用前提必须持有锁无要求
唤醒方式notify/notifyAll/超时超时自动恢复

误用sleep做线程间协调是最常见的错误之一。比如线程A持有锁后sleep了5秒,其他线程全部被阻塞,整个系统吞吐量急剧下降。正确的协调手段应该是wait配合notify。

notify和notifyAll的选择同样重要。notify只会随机唤醒一个等待线程,如果等待队列中有多种不同条件的线程在等,被唤醒的可能根本不是你想要的那个,最终导致信号丢失、任务卡死。在不确定的情况下,优先使用notifyAll,让所有等待线程都醒来重新检查条件,条件不满足的会再次进入等待,虽然多消耗一点CPU,但逻辑上是安全的。

完整示例:生产者消费者模式

把前面的要点串起来,一个线程安全的阻塞队列生产者消费者示例如下:

import java.util.LinkedList;
import java.util.Queue;

public class ProducerConsumer {
    private static final Queue<Integer> queue = new LinkedList<>();
    private static final int CAPACITY = 5;
    private static final Object lock = new Object();

    static class Producer extends Thread {
        public void run() {
            int value = 0;
            while (true) {
                synchronized (lock) {
                    while (queue.size() == CAPACITY) { // 队列满则等待
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            return;
                        }
                    }
                    queue.offer(value++);
                    System.out.println("生产: " + (value - 1) + " 当前大小: " + queue.size());
                    lock.notifyAll(); // 唤醒消费者
                }
            }
        }
    }

    static class Consumer extends Thread {
        public void run() {
            while (true) {
                synchronized (lock) {
                    while (queue.isEmpty()) { // 队列空则等待
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            return;
                        }
                    }
                    int value = queue.poll();
                    System.out.println("消费: " + value + " 剩余: " + queue.size());
                    lock.notifyAll(); // 唤醒生产者
                }
            }
        }
    }

    public static void main(String[] args) {
        new Producer().start();
        new Consumer().start();
    }
}

这段代码体现了三个关键点:所有对队列的读写都包裹在synchronized中;wait一律放在while循环里;状态变化后使用notifyAll唤醒全部相关线程。另外注意wait会响应中断,抛出InterruptedException时应该正确处理,比如恢复中断标记或者退出循环,直接吞掉异常是不好的实践。

如果觉得手写wait和notify过于繁琐,实际项目中更推荐使用java.util.concurrent包提供的BlockingQueue、Condition配合Lock等工具,它们封装了等待通知语义,代码更简洁,也不容易出错。但理解wait的底层机制依然是必要的,因为Condition的await和signal本质上就是wait和notify的升级版。

Java wait线程通信多线程修改时间:2026-09-13 11:50:31

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。