目录

1. 完整的 Java 代码演示 (Producer-Consumer 模型)

2. 图片涉及知识点详细解析

7.1 wait() 方法

7.2 notify() 方法

7.3 notifyAll() 方法

7.4 wait 和 sleep 的对比 (高频面试题)

3. 详细扩展说明

A. 为什么 wait() 必须在 while 循环中调用?

B. IllegalMonitorStateException

C. 现代替代方案:Lock 和 Condition

下一步建议

1. 它们是干嘛的?(说人话版)

2. 存在的意义?(为什么要发明这玩意?)

意义一:避免“占着茅坑不干活” (资源利用)

意义二:避免“一直敲门问好了没” (CPU 效率)

3. 总结一下

下一步建议

1. 如果用 if (一次性判断) —— 容易出大锅

2. 如果用 while (循环判断) —— 安全可靠

3. 总结:为什么要用 while?

下一步建议


这张图片展示的是 Java 多线程编程 中关于 线程通信(Thread Communication) 的核心知识点大纲,特别是围绕 Object 类提供的 wait()notify() 机制。

根据您的要求,我将编写一段完整的 Java 代码来演示这些概念,逐行注释,然后详细解析图片中的知识点以及 waitsleep 的经典面试题对比。


1. 完整的 Java 代码演示 (Producer-Consumer 模型)

为了演示 wait()notify()notifyAll(),最经典的场景是生产者-消费者模型。

Java

package com.example.thread;

// 定义一个简单的共享资源类
class SharedResource {
    // 这是一个共享的数据变量
    private String message;
    // 这是一个标志位,true 表示有数据(Wait for consumer),false 表示没数据(Wait for producer)
    private boolean hasMessage = false;

    // 7.1 & 7.3: 生产数据的方法
    public synchronized void produce(String newMessage) {
        // 使用 while 循环而不是 if,防止“虚假唤醒”(Spurious Wakeup)
        while (hasMessage) { 
            try {
                // 打印当前状态,表示生产者需要等待
                System.out.println("生产者: 队列已满,等待消费者消费...");
                // 7.1 wait()方法:释放锁,当前线程进入等待状态
                wait(); 
            } catch (InterruptedException e) {
                // 处理线程中断异常
                Thread.currentThread().interrupt();
                e.printStackTrace();
            }
        }
        
        // 模拟生产数据的过程
        this.message = newMessage;
        this.hasMessage = true;
        System.out.println("生产者: 已生产消息 -> " + newMessage);

        // 7.2 / 7.3 notify() 或 notifyAll() 方法:唤醒等待的线程
        // notify() 随机唤醒一个,notifyAll() 唤醒所有。这里演示 notify()
        notify(); 
    }

    // 消费数据的方法
    public synchronized void consume() {
        // 如果没有消息,消费者需要等待
        while (!hasMessage) {
            try {
                // 打印当前状态
                System.out.println("消费者: 队列为空,等待生产者生产...");
                // 7.1 wait()方法:释放锁,暂停执行
                wait(); 
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                e.printStackTrace();
            }
        }

        // 模拟消费数据的过程
        System.out.println("消费者: 已消费消息 -> " + this.message);
        this.hasMessage = false;

        // 唤醒生产者线程
        notify(); 
    }
}

// 测试主类
public class WaitNotifyDemo {
    public static void main(String[] args) {
        // 创建共享资源对象
        SharedResource resource = new SharedResource();

        // 创建生产者线程
        Thread producerThread = new Thread(() -> {
            // 生产 3 条数据
            String[] messages = {"消息A", "消息B", "消息C"};
            for (String msg : messages) {
                resource.produce(msg);
                try {
                    // 模拟生产耗时,sleep 不会释放锁,但这里是在同步块之外调用的
                    Thread.sleep(1000); 
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "Producer-Thread");

        // 创建消费者线程
        Thread consumerThread = new Thread(() -> {
            // 消费 3 条数据
            for (int i = 0; i < 3; i++) {
                resource.consume();
                try {
                    // 模拟消费耗时
                    Thread.sleep(1000); 
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "Consumer-Thread");

        // 启动线程
        producerThread.start();
        consumerThread.start();
    }
}

2. 图片涉及知识点详细解析

图片中的大纲涵盖了 Java 线程间通信的基础机制。所有这些方法都属于 java.lang.Object 类,而不是 Thread 类,这意味着任何对象都可以作为锁(Monitor)。

7.1 wait() 方法

  • 定义: 使当前线程立即停止运行,释放对象锁,并进入等待队列(Wait Set)。

  • 前提条件: 调用 wait() 的线程必须拥有该对象的监视器锁(Monitor),即必须在 synchronized 代码块或方法中调用。

  • 核心行为:

    1. 释放锁: 这是与 sleep() 最关键的区别。

    2. 阻塞: 线程进入 WAITINGTIMED_WAITING 状态。

    3. 等待唤醒: 直到其他线程调用同一个对象的 notify()notifyAll()

7.2 notify() 方法

  • 定义: 唤醒在此对象监视器上等待的单个线程。

  • 随机性:如果有多个线程在等待,选择唤醒哪一个是任意的(取决于具体的 JVM 实现),开发者无法指定唤醒哪一个。

  • 锁的持有: 调用 notify() 后,当前线程不会立即释放锁,而是要等到执行完当前的 synchronized 代码块后才释放。被唤醒的线程也要等到获得锁之后才能继续执行。

7.3 notifyAll() 方法

  • 定义: 唤醒在此对象监视器上等待的所有线程。

  • 行为: 所有等待的线程都会被唤醒,并开始竞争锁。只有一个线程能抢到锁并执行,其他的线程会再次进入阻塞状态(Blocked),等待锁被释放。

  • 使用场景: 当有多个线程等待,且你不确定应该唤醒哪一个,或者多个线程等待的条件可能同时满足时,使用 notifyAll() 更安全,可以避免“信号丢失”导致的死锁。

7.4 waitsleep 的对比 (高频面试题)

这是 Java 面试中最常见的问题之一。

特性 Object.wait() Thread.sleep()
所属类 Object Thread
锁的释放 释放锁 (Release Lock) 不释放锁 (Hold Lock)
使用范围 必须在 synchronized 块中 任何地方
用途 线程间通信 (Thread Communication) 暂停执行 (Pause Execution)
唤醒方式 需要 notify() / notifyAll() (除非设置超时) 时间到了自动唤醒
异常 抛出 InterruptedException 抛出 InterruptedException

一句话总结: wait 是用来让线程之间协作的(交出控制权和锁),而 sleep 只是让线程休息一下(不交出锁)。


3. 详细扩展说明

为了深入理解,我们需要补充几个图里没写但非常重要的点:

A. 为什么 wait() 必须在 while 循环中调用?

在上面的代码中,我使用了:

Java

while (condition) { 
    wait(); 
}

而不是:

Java

if (condition) { 
    wait(); 
}

原因: 防止虚假唤醒 (Spurious Wakeup)

  • 在底层操作系统层面,线程有时会在没有收到 notify 信号的情况下莫名其妙地醒来。

  • 如果使用 if,线程醒来后会直接往下执行,而不会再次检查条件是否满足,这可能导致程序逻辑错误(例如在队列为空时强行消费)。

  • 使用 while,线程醒来后会再次检查条件,如果条件不满足,它会再次调用 wait() 继续等待。

B. IllegalMonitorStateException

如果你在没有 synchronized 的代码块中调用 wait()notify(),程序会抛出 IllegalMonitorStateException 运行时异常。这是因为 Java 强制要求线程通信必须建立在持有锁的基础上。

C. 现代替代方案:Lock 和 Condition

虽然 wait/notify 是基础,但在 Java 5 引入 java.util.concurrent 包后,通常推荐使用更高级的 ReentrantLockCondition

  • synchronized $\rightarrow$ ReentrantLock

  • wait() $\rightarrow$ Condition.await()

  • notify() $\rightarrow$ Condition.signal()

优势: Condition 允许通过创建多个 Condition 对象(例如 notFullnotEmpty),实现精准唤醒某类线程,而不是像 notifyAll 那样粗暴地唤醒所有线程。

下一步建议

您是否希望我把上面的 wait/notify 代码改写成使用 ReentrantLock 和 Condition 的版本,以便您对比新旧两种写法的区别?


这就用最通俗的大白话(配合一个生动的场景)来解释这三个东西。

为了让你秒懂,我们把“对象锁”想象成只有一个坑位的公共厕所。

把“线程”想象成想上厕所的人。


1. 它们是干嘛的?(说人话版)

  • wait() —— “占着茅坑不拉屎?不行,我出去等。”

    • 人话:你抢到了厕所钥匙(锁),进去了,结果发现便秘(条件不满足,比如没纸了)。你不能一直占着坑位啊,于是你把钥匙交出来(释放锁),乖乖走到厕所门外的长椅上坐着等(进入等待状态),直到有人喊你。

  • notify() —— “嘿,那个谁,有纸了,快醒醒!”

    • 人话:保洁阿姨(另一个线程)进厕所补好纸了。她冲着门外的长椅喊了一嗓子:“刚才谁在等的?有纸了!”

    • 重点:她这一嗓子,只能随机叫醒一个人。这人醒了之后,还得去抢那把钥匙,抢到了才能进。

  • notifyAll() —— “所有睡觉的,全起来!开抢!”

    • 人话:保洁阿姨这次不想点名了,直接拿大喇叭喊:“都有纸了啊!”

    • 重点:长椅上所有在睡觉等待的人全都被叫醒。大家一起冲向门口抢那把钥匙,谁抢到谁进,抢不到的继续等。


2. 存在的意义?(为什么要发明这玩意?)

你可能会问:“既然没纸(条件不满足),我在厕所里干等着不行吗?为什么要出去?”

这就是它们存在的两大核心意义

意义一:避免“占着茅坑不干活” (资源利用)

如果在这个单人厕所里,你没纸了还不出去,一直蹲在里面干等,别人(其他线程)也进不来

  • 没有 wait():整个厕所(系统资源)就被你一个人死锁住了,程序就卡死了。

  • 有了 wait():你暂时让出资源,让能干活的人(比如送纸的人)先进来操作一下。

意义二:避免“一直敲门问好了没” (CPU 效率)

如果没有这套机制,想要知道“有没有纸”,你就得每隔一秒钟去拉一下门:“有纸了吗?有纸了吗?”。

  • 这叫“忙轮询”(Busy Waiting):这非常消耗精力(CPU 狂转),而且大部分时间都是在做无用功。

  • 有了 notify():你就可以放心地“睡过去”,完全不消耗精力。等条件满足了,自然会有人来叫醒你。这就是不用你自己操心的“通知机制”

3. 总结一下

方法 简单理解 关键动作
wait() 我先让位 交出锁,去睡觉,等通知。
notify() 叫醒一个 随机拍醒一个睡觉的人,让他准备干活。
notifyAll() 叫醒所有 把所有睡觉的人都拍醒,让他们竞争上岗。

一句话总结:

这三个方法就是为了让多个人(线程)在抢同一个资源时,能够有商有量地配合,而不是在那儿死磕或者傻等。

下一步建议

既然你已经理解了概念,要不要看看面试中最爱问的坑:

“为什么 wait 必须写在 while 循环里,而不能写在 if 里?” (这关乎到底是谁把你叫醒的,以及叫醒后是不是真的有纸了)。


这个问题的核心在于:你被叫醒了,不代表事情就一定办成了。

继续用我们刚才的“厕所(锁)+ 纸巾(条件)”的例子来解释。


1. 如果用 if (一次性判断) —— 容易出大锅

想象代码是这样的:

Java

if (没有纸) {
    wait(); // 没纸,我去睡觉
}
// 醒来后直接往下冲!
擦屁股();

场景模拟:

  1. 你(线程A) 进厕所,发现没纸,于是执行 wait(),交出钥匙,去长椅上睡觉。

  2. 保洁阿姨(线程B) 来了,补了一卷纸,然后喊了一声 notify()(或者 notifyAll())。

  3. 此时,另一个路人(线程C) 手快,抢在刚被叫醒的你之前,拿到了钥匙冲进了厕所。把阿姨刚补的那卷纸用完了! 然后走了。

  4. 这时候你(线程A) 终于拿到了钥匙,进了厕所。

  5. 灾难发生:因为你用的是 if,你醒来后不会再检查一遍有没有纸,而是直接往下执行“擦屁股”的操作。

  6. 结果空手擦屁股(程序报错,数据异常,数组越界等)。

这就是所谓的 “虚假唤醒” 带来的后果:醒了不代表条件满足了


2. 如果用 while (循环判断) —— 安全可靠

代码改成这样:

Java

while (没有纸) {
    wait(); // 没纸,我去睡觉
}
// 循环退出了,说明肯定有纸了
擦屁股();

场景模拟:

  1. 你(线程A) 进厕所,没纸,wait() 睡觉。

  2. 保洁阿姨(线程B) 补纸,喊人。

  3. 路人(线程C) 还是手快,抢先进去把纸用完了。

  4. 你(线程A) 终于拿到了钥匙,进了厕所。

  5. 关键点来了:因为是 while,你醒来的第一件事不是“擦屁股”,而是回头再看一眼纸筒(再次执行循环判断)。

  6. 结果:你发现:“卧槽?怎么又没纸了?”(虽然我被叫醒了,但条件依然不满足)。

  7. 行动:叹口气,再次执行 wait(),把钥匙交出去,继续回长椅睡觉,等待下一次机会。


3. 总结:为什么要用 while?

哪怕是 notify 把你叫醒的,在你抢到锁的那一瞬间,世界可能已经变了

中间可能有别的线程插队把资源抢走了,或者操作系统出于某种原因莫名其妙把你唤醒了(这叫虚假唤醒,Spurious Wakeup)。

所以,醒来后的第一件事,必须是重新检查条件

  • if = 醒了就干,不管还在不在。 (鲁莽)

  • while = 醒了再确认一眼,不在继续睡。 (稳健)

下一步建议

你现在已经搞懂了 wait/notify 的核心机制和坑。

如果想进阶一点,你想不想知道:既然 notify 这么麻烦还要防止虚假唤醒,Java 后面出的 ReentrantLock 和 Condition 是怎么优化这个流程的?(它们能实现“精准叫醒某个人”,比如只叫醒“修厕所的”或者只叫醒“上厕所的 ——见下篇博客。
 

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐