本节⽬标
• 掌握 wait 和 notify(线程的等待通知机制)

7. wait 和 notify

线程的 等待通知 机制(协调线程之间的执行逻辑的顺序的)。

由于系统内部,线程之间是抢占式执⾏的,随机调度, 因此线程之间执⾏的先后顺序难以预知。但是实际开发中有时候我们希望合理的协调多个线程之间的执⾏先后顺序。程序员也是有手段干预的、通过"等待”的方式,能够让线程一定程度的按照咱们预期的顺序来执行。无法主动让某个线程被调度,但是可以主动让某个线程等待 (就给别的线程机会了)。

球场上的每个运动员都是独⽴的 “执⾏流” , 可以认为是⼀个 “线程”.
⽽完成⼀个具体的进攻得分动作, 则需要多个运动员相互配合, 按照⼀定的顺序执⾏⼀定的动作, 线程1 先 “传球” , 线程2 才能 “扣篮”.

完成这个协调⼯作, 主要涉及到三个⽅法
• wait() / wait(long timeout): 让当前线程进⼊等待状态.
• notify() / notifyAll(): 唤醒在当前对象上等待的线程.
注意: wait, notify, notifyAll 都是 Object 类的⽅法.

join和wait区别
join 是等另一个线程彻底执行完, 才继续走。
wait 是等到另一个线程执行 notify, 才继续走(不需要另一个线程执行完),更精细的控制线程之间的执行顺序了。

等待通知可以安排线程之间的 执行顺序,另外,wait notify 也能解决"线程饿死"的问题:

"线程饿死” 不是 死锁。只是因为 某个线程 频繁获取释放锁,由于获取的太快,以至于其他线程捞不着 cpu 资源.
当多个线程竞争一把锁的时候,获取到锁的线程如果释放了,其他是哪个线程拿到锁?
不确定(随机调度)
操作系统的调度是随机的,其他线程都属于在锁上阻塞等待,是阻塞状态,当前这个释放锁的线程,是就绪状态,这个线程有很大的概率能够再次拿到这个锁
系统中的线程调度无序,上述情况很可能出现(不至于长时间一直进进出出,进出个几十次 还是有可能),不会像死锁那样卡死,但是可能会卡住一下下,对于程序的效率,肯定是影响的。

等待通知机制,就能够解决上述问题:拿到锁的线程 通过条件,判定看当前逻辑是否能够执行.如果时机还不成熟的时候,不能执行, 就主动 wait (使用 wait 主动进行阻塞等待),就把执行的机会让给别的线程了,避免该线程进行一些无意义的重试。等到后续条件时机成熟了(需要其他线程进行通知的),再让阻塞的线程被唤醒。

7.1 wait()⽅法

wait是 Object 类提供的方法,任何一个对象都有这个方法

wait 做的事情:
• 释放当前的锁
• 使当前执⾏代码的线程进⾏等待. (把线程放到等待队列中)
(第1件和第2件同时进行)
• 满⾜⼀定条件时被唤醒, 重新尝试获取这个锁.

wait 要搭配 synchronized 来使⽤. 脱离 synchronized 使⽤ wait 会直接抛出异常.
在这里插入图片描述
代码进入 wait,就会先释放锁,并且阻塞等待。如果其他线程做完了必要的工作,调用 notify 唤醒这个 wait 线程,wait 就会解除阻塞, 重新获取到锁.继续执行并返回.

wait 结束等待的条件:
• 其他线程调⽤该对象的 notify ⽅法.
• wait 等待时间超时 (wait ⽅法提供⼀个带有 timeout 参数的版本, 来指定等待时间).
• 其他线程调⽤该等待线程的 interrupted ⽅法, 导致 wait 抛出 InterruptedException 异常.

  1. wait提供了2个版本在这里插入图片描述
    死等这样的策略一般来说是下策.没有回旋的余地了。
    工程上有个术语"鲁棒性”:
    "你对他越粗鲁,他表现的越棒!
    商业程序,也是要考虑到 鲁棒性 ~~ 容错能力, 即使出现一些错误,也不会有太大影响,甚至能自动恢复。
  2. Java 标准库中,涉及到阻塞的方法,都可能会抛出 InterruptedException
    在这里插入图片描述
    在这里插入图片描述
    wait 进入阻塞之后, 需要通过 notify 唤醒.默认情况下,wait 的阻塞也是"死等!设定等待的时间上限 (超时时间)

代码⽰例: 观察wait()⽅法使⽤

public static void main(String[] args) throws InterruptedException {
 Object object = new Object();
 synchronized (object) {
	 System.out.println("等待中");
	 object.wait();
	 System.out.println("等待结束");
 }
}

这样在执⾏到object.wait()之后就⼀直等待下去。
那么程序肯定不能⼀直这么等待下去了。这个时候就需要使⽤到了另外⼀个⽅法唤醒的⽅法notify()。

7.2 notify()⽅法

notify ⽅法是唤醒等待的线程.
• ⽅法notify()也要在同步⽅法或同步块中调⽤,该⽅法是⽤来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
• 如果有多个线程等待,则有线程调度器随机挑选出⼀个呈 wait 状态的线程。(并没有 “先来后到”)
• 在notify()⽅法后,当前线程不会⻢上释放该对象锁,要等到执⾏notify()⽅法的线程将程序执⾏完,也就是退出同步代码块之后才会释放对象锁。

  1. 使用 wait 的时候,阻塞其实是有两个阶段的
    1.WAITING 的阻塞, 通过 wait 等待其他线程的通知.
    2.BLOCKED 的阻塞,当收到通知之后,就会重新尝试获取锁, 重新尝试获取锁,很可能又会遇到锁竞争
  2. wait 和 notify 彼此之间是通过 object 对象联系起来的,必须是同一个对象才能唤醒!
    object1.wait()和object2. notify() :此时无法唤醒的!必须是两个对象一致才能唤醒!!!
    如果有俩 wait 是不同的对象调用的,此时 notify 使用的是哪个对象,就是唤醒哪个对象的wait。如果这俩 wait 是同一个对象调用的呢??随机唤醒其中一个.在这里插入图片描述
  3. 如果有多个线程都在进行 wait (同一个对象上 wait ),此时进行 notify 是随机唤醒其中的一个线程

咱们在多线程中谈到的"随机" 其实不是 “数学上,概率均等的随机”
无法预测~~
取决于调度器, 怎么进行调度。调度器里,其实不是"概率均等的唤醒"内部也是有一套规则的,这套规则,对于程序员是"透明"的。程序员做的,就是不能依赖这里的顺序。
mysql 的时候,select 查询一个数据,得到的结果集,是按照怎样的顺序呢?(是按照 id 的顺序, 时间的顺序,排列的嘛?)mysql 就没有这样的承诺,必须加上 order by。

  1. 这里唤醒等待的线程同样也是,需要先拿到锁,再进行 notify(属于是 Java 中给出的限制)
    wait 操作必须要搭配锁来进行(放到 synchronized里) 是因为要释放锁, 前提是先加上锁.
    notify 操作,原则上说,其实可以不放到 synchronized 里(不涉及到加锁解锁操作)但是 Java 中特别约定要把 notify 放到synchronized 里头了

(线程,锁, 都是操作系统本身支持的特性,wait 和 notify 在操作系统中, 也有原生的对应的 api,操作系统原生 apì 中,wait 必须搭配锁使用,notify 则不需要.)。

通过另一个线程,调用 notify 来唤醒阻塞的线程的 运用示例:

  1. 借助 scanner 控制阻塞,用户输入之前,都是阻塞状态:
    在这里插入图片描述
  1. 借助 sleep 阻塞通知:在这里插入图片描述

代码⽰例: 使⽤notify()⽅法唤醒线程
• 创建 WaitTask 类, 对应⼀个线程, run 内部循环调⽤ wait.
• 创建 NotifyTask 类, 对应另⼀个线程, 在 run 内部调⽤⼀次 notify
• 注意, WaitTask 和 NotifyTask 内部持有同⼀个 Object locker.。WaitTask 和 NotifyTask 要想配合就需要搭配同⼀个 Object.

static class WaitTask implements Runnable {
 private Object locker;
 
 public WaitTask(Object locker) {
 	this.locker = locker;
 }
 
 @Override
 public void run() {
	 synchronized (locker) {
		 while (true) {
			 try {
				 System.out.println("wait 开始");
				 locker.wait();
				 System.out.println("wait 结束");
			 } catch (InterruptedException e) {
			 	e.printStackTrace();
			 }
		 }
	 }
 }
}

static class NotifyTask implements Runnable {
 private Object locker;
 public NotifyTask(Object locker) {
 	this.locker = locker;
 }
 
 @Override
 public void run() {
	 synchronized (locker) {
		 System.out.println("notify 开始");
		 locker.notify();
		 System.out.println("notify 结束");
	 }
 }
}

public static void main(String[] args) throws InterruptedException {
 Object locker = new Object();
 Thread t1 = new Thread(new WaitTask(locker));
 Thread t2 = new Thread(new NotifyTask(locker));
 t1.start();
 Thread.sleep(1000);
 t2.start();
}

7.3 notifyAll()⽅法

notify⽅法只是唤醒某⼀个等待线程. 使⽤notifyAll⽅法可以⼀次唤醒所有的等待线程.

假设有很多个线程,都使用同一个对象 wait,针对这个对象进行 notifyAll, 此时就会全都唤醒~~
但是注意,这些线程在wait返回的时候,要重新获取锁,就会因为锁的竞争,使这些线程实际上是一个一个串行执行的.(谁先拿到锁, 谁后拿到, 也是不确定的)

相比之下,还是更倾向于使用 notify,notifyAll, 全都唤醒之后,不太好控制

范例:使⽤notifyAll()⽅法唤醒所有等待线程, 在上⾯的代码基础上做出修改.

• 创建 3 个 WaitTask 实例. 1 个 NotifyTask 实例.

static class WaitTask implements Runnable {
 // 代码不变
}

static class NotifyTask implements Runnable {
 // 代码不变
}

public static void main(String[] args) throws InterruptedException {
 Object locker = new Object();
 Thread t1 = new Thread(new WaitTask(locker));
 Thread t3 = new Thread(new WaitTask(locker));
 Thread t4 = new Thread(new WaitTask(locker));
 Thread t2 = new Thread(new NotifyTask(locker));
 t1.start();
 t3.start();
 t4.start();
 Thread.sleep(1000);
 t2.start();
}

此时可以看到, 调⽤ notify 只能唤醒⼀个线程.

• 修改 NotifyTask 中的 run ⽅法, 把 notify 替换成 notifyAll

public void run() {
 synchronized (locker) {
 System.out.println("notify 开始");
 locker.notifyAll();
 System.out.println("notify 结束");
 }
}

此时可以看到, 调⽤ notifyAll 能同时唤醒 3 个wait 中的线程

注意: 虽然是同时唤醒 3 个线程, 但是这 3 个线程需要竞争锁. 所以并不是同时执⾏, ⽽仍然是有先有后的执⾏.

理解 notify 和 notifyAll
notify 只唤醒等待队列中的⼀个线程. 其他线程还是乖乖等着
在这里插入图片描述
notifyAll ⼀下全都唤醒, 需要这些线程重新竞争锁
在这里插入图片描述

7.4 wait 和 sleep 的对⽐(⾯试题)

其实理论上 wait 和 sleep 完全是没有可⽐性的,因为⼀个是⽤于线程之间的通信的,⼀个是让线程阻塞⼀段时间,唯⼀的相同点就是都可以让线程放弃执⾏⼀段时间.

wait 提供了一个 带有超时时间的版本,sleep 也能指定时间~~都是时间到, 就继续执行,解除阻塞了
wait 和 sleep 都可以被提前唤醒(虽然时间没到,但是也能提前唤醒)
wait 通过 notify 唤醒,sleep 通过 interrupt 唤醒。
使用 wait,最主要的目标,一定是不知道要等多少时间的前提下使用的,所谓的超时时间,其实是“兜底的”(大多数情况下,wait 都是在超时时间之内就被唤醒了)。
使用 sleep,一定是知道要等多少时间的前提下使用的,虽然能提前唤醒,但是通过异常唤醒,这个操作不应该作为"正常的业务流程”.(sleep 提前唤醒,是通过异常的方式,说明程序应该是出现一些特殊的情况了。正常的业务流程不应该依赖异常处理,异常处理认为是在进行一些补救措施)
在这里插入图片描述

经典面试题 sleep 和 wait 的区别:
1.wait 的设计就是为了提前唤醒的.超时时间,是"后手"(B计划)
sleep 的设计就是为了到时间唤醒.虽然也可以通过 Interrupt() 提前唤醒,这样的唤醒是会产生异常的(程序出现不符合预期 的情况, 才称为"异常")
2.wait 需要搭配锁来使用. wait 执行时会先释放锁。sleep 不需要搭配锁使用.当把 sleep 放到 synchronized 内部时,不会释放锁(抱着锁睡的)
另外,实际开发中,wait 比 sleep 用的更多的。

当然为了面试的⽬的,我们还是总结下:
(1) wait 需要搭配 synchronized 使⽤. sleep 不需要.
(2)wait 是 Object 的⽅法 sleep 是 Thread 的静态⽅法.

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐