目录
一、等待和通知机制的概念
二、wait() 方法
2.1 wait() 方法的使用
2.2 超时等待
2.3 异常唤醒
2.4 唤醒等待的方法
三、notify() 方法
四、notifyAll() 方法
五、wait 和 sleep 的对比
一、等待和通知机制的概念
1)什么是等待和通知机制? |
线程是抢占式执行的,无法预知线程之间的执行顺序。 但有时程序员也希望能合理协调多个线程的执行顺序。 因此,在 Java 中使用了等待(wait)和通知(notify)机制,用于在应用层面上干预多个线程的执行顺序。 应当注意的是,干预执行顺序并不是干预系统的线程调度策略(操作系统内核中的线程调度仍是无序的),而是使被指定的线程,主动放弃被系统调度的机会,直到其他线程对被指定的线程发出通知,这个线程才再次参与系统的线程调度。(排队都不排了,自然也就轮不到它了) |
2)使用等待和通知机制主要涉及以下三个方法 | |
wait() | 让线程进入等待状态。 |
notify() | 唤醒在当前对象上等待的一个线程。 |
notifyAll() | 唤醒在当前对象上等待的所有线程。 |
以上三个方法都是 Object 类的方法。 |
二、wait() 方法
2.1 wait() 方法的使用
1)wait() 方法需要配合 synchronized 关键字使用 |
wait() 方法必须在 synchronized 修饰的代码块或方法中使用,否则会抛出 IllegalMonitorStateException 异常。 |
2)使用锁对象调用 wait() 方法 |
虽然,wait() 是 Object 类的方法,任何对象都可以调用该方法。 但是为了实现等待通知机制,要求调用 wait() 的对象必须是锁对象,且这个锁对象要与 synchronized 指定的锁对象一致。 |
3)wait() 方法具体做了什么? | |
wait() 方法主要执行了以下三个操作: | |
<1> | 释放当前的锁。 |
<2> | 使当前线程进入等待队列。 |
<3> | 通过某些条件被唤醒时,重新尝试获取当前锁。 |
代码演示wait()使用方法和使用结果:
public static void main(String[] args) throws InterruptedException {
//创建锁对象;
Object locker = new Object();
System.out.println("wait前");
//在 synchronized 代码块中调用 wait 方法;
synchronized (locker){
// wait 方法是由锁对象调用的,调用后,线程释放锁,进入等待队列;
locker.wait();
}
System.out.println("wait后");
}
//运行结果:
wait前
...
程序没有执行完毕,线程一直在 wait 。
2.2 超时等待
有时间限制的等待 |
上文中的代码,除非有其他线程唤醒,否则执行后会一直处于 wait 的状态,这就使得程序陷入了“停摆”状态。 在部分场景中,我们可以使用带时间参数的 wait() 方法来规避这个问题。即使没有其他线程唤醒 wait ,wait 仍会在超过规定时间后,自动唤醒,避免了程序的“停摆”。 |
代码演示带时间参数的wait()使用方法和使用结果:
public static void main(String[] args) throws InterruptedException {
//创建锁对象;
Object locker = new Object();
System.out.println("wait前");
//在 synchronized 代码块中调用 wait 方法;
synchronized (locker){
// wait 方法是由锁对象调用的,调用后,线程释放锁,进入等待队列;
locker.wait(3000);
//3秒后线程被唤醒;
}
System.out.println("wait后");
}
//运行结果:
wait前
wait后
成功执行完毕。
2.3 异常唤醒
代码演示通过抛出异常唤醒wait:
public static void main(String[] args) {
//创建锁对象;
Object locker = new Object();
Thread t1 = new Thread(()->{
System.out.println("wait前");
//在 synchronized 代码块中调用 wait 方法;
synchronized (locker){
// wait 方法是由锁对象调用的,调用后,线程释放锁,进入等待队列;
try {
locker.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
//3秒后线程被唤醒;
}
System.out.println("wait后");
});
t1.start();
//抛出异常,清除中断标志,唤醒t1线程。
t1.interrupt();
}
//运行结果:
wait前
wait后
java.lang.InterruptedException
抛出了InterruptedException后,线程被唤醒,继续执行。
2.4 唤醒等待的方法
唤醒等待的方法有三种: | |
<1> | 超时等待:超过了 wait() 方法指定的等待时间。 |
<2> | 异常唤醒:通过其他线程调用该等待线程的 interrupted() 方法,抛出异常唤醒。 |
<3> | notify() 方法:其他线程调用该对象的 notify() 方法。 |
三、notify() 方法
1)notify() 方法有什么作用? |
notify() 方法可以唤醒等待的线程。 |
2)notify() 也要写在 synchronized 修饰的代码块或方法中 |
notify() 方法也是 Object 类的方法,所以任何对象都可以调用。 在操作系统的原生 API 中,也有 wait() 和 notify() 方法。与 wait() 方法不同,操作系统的原生 API 没有要求 notify() 必须在 synchronized 修饰的代码块或方法中使用。 但是,应注意,在 Java 中还是特别约定了 notify() 方法也是要放在 synchronized 修饰的代码块或方法中的。 |
3)wait() 和 notify() 方法是通过锁对象联系的 |
一个锁对象调用的 wait() 只能被同一个锁对象调用的 notify() 唤醒。 如果唤醒时,同一个锁对象有多个线程正在等待,此时只会随机唤醒一个。 |
4)执行 notify() 方法后,锁在什么时候释放? |
在 notify() 方法后,当前线程不会马上释放锁对象,而是等到线程执行完 notify() 方法所在的代码块或方法后,才会释放锁对象。 这也是 Java 中约定 notify() 方法要放在 synchronized 修饰的代码块或方法中的原因。 |
代码演示通过 notify 唤醒wait:
public static void main(String[] args) throws InterruptedException {
//创建一个锁对象;
Object locker = new Object();
//创建一个线程;
Thread t1 = new Thread(()->{
System.out.println("wait前");
//打印"wait前"后等待;
synchronized (locker){
try {
locker.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//被唤醒后打印"wait后";
System.out.println("wait后");
});
t1.start();
//休眠两秒,保证t1线程进入等待状态。
Thread.sleep(2000);
//打印"notify前"后唤醒;
System.out.println("notify前");
synchronized (locker){
locker.notify();
//在出代码块前,打印"notify后"。
System.out.println("notify后");
}
}
//运行结果:
wait前
notify前
notify后
wait后
打印"wait前"之后进入阻塞等待,直到被notify唤醒之后才打印了"wait后"。
四、notifyAll() 方法
notifyAll() 方法有什么作用? |
唤醒这个锁对象上所有等待的线程。 有多个线程使用同一个锁对象 wait ,当对这个锁对象使用 notifyALL() 方法时,所有在等待的线程都会唤醒。 但是需要注意,在唤醒之后,由于需要重新获取锁,此时被唤醒的线程必然要进行锁竞争,所以这些被唤醒的线程并不是同时就开始执行各自的代码了,而仍然是有先后顺序的执行,顺序依旧是随机的。 |
五、wait 和 sleep 的对比
相同点 |
都会使线程阻塞等待。 |
不同点 | wait() 方法 | sleep() 方法 |
用途 | 用于线程间通信。 | 用于线程阻塞等待。 |
用法 | 是 Object 类中的方法, 需要在被 synchronized 修饰的代码块或方法中使用。 | 是 Tread 类中的静态方法, 方法的使用与 synchronized 无关。 |
状态 | 被调用后,当前线程进入 BLOCK 状态并释放锁。 | 被调用后,当前线程进入 TIME_WAIT 状态。 |
唤醒 | 通常通过 notify 唤醒; 可以通过超时或抛出异常唤醒; | 通常按设定的时间唤醒; 可以通过抛出异常唤醒; |
阅读指针 -> 《经典设计模式之 -- 单例模式(“饿汉模式”和“懒汉模式”实现单例模式)》
<JavaEE> 单例模式的两种实现:“饿汉模式”和“懒汉模式”-CSDN博客介绍了什么是单例模式和单例模式的两种实现模式。重点介绍了单例模式中,“懒汉模式”在多线程下的实现。https://blog.csdn.net/zzy734437202/article/details/134785459