Java EE初阶——wait 和 notify

1. 线程饥饿

线程饥饿是指一个或多个线程因长期无法获取所需资源(如锁,CPU时间等 )而持续处于等待状态,导致其任务无法推进的现象。

典型场景

  1. 优先级抢占

    • 在支持线程优先级的系统中,高优先级线程可能持续抢占CPU资源

    • 导致低优先级线程长期无法获得CPU时间片

  2. 不公平锁竞争

    • 某些线程频繁获取锁,其他线程长期等待

    • 典型场景:某个线程释放锁后立即重新竞争并获得锁

    • 导致其他线程始终处于BLOCKED状态而无法执行

  3. 资源分配不均

    • 某些线程占用大量I/O或内存资源

    • 其他线程因资源不足而无法执行

  4. 线程池配置不当

    • 固定大小线程池中,长任务占用所有线程

    • 短任务无法得到执行机会

关键问题点

  1. 锁获取模式问题

    • 活跃线程释放锁后立即重新请求锁

    • 处于RUNNABLE状态的线程比BLOCKED状态的线程有更快的响应速度

    • 操作系统唤醒BLOCKED线程需要上下文切换,造成竞争劣势

  2. 系统调度机制

    • 默认调度策略可能不利于公平性

    • 缺乏有效的防饥饿机制

  3. 线程状态转换开销

    • BLOCKED→RUNNABLE状态转换需要系统介入

    • 这种转换比保持RUNNABLE状态有更高的延迟

2. wait 和 notify

wait/notify 的本质作用

  • 应用层协作工具wait()notify() 是 Java 提供的应用层线程协作机制,用于控制线程对共享资源的访问顺序,而非直接干预操作系统的线程调度策略。
  • 不改变调度规则 :操作系统内核仍按自身调度算法(如轮转法、优先级调度)决定线程何时获得 CPU 时间,wait/notify 无法强制指定某个线程优先执行。

1. wait()⽅法

wait(); 内部做的三件事:

  1. 立即释放锁,无需等待同步块结束

2. 线程状态变化RUNNINGWAITING

  1. 线程被唤醒后需重新获取锁 ,获取成功后从 wait() 调用处继续执行。WAITINGBLOCKED(被唤醒后重新竞争锁)→ RUNNING

线程状态变化后,其他线程就有机会获取锁。

wait() 方法的三种重载形式

方法 说明
wait() 使当前线程无限期等待,直到另一个线程调用 notify()notifyAll() 方法
wait(long timeout) 指定一个超时时间,线程将在超时后自动被唤醒。线程也可以在超时前被 notify()notifyAll() 方法唤醒。
wait(long timeout, int nanos) 提供更高精度的超时设置,总超时时间(以纳秒为单位)计算为 1_000_000*timeout + nanos

在 Java 中,调用 wait 方法的对象必须和锁对象一致,这是因为 wait 方法的行为是基于对象的监视器(锁)来实现的。以下是具体解释:

  • 原理 :当一个线程调用某个对象的 wait 方法时,该线程会释放它所持有的该对象的锁,并进入等待状态,直到其他线程调用同一个对象的 notifynotifyAll 方法来唤醒它。如果调用 wait 方法的对象与获取锁的对象不一致,那么线程在等待时就无法正确地与该锁关联,也就无法按照预期被唤醒,并且可能会导致程序出现逻辑错误。

2. notify()⽅法

方法 说明
notify() 唤醒等待该对象监视器的一个随机线程。选择唤醒哪个线程是非确定性的,取决于"随机调度"算法
notifyAll() 唤醒所有等待该对象监视器的线程。被唤醒的线程会和其他试图获取该对象锁的线程一起竞争锁

调用 notify()notifyAll() 的对象必须与调用 wait() 的对象相同,并且它们必须与 synchronized 使用的锁对象一致 ,否则会抛出 IllegalMonitorStateException

  1. wait()notify()notifyAll() 必须由同一个对象调用

  2. 必须在 synchronized 块中使用,并且锁对象必须与调用 wait()/notify() 的对象一致

每个 Java 对象都有一个监视器(monitor),也可以理解为锁。当一个线程进入synchronized代码块时,它会获取该代码块所关联对象的锁。wait方法会让当前线程释放这个锁,并进入等待状态,直到其他线程调用同一个对象的notifynotifyAll方法来唤醒它。而notifynotifyAll方法也需要在获取相同对象的锁之后才能调用,这样它们才能准确地唤醒在该对象上等待的线程。如果这三个对象不一致,就会破坏这种线程同步机制,导致程序出现不可预测的结果,例如线程无法被唤醒、死锁等问题。

wait()notify()notifyAll() 方法必须在 synchronized 修饰的代码块或方法中调用 ,否则会抛出 IllegalMonitorStateException

1. 锁与等待队列的绑定

每个 Java 对象都有两个核心属性:

  • 监视器锁(Monitor):用于实现同步。
  • 等待队列(Wait Set) :用于存储调用 wait() 的线程。

wait()notify() 的操作对象是对象的等待队列 ,而等待队列的状态由来保护。因此,必须先获取锁才能操作等待队列。

2. 原子性与可见性保障
复制代码
public class SynchronizedDomo8 {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();// 作为同步锁和 wait/notify 的监视器对象
        Thread t1 = new Thread(()->{
            synchronized (object){// 获取 object 的锁
                System.out.println("t1 线程之前");// ①
                try {
                    //必须使用同一个对象调用
                    object.wait();// ② 释放锁,进入WAITING状态
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t1 线程之后");//  ⑦  被唤醒后重新获取锁,继续执行
            }
        });
        Thread t2 = new Thread(()->{
            try {
                Thread.sleep(2000);// ③ 休眠 2 秒,确保 t1 先执行并进入 wait()
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            synchronized (object){// 获取 object 的锁
                System.out.println("t2 线程之前");// ④
                //必须使用同一个对象调用
                object.notify();  // ⑤ 唤醒t1,但t2仍持有锁
                System.out.println("t2 线程之后");// ⑥ 同步块结束后释放锁
            }
        });
        t1.start();
        t2.start();
    }
}

执行步骤

  1. t1 进入 synchronized 块,获取 object 的锁。

  2. 打印 "t1 线程之前"

  3. 调用 object.wait()

    • 释放 object 的锁t1 进入等待状态。
  4. t2 先休眠 2 秒,确保 t1 先执行并进入 wait() 状态。

  5. t2 进入 synchronized 块,获取 object 的锁。

  6. 打印 "t2 线程之前"

  7. 调用 object.notify()

    • 唤醒 t1(WAITING -> BLOCKED) ,但 t1 不会立即执行,因为 t2 仍持有锁。
  8. 打印 "t2 线程之后",退出 synchronized 块,释放锁。

  9. t1 重新获取锁,继续执行 "t1 线程之后"

**3.**wait()、join()、sleep()方法的区别

| 方法 | sleep() | join() | wait() |
| 所属类 | Thread类 | Thread类 | Object类 |
| 释放锁 | ❌ | ❌ | ✅ |
| 唤醒条件 | 时间到期 | 目标线程结束或超时 | notify()/notifyAll()或超时 |
| 使用限制 | 可以直接调用 | 可以直接调用 | 必须在同步块中使用 |
| 抛出InterruptedException | ✅ | ✅ | ✅ |
| 精度控制 | 毫秒(实际精度依赖操作系统) | 毫秒+纳秒 | 毫秒+纳秒 |

线程状态变化 RUNNING → WAITING RUNNING → TIMED_WAITING RUNNING → WAITING/TIMED_WAITING
相关推荐
zh_xuan29 分钟前
c++ 单例模式
开发语言·c++·单例模式
coderSong256832 分钟前
Java高级 |【实验八】springboot 使用Websocket
java·spring boot·后端·websocket
老胖闲聊1 小时前
Python Copilot【代码辅助工具】 简介
开发语言·python·copilot
Blossom.1181 小时前
使用Python和Scikit-Learn实现机器学习模型调优
开发语言·人工智能·python·深度学习·目标检测·机器学习·scikit-learn
Mr_Air_Boy1 小时前
SpringBoot使用dynamic配置多数据源时使用@Transactional事务在非primary的数据源上遇到的问题
java·spring boot·后端
曹勖之1 小时前
基于ROS2,撰写python脚本,根据给定的舵-桨动力学模型实现动力学更新
开发语言·python·机器人·ros2
豆沙沙包?2 小时前
2025年- H77-Lc185--45.跳跃游戏II(贪心)--Java版
java·开发语言·游戏
军训猫猫头2 小时前
96.如何使用C#实现串口发送? C#例子
开发语言·c#
年老体衰按不动键盘2 小时前
快速部署和启动Vue3项目
java·javascript·vue
咖啡啡不加糖2 小时前
Redis大key产生、排查与优化实践
java·数据库·redis·后端·缓存