目录
wait (等待)/ notify (通知)
由于线程在操作系统上是“随机调度,抢占式执行”的,因此线程之间执行的先后顺序难以预知。但是实际开发中,有时候我们希望合理的协调多个线程之间执行的先后顺序,就可以让后执行的逻辑使用 wait 进行阻塞,先执行的线程 完成某些逻辑之后,通过 notify 唤醒同一把锁下的wait
形象的比喻:
大家排队到ATM取款机取款,每次进去一个人后,门就会锁上。
当1号发现ATM中没钱了,就会出来等待运钞车存钱。但1号害怕其他人又进去,之后会立即回去,但此时ATM中依旧没钱,又会出去.....1号就这样进进出出,导致堵塞。
像这种情况 属于“线程饿死”,是概率性问题,和调度器具体的策略直接相关。
针对上述问题,同样也可以使用 wait 和 notify 来解决。
1号拿到锁的时候,进行判定:当前能否执行 “取钱” 操作。如果能执行,就正常执行;如果不能执行,就需要主动释放锁,并且“阻塞等待”(通过调用wait),此时这个线程就不会在后续参与锁的竞争了。一直阻塞到 "取钱" 条件具备了,此时,再由运钞车通知机制 (notify) 唤醒1号的 wait,1号重新获得锁,这样就能进去取钱了。
一. 监视器锁与监视器
监视器锁(Monitor Lock):监视器锁是一个较低层次的抽象,它仅仅是一个锁,用于控制对共享资源的访问。每个对象都可以作为监视器锁,线程通过获取这个锁来进入同步块或方法。
监视器(Monitor):监视器是一个更高级的抽象概念,它不仅包括锁,还包括条件变量(线程可以在这个变量上等待或被唤醒)和等待队列(当线程调用wait时,它会被放入监视器的等待队列中)。
监视器锁只负责同步;监视器不仅负责线程同步,还负责线程间的通信。线程可以通过调用wait(), notify(),和notifyAll() 方法在监视器上进行协作。
二. wait ()
wait() 方法是 0bject 类的一部分,用于线程同步。当一个线程执行到一个对象实例的 wait()方法时,该线程会暂停执行,并放弃监视器,也就是释放它所持有的锁,然后等待其他线程在相同的对象上调用 notify() 或 notifyAll () 方法来唤醒它。
public class WaitDemo1 {
//Java 多线程中涉及到的阻塞操作,很多都会抛出InterruptedException
//如果另一个线程里调用Interrupt方法就能触发这个异常,表示等待被中断
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
System.out.println("wait 之前");
obj.wait();
System.out.println("wait 之后");
}
}
运行结果:
IllegalMonitorStateException:
这个异常表明一个线程试图执行一个操作,但是这个操作只有在当前线程持有某个对象的 监视器锁 时才是合法的,而实际上当前线程并没有持有这个锁。
由于 wait() 必须要先释放锁,才能“阻塞等待”,因此,线程必须持有对象的监视器锁才能调用 wait()方法。
public class WaitDemo2 {
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
System.out.println("wait 之前");
//必须先持有锁
synchronized (obj){
obj.wait();
}
System.out.println("wait 之后");
}
}
运行结果:
通过jconsole我们发现,此时线程处在一个没有超时间等待的状态(WAITING)。
wait() 结束等待的条件:
- 其他线程调用该对象的 notify方法
- wait等待时间超时——wait方法提供一个带有timeout参数的版本:void wait(long timeout),可指定等待时间
- 其他线程调用该等待线程的 interrupt方法,导致 wait 抛出 InterruptedException 异常
在调用 wait() 的时候 :1)释放锁 2)执行等待(为了收到通知)--> 必须同时执行(打包成“原子”的)。
* 如果不是“原子”的,会怎样呢? 如果“释放锁”和“执行等待”两个操作不是“原子”的,这两者之间,就可能发生线程切换。
比如:当1号 释放锁 但还未执行等待,此处 运钱的线程 穿插进来了,执行完放钱操作,并会通知所有等待他的线程来取钱。由于1号还没有执行等待操作,因此上述运钱线程的通知,不会被1号感知到....
此时,当1号切换回来时,继续执行等待.......由于错过了通知,这个等待就会持续下去,无法被及时唤醒了....
wait 使调用的线程进入阻塞,notify 则是通知 wait 的线程被唤醒(另一个线程调用的)。被唤醒的 wait 会重新竞争锁,并且在拿到锁之后,再继续执行。
因此,wait() 一共做了三件事:
- 释放锁
- 进入阻塞状态,准备接收通知
- 收到通知之后,被唤醒,并且尝试重新获取锁
wait() 使用步骤
- 获取监视器锁:线程必须持有对象的监视器锁才能调用wait()方法。
- 调用 wait() 方法:当线程执行到 wait() 方法时,会释放锁并执行等待操作
- 等待被唤醒:线程进入对象的等待队列等待被其他线程通过 notify() 或 notifyAll() 唤醒。
- 重新获取锁:一旦线程被唤醒,它必须重新获取监视器锁才能继续执行
三. notify () 和 notifyAll ()
3.1 notify()
notify() 方法是Object类的一个方法,用于线程同步。用来唤醒正在等待这个对象的监视器的一个或多个线程。
public class WaitDemo2 {
public static void main(String[] args) {
Object locker = new Object();
Thread t1 = new Thread(()->{
System.out.println("wait 执行之前");
synchronized (locker){
try {
locker.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("wait 执行之后");
});
Thread t2 = new Thread(()->{
System.out.println("notify 执行之前");
Scanner scanner = new Scanner(System.in);
//t2 线程需要先阻塞一会,等待t1线程执行wait,否则t1线程接受不到通知
scanner.next();
//notify也必须加锁,否则也会抛出IllegalMonitorStateException
//要加与wait相同的监视器锁,否则wait不会被通知到
synchronized (locker){
locker.notify();
}
System.out.println("notify 执行之后");
});
t1.start();
t2.start();
}
}
运行结果:
* 如果对方没有线程wait或者只有一个线程wait,但是另一个线程notify多次,会怎样呢?——不会怎样~~notify通知的时候,如果无人wait,不会有任何副作用!!
要点:
- notify() 也要在同步方法或同步块中调用(搭配synchronized使用,否则抛出IllegalMonitorStateException),该方法是用来通知那些等待该对象的 “对象锁” 的其它线程,对其发出通知notify,并使它们重新获取该对象的 “对象锁”。
- 如果有多个线程等待,则由线程调度器随机唤醒一个线程中的wait。(“随机调度,抢占式执行”)
- 执行完 notify() 后,当前线程不会马上释放该对象锁,要等到执行 notify() 方法的线程将程序执行完,也就是退出同步代码块之后才会释放 “对象锁”。
- 必须确保 “对象锁” 是同一个才能正确执行
- notify通知的时候,如果无人wait,不会有任何副作用
3.2 notifyAll()
和notify相对的还有一个操作——notifyAll:唤醒所有等待的线程。
但是大部分的情况 使用唤醒一个的notify。因为一个一个唤醒(多执行几次notify)整个程序执行过程是比较有序的;如果一下唤醒所有,这些被唤醒的线程,就会无序地竞争锁。
public class NotifyAllTask {
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread t1 = new Thread(()->{
synchronized (locker){
try {
locker.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t1 wait结束");
});
Thread t2 = new Thread(()->{
synchronized (locker){
try {
locker.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t2 wait结束");
});
Thread t3 = new Thread(()->{
synchronized (locker){
try {
locker.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t3 wait结束");
});
t1.start();
t2.start();
t3.start();
// 等待t1、t2、t3都wait结束
Thread.sleep(10);
synchronized (locker){
locker.notifyAll();
}
}
}
运行结果:
3.3 wait等待 和 sleep休眠 的对比(面试题)
其实理论上,wait 和 sleep 完全是没有可比性的,因为一个是用于线程之间的通信,一个是让线程阻塞一段时间。
标签:初阶,等待,JavaEE,线程,notify,监视器,多线程,唤醒,wait From: https://blog.csdn.net/2301_80243321/article/details/140739103对比:
- 定义:wait是Object的方法,sleep是Thread的静态方法
- 锁的释放:wait 释放锁,而 sleep 不释放锁
- 调用位置:wait 必须在同步代码块中调用(搭配synchronized使用,否则抛出IllegalMonitorStateException ),sleep则可以在任何位置调用
- 唤醒方式:wait需要其他线程显式地唤醒(调用notify),sleep则在指定时间后自动唤醒
- 用途:wait通常用于线程之间的通信,等待另一个线程的通知;sleep通常用于模拟延时或减少CPU的使用率,阻塞一段时间
相同点:
- 都可以让线程放弃执行一段时间
- 都可以被 Interrupt 唤醒,但实际上是线程终止了