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
相关推荐
2401_cf26 分钟前
为什么hadoop不用Java的序列化?
java·hadoop·eclipse
帮帮志31 分钟前
idea整合maven环境配置
java·maven·intellij-idea
belldeep1 小时前
如何阅读、学习 Tcc (Tiny C Compiler) 源代码?如何解析 Tcc 源代码?
c语言·开发语言
LuckyTHP1 小时前
java 使用zxing生成条形码(可自定义文字位置、边框样式)
java·开发语言·python
无声旅者4 小时前
深度解析 IDEA 集成 Continue 插件:提升开发效率的全流程指南
java·ide·ai·intellij-idea·ai编程·continue·openapi
Blossom.1184 小时前
使用Python实现简单的人工智能聊天机器人
开发语言·人工智能·python·低代码·数据挖掘·机器人·云计算
da-peng-song4 小时前
ArcGIS Desktop使用入门(二)常用工具条——数据框工具(旋转视图)
开发语言·javascript·arcgis
galaxy_strive4 小时前
qtc++ qdebug日志生成
开发语言·c++·qt
Ryan-Joee4 小时前
Spring Boot三层架构设计模式
java·spring boot
TNTLWT4 小时前
Qt功能区:简介与安装
开发语言·qt