乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • 学习路径
  • 第1章 Java概述与环境搭建

    • Java概述与环境搭建
    • Java语言概述
    • 解释型语言与编译型语言对比
    • JDK安装与配置
    • JDK、JRE、JVM 详解
    • HelloWorld程序详解
    • IDE 介绍
  • 第2章 标识符与基本数据类型

    • 章节导读
    • 变量概述
    • 常量概述
    • 基本类型与包装类
    • 字节型 byte
    • 短整型 short
    • 整型 int
    • 长整型 long
    • 单精度浮点型 float
    • 双精度浮点型 double
    • 字符型 char
    • 布尔型 boolean
    • 类型转换
  • 第3章 运算符与表达式

    • 章节导读
    • 算术运算符
    • 赋值运算符
    • 关系运算符
    • 逻辑运算符
    • 位运算符
    • 条件运算符
    • 运算符优先级
    • 表达式
  • 第4章 流程控制

    • 章节导读
    • 常见的程序运行流程
    • if-else 选择结构
    • switch 多分支选择
    • while 循环
    • do-while 循环
    • for 循环
    • break 与 continue
  • 第5章 数组

    • 章节导读
    • 一维数组
    • 多维数组
    • Arrays 工具类
  • 第6章 类与对象

    • 章节导读
    • 类与对象
    • 方法定义与调用
    • 构造方法
    • 封装
    • 访问修饰符
    • package 与 import
    • static 关键字
    • this 关键字
    • 参数传递 详解
    • 枚举
    • 成员内部类
    • 局部内部类
    • 静态内部类
    • 匿名内部类
  • 第7章 接口与继承

    • 章节导读
    • 继承
    • super 关键字
    • final 关键字
    • 多态
    • 向上转型与向下转型
    • 抽象类
    • 接口
    • 抽象类与接口对比
  • 第8章 注解

    • 章节导读
    • 注解基础
    • 元注解详解
    • 自定义注解
  • 第9章 常用类

    • 章节导读:Java 常用类
    • Object 类:万类之祖
    • 包装类:基本类型的对象化
    • String:不可变的字符串
    • StringBuffer:线程安全的可变字符串
    • StringBuilder:可变的字符串构建器
    • Math:数学运算工具类
    • Random:伪随机数生成器
    • 大数值运算 详解
    • 日期时间API 详解
  • 第10章 异常机制

    • 章节导读
    • 异常体系与分类
    • try-catch-finally
    • try-with-resources
    • throws 与 throw
    • 自定义异常
  • 第11章 泛型

    • 章节导读
    • 泛型基础
    • 通配符与PECS原则
    • 类型擦除
  • 第12章 集合框架

    • 章节导读
    • 集合框架概述
    • ArrayList
    • LinkedList
    • HashMap 详解
    • LinkedHashMap 详解
    • TreeMap 详解
    • HashSet
    • TreeSet 详解
    • TreeSet 与 Comparable
    • Collections 工具类详解
  • 第13章 IO流

    • 章节导读
    • IO流概述
    • 字节流
    • 字符流
    • 缓冲流
    • 转换流 详解
    • 序列化 详解
    • NIO与Files 详解
    • NIO与Files工具类
  • 第14章 多线程与并发

    • 第十六章 多线程与并发 —— 章节导读
    • 线程基础详解
    • synchronized 详解
    • Lock 与显式锁详解
    • volatile 详解
    • wait 与 notify 详解
    • ThreadLocal详解
    • 原子类详解
    • 并发工具类详解
    • 线程池详解
  • 第15章 反射

    • 章节导读
    • 反射概述与 Class 对象
    • Constructor 与对象创建
    • Field与Method详解
    • 反射应用详解
  • 第16章 JDK8新特性

    • 章节导读
    • Lambda 表达式
    • Stream API 基础
    • Stream API 高级详解
    • Optional 详解
    • 新日期时间API详解
  • 第17章 JDK9-11新特性

    • 章节导读
    • 模块化系统 — Project Jigsaw(JDK 9)
    • var 局部变量类型推断(JDK 10)
    • 集合工厂方法与增强(JDK 9 / 10 / 11)
    • 接口增强:private 方法(JDK 9)
    • Stream API 增强(JDK 9)
    • Optional 增强(JDK 9 / 10 / 11)
    • String 新增方法(JDK 11)
    • HTTP Client 与 Files 增强(JDK 11)
    • 直接运行 Java 源文件 — JEP 330(JDK 11)
  • 第18章 JDK12-17新特性

    • 章节导读
    • Switch 表达式(JDK 12 预览 / JDK 14 正式)
    • 文本块 Text Blocks(JDK 13 预览 / JDK 15 正式)
    • Records 记录类(JDK 14 预览 / JDK 16 正式)
    • 密封类 Sealed Classes(JDK 15 预览 / JDK 17 正式)
    • instanceof 模式匹配(JDK 14 预览 / JDK 16 正式)
    • Switch 模式匹配 — Pattern Matching for switch(JDK 17 预览 / JDK 21 正式)
    • Helpful NPE 与 String 增强(JDK 12 / JDK 14 / JDK 15)
    • Stream 增强(JDK 12 / JDK 16)
    • 日期时间增强 — Day Period 支持(JDK 16)
  • 第19章 JDK18-21新特性

    • 章节导读
    • 虚拟线程(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 序列集合(JDK 21 正式)
    • Switch 模式匹配(JDK 17 预览 / JDK 18 第二预览 / JDK 20 第四预览 / JDK 21 正式)
    • Record 模式匹配(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 未命名模式与变量(JDK 21 预览 / JDK 22 正式)
  • 第20章 JDK 22-25 新特性

    • 章节导读
    • 字符串模板(JDK 22 预览 / JDK 23 第二预览 / JDK 24 第三预览)
    • Stream Gatherers(JDK 22 预览 / JDK 24 第二预览)
    • 隐式声明类与实例方法(JDK 23 预览 / JDK 24 第二预览)
    • 原始类型模式匹配(JDK 24 预览)
  • 附录

    • Java 核心知识点
    • Java SE 专业术语
    • Java特性索引(JDK 8 → 25)

wait 与 notify 详解

场景:小崔正在写飞翔科技的消息推送系统——后台线程生成推送消息,前台线程取消息发给用户。他自信满满地用了一个 while(true) 循环不断检查队列:"有消息就发,没消息就睡一会儿……"

白歌(看了一眼代码,眉头紧皱):"这叫什么设计?空转轮询 CPU 浪费不说,sleep 时间设短了空转,设长了延迟大——典型的'忙等'反模式。"

大翔:"那正确的做法是什么?"

白歌:"wait/notify。Java 线程间通信的标准方式。生产者没东西时 wait 释放锁睡觉,消费者消费完 notify 唤醒。零 CPU 消耗,即时响应。"

朱璐(认真补充):"但 wait/notify 用不好比轮询更危险——必须在 synchronized 里调用,wait 会释放锁,sleep 不会,还有虚假唤醒。"

Frank:"And in JDK 8, you should actually consider BlockingQueue or CompletableFuture for producer-consumer. But understanding wait/notify is fundamental — it's the foundation of all Java inter-thread communication."

小崔:"好,那我从 wait/notify 的底层机制开始学。"


一、定义速查表

属性说明
wait()Object 方法,让当前线程等待,直到被 notify()/notifyAll() 唤醒或中断。必须在 synchronized 中调用,调用后释放锁并进入 WAITING 状态
wait(long timeout)带超时的 wait,超时后自动唤醒。进入 TIMED_WAITING 状态
wait(long timeout, int nanos)纳秒级超时,JDK 中实际按毫秒取整处理
notify()Object 方法,随机唤醒一个在该对象监视器上等待的线程。被唤醒线程不会立即执行,需要重新获取锁
notifyAll()Object 方法,唤醒所有在该对象监视器上等待的线程。推荐使用,避免"信号丢失"问题
监视器(Monitor)每个 Java 对象关联的同步机制,包含 EntryList(竞争锁队列)和 WaitSet(等待队列)
EntryList等待获取对象锁的线程队列(BLOCKED 状态)。竞争 synchronized 失败的线程进入此处
WaitSet调用 wait() 后释放锁并等待的线程集合(WAITING 状态)。notify() 将线程从 WaitSet 移到 EntryList
虚假唤醒(Spurious Wakeup)线程在没有收到 notify 的情况下被唤醒(OS 层面的原因)。必须用 while 循环检查条件,不能用 if
wait vs sleepwait 是 Object 方法,释放锁,必须在 synchronized 中;sleep 是 Thread 方法,不释放锁,可在任意位置调用

二、Mermaid 图解

2.1 wait/notify 线程交互时序图

2.2 线程状态转换图(含 wait/notify)


三、wait/notify 核心规则

规则说明
必须在 synchronized 中调用 wait(), notify(), notifyAll() 前必须持有对象锁,否则抛出 IllegalMonitorStateException
wait() 释放锁wait() 调用后当前线程释放锁并进入 WAITING,被唤醒后需重新获取锁才能继续
sleep() 不释放锁Thread.sleep() 让出 CPU 但不释放锁,其他线程无法进入 synchronized 块
notify() 不立即释放锁notify() 只是把线程从 WaitSet 移到 EntryList,当前线程退出 synchronized 后才释放锁
while 循环检查必须用 while (条件) 而非 if (条件),防止虚假唤醒导致条件不满足时继续执行
notifyAll() 优先多线程等待时使用 notifyAll() 避免信号丢失,除非确定只有一个等待线程

四、完整代码示例

示例 1:飞翔科技消息队列 —— 生产者-消费者 wait/notify 实现

场景:飞翔科技的内部消息系统,大翔(工号 10001)作为生产者发布系统通知,白歌(工号 10002)作为消费者处理通知。消息队列容量为 5。

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

/**
 * 飞翔科技 - 消息队列生产者消费者 wait/notify 实现
 * 
 * 场景:大翔(10001) 生产消息,白歌(10002) 消费消息
 *       队列容量 5,使用 wait/notifyAll 实现线程通信
 */
public class MessageQueueDemo {

    static class MessageQueue {
        private final Queue<String> queue = new LinkedList<>();
        private final int capacity;

        public MessageQueue(int capacity) {
            this.capacity = capacity;
        }

        /**
         * 生产者:大翔发布消息
         * @param message 消息内容
         * @param employeeId 员工工号
         */
        public synchronized void produce(String message, int employeeId) 
                throws InterruptedException {
            // ★ while 循环防止虚假唤醒 — 不能用 if!
            while (queue.size() == capacity) {
                System.out.printf("[生产者-工号%d] 队列已满(%d/%d),等待消费...\n",
                        employeeId, queue.size(), capacity);
                wait();  // 释放锁,等待消费者唤醒
            }

            queue.offer(message);
            System.out.printf("[生产者-工号%d] 发布: \"%s\" | 队列: %d/%d\n",
                    employeeId, message, queue.size(), capacity);

            // ★ 唤醒所有等待线程 — 包括消费者和可能等待的其他生产者
            notifyAll();
        }

        /**
         * 消费者:白歌消费消息
         * @param employeeId 员工工号
         * @return 消费的消息
         */
        public synchronized String consume(int employeeId) 
                throws InterruptedException {
            // ★ while 循环防止虚假唤醒
            while (queue.isEmpty()) {
                System.out.printf("[消费者-工号%d] 队列为空,等待生产...\n", employeeId);
                wait();  // 释放锁,等待生产者唤醒
            }

            String message = queue.poll();
            System.out.printf("[消费者-工号%d] 消费: \"%s\" | 队列: %d/%d\n",
                    employeeId, message, queue.size(), capacity);

            // ★ 唤醒所有等待线程 — 包括生产者和可能等待的其他消费者
            notifyAll();

            return message;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MessageQueue mq = new MessageQueue(5);
        System.out.println("========== 飞翔科技内部消息系统 ==========\n");

        // 生产者:大翔(10001)
        Thread producer = new Thread(() -> {
            try {
                String[] messages = {
                    "系统维护通知:今晚 22:00 停机",
                    "薪资调整:本月起基本工资 8888.88 元",
                    "团建通知:周末南山烧烤",
                    "安全提醒:请更新登录密码",
                    "新项目立项:智能监控平台 v2.0",
                    "报销通知:本月报销截止 25 日",
                    "培训通知:周五 Java 并发编程培训",
                    "福利通知:下午茶已到茶水间",
                    "会议通知:周一 9:00 全体会议",
                    "年终奖通知:本月发放年终奖"
                };
                for (int i = 0; i < messages.length; i++) {
                    mq.produce(messages[i], 10001);
                    Thread.sleep(200); // 模拟生产间隔
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }, "Producer-大翔");

        // 消费者:白歌(10002)
        Thread consumer = new Thread(() -> {
            try {
                for (int i = 0; i < 10; i++) {
                    String msg = mq.consume(10002);
                    Thread.sleep(400); // 模拟消费处理耗时
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }, "Consumer-白歌");

        producer.start();
        consumer.start();

        producer.join();
        consumer.join();

        System.out.println("\n========== 所有消息处理完毕 ==========");
    }
}

控制台输出:

========== 飞翔科技内部消息系统 ==========

[生产者-工号10001] 发布: "系统维护通知:今晚 22:00 停机" | 队列: 1/5
[生产者-工号10001] 发布: "薪资调整:本月起基本工资 8888.88 元" | 队列: 2/5
[消费者-工号10002] 消费: "系统维护通知:今晚 22:00 停机" | 队列: 1/5
[生产者-工号10001] 发布: "团建通知:周末南山烧烤" | 队列: 2/5
[消费者-工号10002] 消费: "薪资调整:本月起基本工资 8888.88 元" | 队列: 1/5
[生产者-工号10001] 发布: "安全提醒:请更新登录密码" | 队列: 2/5
[生产者-工号10001] 发布: "新项目立项:智能监控平台 v2.0" | 队列: 3/5
[消费者-工号10002] 消费: "团建通知:周末南山烧烤" | 队列: 2/5
...(生产消费交替进行)
[生产者-工号10001] 发布: "年终奖通知:本月发放年终奖" | 队列: 1/5
[消费者-工号10002] 消费: "年终奖通知:本月发放年终奖" | 队列: 0/5

========== 所有消息处理完毕 ==========

示例 2:飞翔科技奇偶数交替打印 —— wait/notify 经典面试题

场景:两个线程交替打印 1-20 的奇偶数。大翔(10001)打印奇数,白歌(10002)打印偶数。使用 wait/notify 精确控制交替。

/**
 * 飞翔科技 - 奇偶数交替打印 wait/notify 实现
 * 
 * 场景:大翔(10001) 打印奇数,白歌(10002) 打印偶数
 *       使用 wait/notify 实现交替输出 1-20
 */
public class OddEvenPrinterDemo {

    // 共享计数器
    private int number = 1;
    private final int max;
    private final Object lock = new Object();

    public OddEvenPrinterDemo(int max) {
        this.max = max;
    }

    /**
     * 大翔打印奇数
     */
    public void printOdd(int employeeId) {
        synchronized (lock) {
            while (number <= max) {
                // ★ while 循环检查条件
                while (number % 2 == 0) {  // 当前是偶数,等待
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        return;
                    }
                }

                if (number > max) break;

                System.out.printf("[工号%d-大翔] 奇数: %d\n", employeeId, number);
                number++;
                lock.notifyAll();  // ★ 唤醒偶数线程
            }
        }
    }

    /**
     * 白歌打印偶数
     */
    public void printEven(int employeeId) {
        synchronized (lock) {
            while (number <= max) {
                // ★ while 循环检查条件
                while (number % 2 == 1) {  // 当前是奇数,等待
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        return;
                    }
                }

                if (number > max) break;

                System.out.printf("[工号%d-白歌] 偶数: %d\n", employeeId, number);
                number++;
                lock.notifyAll();  // ★ 唤醒奇数线程
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        OddEvenPrinterDemo printer = new OddEvenPrinterDemo(20);
        System.out.println("========== 飞翔科技奇偶数交替打印 (1-20) ==========\n");

        // 大翔(10001) — 打印奇数
        Thread oddThread = new Thread(
                () -> printer.printOdd(10001), "Odd-大翔");

        // 白歌(10002) — 打印偶数
        Thread evenThread = new Thread(
                () -> printer.printEven(10002), "Even-白歌");

        oddThread.start();
        evenThread.start();

        oddThread.join();
        evenThread.join();

        System.out.println("\n========== 打印完成,工号10001月薪: 8888.88 ==========");
    }
}

控制台输出:

========== 飞翔科技奇偶数交替打印 (1-20) ==========

[工号10001-大翔] 奇数: 1
[工号10002-白歌] 偶数: 2
[工号10001-大翔] 奇数: 3
[工号10002-白歌] 偶数: 4
[工号10001-大翔] 奇数: 5
[工号10002-白歌] 偶数: 6
[工号10001-大翔] 奇数: 7
[工号10002-白歌] 偶数: 8
[工号10001-大翔] 奇数: 9
[工号10002-白歌] 偶数: 10
[工号10001-大翔] 奇数: 11
[工号10002-白歌] 偶数: 12
[工号10001-大翔] 奇数: 13
[工号10002-白歌] 偶数: 14
[工号10001-大翔] 奇数: 15
[工号10002-白歌] 偶数: 16
[工号10001-大翔] 奇数: 17
[工号10002-白歌] 偶数: 18
[工号10001-大翔] 奇数: 19
[工号10002-白歌] 偶数: 20

========== 打印完成,工号10001月薪: 8888.88 ==========

五、wait vs sleep 深度对比

/**
 * 飞翔科技 - wait vs sleep 锁释放行为对比
 */
public class WaitVsSleepDemo {

    private static final Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        System.out.println("========== wait vs sleep 锁行为对比 ==========\n");

        // === 测试1:sleep 不释放锁 ===
        System.out.println("--- 测试1: sleep 不释放锁 ---");
        Thread sleepThread = new Thread(() -> {
            synchronized (lock) {
                System.out.println("[sleep线程] 获取锁,开始 sleep 2 秒...");
                try {
                    Thread.sleep(2000); // sleep 不释放锁!
                } catch (InterruptedException e) { }
                System.out.println("[sleep线程] sleep 结束,释放锁");
            }
        }, "SleepThread");

        Thread tryLockThread = new Thread(() -> {
            try { Thread.sleep(200); } catch (InterruptedException e) { }
            System.out.println("[尝试线程] 尝试获取锁...");
            long start = System.currentTimeMillis();
            synchronized (lock) {
                long waited = System.currentTimeMillis() - start;
                System.out.printf("[尝试线程] 获取锁成功!等待了 %d ms\n", waited);
            }
        }, "TryLockThread");

        sleepThread.start();
        tryLockThread.start();
        sleepThread.join();
        tryLockThread.join();

        // === 测试2:wait 释放锁 ===
        System.out.println("\n--- 测试2: wait 释放锁 ---");
        Thread waitThread = new Thread(() -> {
            synchronized (lock) {
                System.out.println("[wait线程] 获取锁,调用 wait()...");
                try {
                    lock.wait(2000); // wait 释放锁!其他线程可以获取
                } catch (InterruptedException e) { }
                System.out.println("[wait线程] wait 返回,释放锁");
            }
        }, "WaitThread");

        Thread tryLockThread2 = new Thread(() -> {
            try { Thread.sleep(200); } catch (InterruptedException e) { }
            System.out.println("[尝试线程2] 尝试获取锁...");
            long start = System.currentTimeMillis();
            synchronized (lock) {
                long waited = System.currentTimeMillis() - start;
                System.out.printf("[尝试线程2] 获取锁成功!等待了 %d ms\n", waited);
            }
        }, "TryLockThread2");

        waitThread.start();
        tryLockThread2.start();
        waitThread.join();
        tryLockThread2.join();

        System.out.println("\n========== 结论 ==========");
        System.out.println("sleep 不释放锁 → 其他线程被阻塞 2 秒");
        System.out.println("wait 释放锁 → 其他线程可以立即获取");
    }
}

控制台输出:

========== wait vs sleep 锁行为对比 ==========

--- 测试1: sleep 不释放锁 ---
[sleep线程] 获取锁,开始 sleep 2 秒...
[尝试线程] 尝试获取锁...
[sleep线程] sleep 结束,释放锁
[尝试线程] 获取锁成功!等待了 ~1800 ms          ← sleep 不释放锁,等了近 2 秒

--- 测试2: wait 释放锁 ---
[wait线程] 获取锁,调用 wait()...
[尝试线程2] 尝试获取锁...
[尝试线程2] 获取锁成功!等待了 ~200 ms           ← wait 释放锁,几乎立即获取

========== 结论 ==========
sleep 不释放锁 → 其他线程被阻塞 2 秒
wait 释放锁 → 其他线程可以立即获取

六、易错场景

❌ 错误 1:不在 synchronized 中调用 wait/notify

// ✗ 错误:不在 synchronized 中调用 wait() → IllegalMonitorStateException
Object lock = new Object();
lock.wait();  // 运行时异常!没有持有锁

// ✓ 正确:必须在 synchronized 块中
synchronized (lock) {
    lock.wait();  // 合法
}

❌ 错误 2:用 if 而非 while 判断条件

// ✗ 错误:if 只检查一次,虚假唤醒后不重新检查
synchronized (lock) {
    if (queue.isEmpty()) {          // ★ 用 if!
        lock.wait();                 // 虚假唤醒后直接往下执行
    }
    queue.poll();                    // 可能队列仍为空!
}

// ✓ 正确:用 while 循环,每次醒来都重新检查
synchronized (lock) {
    while (queue.isEmpty()) {       // ★ 用 while!
        lock.wait();
    }
    queue.poll();
}

❌ 错误 3:notify 后不释放锁

// ✗ 错误:notify 后还持有锁,被唤醒线程无法立即执行
synchronized (lock) {
    queue.offer(data);
    lock.notify();
    // 还在做一些耗时操作...被唤醒的线程必须等待!
    Thread.sleep(5000);  // 糟糕!
}

// ✓ 正确:notify 后尽快释放锁(最小化临界区)
synchronized (lock) {
    queue.offer(data);
    lock.notify();
}  // 立即释放锁

❌ 错误 4:使用 notify() 而非 notifyAll() 导致信号丢失

// ✗ 错误:多生产者-多消费者用 notify() 可能唤醒同类型线程
// 场景:2 个生产者都在等"队列未满",2 个消费者都在等"队列非空"
// notify() 可能唤醒另一个生产者 → 检查队列仍满 → 继续 wait → 信号丢失!

// ✓ 正确:不确定时使用 notifyAll()
synchronized (lock) {
    queue.offer(data);
    lock.notifyAll();  // 唤醒所有等待线程,避免信号丢失
}

✅ 正确示范:wait/notify 模板代码

// ✓ 标准 wait/notify 模板
synchronized (lock) {
    // 1. while 循环检查条件(防止虚假唤醒)
    while (条件不满足) {
        lock.wait();  // 2. 释放锁,进入等待
    }
    // 3. 条件满足,执行操作
    doSomething();
    // 4. 唤醒其他线程
    lock.notifyAll();  // 5. 优先使用 notifyAll
}

七、面试考点

Q1:wait() 和 sleep() 的区别?

A:

对比维度wait()sleep()
所属类Object 实例方法Thread 静态方法
锁释放✅ 释放锁❌ 不释放锁
调用位置必须在 synchronized 中任意位置
唤醒方式notify()/notifyAll() 或超时超时自动唤醒或 interrupt()
线程状态WAITING / TIMED_WAITINGTIMED_WAITING
使用场景线程间通信(等待条件)暂停执行(延时)

Q2:为什么 wait/notify 必须在 synchronized 中调用?

A:从两个层面理解:

  1. 语义层面:wait/notify 是线程间通信机制,需要基于共享状态。如果不在 synchronized 中,线程 A 调用 wait 时可能线程 B 刚刚修改了条件并调用了 notify(但 A 还没开始 wait),导致信号丢失(Lost Wakeup)。synchronized 保护了"检查条件 → wait"的原子性。

  2. 实现层面:JVM 的 Monitor 机制要求 wait/notify 操作 WaitSet,而 WaitSet 是 Monitor 的一部分。只有持有 Monitor 所有权的线程才能操作 WaitSet。否则抛出 IllegalMonitorStateException。

Q3:什么是虚假唤醒(Spurious Wakeup)?如何处理?

A:虚假唤醒指线程在没有被 notify/notifyAll 调用、没有被中断、也没有超时的情况下,从 wait() 返回。这是操作系统层面的现象(POSIX 条件变量规范允许),Java 为了兼容性也保留了这种行为。

处理方式:必须用 while 循环重新检查条件,而不是 if:

synchronized (lock) {
    while (条件不满足) {  // ★ 每次醒来都重新检查
        lock.wait();
    }
    // 此时条件一定满足
}

Q4:notify() 和 notifyAll() 的区别?什么时候用哪个?

A:

  • notify():随机唤醒一个在 WaitSet 中等待的线程,具体唤醒哪个由 JVM 决定
  • notifyAll():唤醒 WaitSet 中所有等待线程,它们全部进入 EntryList 竞争锁

使用建议:

  • 当所有等待线程等待的是同一条件时,可用 notify()(只有一个能被满足)
  • 当等待线程等待的是不同条件或不确定时,必须用 notifyAll()
  • 保守策略:始终使用 notifyAll(),避免信号丢失。只有在对线程模型完全可控时才用 notify()

大翔(靠在椅背上):"wait/notify 是 Java 线程通信的基石。虽然现在有 Lock、Condition、BlockingQueue,但面试必问,原理必须懂。"

白歌:"记住三个关键点:必须在 synchronized 中、wait 释放锁 sleep 不释放、while 循环防虚假唤醒。"

小崔:"那以后写生产者-消费者,我还用 wait/notify 吗?"

Frank:"In production code, use BlockingQueue or CompletableFuture. But for interviews and understanding the fundamentals, wait/notify is essential knowledge."

朱璐:"而且理解了 wait/notify,再去学 Condition、CountDownLatch、CyclicBarrier 这些高级并发工具,就事半功倍了。"

上一页
volatile 详解
下一页
ThreadLocal详解