🥰🥰🥰来都来了,不妨点个关注叭!
👉博客主页:欢迎各位大佬!👈
文章目录
- 1. 为什么需要wait()方法和notify()方法?
- 2. wait()方法
- 2.1 wait()方法的作用
- 2.2 wait()做的事情
- 2.3 wait()结束等待的条件
- 2.4 带参数的wait方法 —— wait(timeout)
- 2.5 wait()必须写在 synchronized 代码块里
- 3. notify()方法
- 3.1 notify()方法的作用
- 3.2 notify()方法的用法
- 3.3 notify()方法必须写在 synchronized 代码块里
- 3.4 notifyAll() 方法
- 4. 面试题 —— join()、sleep()方法和wait()方法的对比
- 4.1 join()和wait()方法的区别
- 4.1.1 从java包来看
- 4.1.2 从作用效果来看
- 4.2 sleep()方法和wait()方法的对比
- 4.2.1 相同点
- 4.2.2 不同点
通过之前的学习,我们知道线程的调度是无序的,随机的,但在一定的需求场景下,我们希望线程是有序执行的~
join()方法算是一种控制顺序的方式,但是功效是有限的,所以接下来,本期内容具体介绍两种方法:wait()和notify(),合理的协调多个线程之间的执行先后顺序
1. 为什么需要wait()方法和notify()方法?
举一个栗子~ 假如小万和小丁约好一起去吃自助餐,小丁进去之后发现自己喜欢吃的菜都被拿光了!!!而此时工作人员还没有进行补货~ 发生的一系列故事
【解释说明】
这里想要的菜看作是锁,而5个人看作是竞争锁的5个线程,小丁在这个菜面前进进出出,其实并没有实质性地释放锁,但由于这盘菜始终没有工作人员补货,小丁也拿不到自己想要吃的菜,小丁就会陷入忙等,而其它人又竞争不到这盘菜,可看作是线程竞争不到CPU资源,其他人就处于一直在阻塞的状态,也什么事情都干不了
所有线程都可以竞争这个锁,这里就会出现一个极端的情况:线程刚释放锁又是该线程获得锁,发现里面没有货,又释放又进去,一直循环着,而其它线程拿不到锁,处于阻塞状态啥也干不了,导致一个问题 —— 线程饿死
这里引入了一个新的概念—— 线程饿死,通过上述案例,总结为:
【线程饿死】指一个或多个线程由于某种原因无法获取所需的执行机会,导致它们无法继续正常执行,从而被阻塞在某个状态,不能完成其任务,这种情况通常是优先级设置不当导致的
上述问题如何解决呢?
有些同学可能会觉得线程有记账信息,可以避免此问题,但实际上,并不能!!!
线程的记账信息其实是一个比较宏观的东西,它需要多个线程多运行一段时间才能生成,对于上面的案例,线程饿死,同一个线程进进出出的情况是一瞬间的事,故线程的记账信息无法解决~
正确:使用wait()和notify()可以有效解决上述问题!!! 上述情况如下:
【解释说明】
小丁站在菜面前发现没菜时,就先wait()释放锁,并进行阻塞等待,即暂时不参与CPU调度,不参与锁竞争,当服务员进行补菜,通知notify小丁有菜了,可以去拿菜了,小丁再去重新竞争资源拿到菜,小丁在阻塞等待时,小万和其他3个人,也是要拿这盘菜,但是条件不满足,也是wait()等待就行
【wait()】发现条件不满足或是时机不成熟时,线程就先阻塞等待
【notify()】其它线程构造一个成熟的条件,就可以唤醒该线程,唤醒后就可以参与所竞争了
协调好多个线程的执行顺序是很重要的,其实在日常生活中,还有很多这样的案例,为了更深刻理解,再比如小丁还喜欢打篮球,球场上的每个人都是独立的"执行流" ,即每个人可当作是一个线程,完成一个具体的进攻得分好几个动作, 需要多个人一起相互配合,必须按照一定的顺序执行,例如1号球员先传球,2号球员拿到球才能扣篮,没拿到球时只能wait()阻塞等待,对应线程中,线程1 先 “传球” 完成自己的任务,通知notify()2号球员已经传球了,线程2 拿到球才能 "扣篮"才能完成自己的任务,是讲究顺序的,wait()和notify()就是解决上述问题的
【wait()和notify()的作用】即合理的协调多个线程之间的执行先后顺序
2. wait()方法
2.1 wait()方法的作用
作用:让某个线程先暂停下来,等一等
wait()方法的初心就是阻塞等待,让线程进入 WAITING 状态!不过我们要区分开来,不要把概念弄混淆了,
wait()方法导致阻塞,竞争锁也可以导致阻塞,这是两种不同线程进入阻塞的方式!
【注意】wait()和notify()是Object的方法,只要是个类对象,不是内置类型(也叫基本数据类型),都可以使用wait()和notify()方法
2.2 wait()做的事情
1)释放当前的锁
2)使当前执行代码的线程进行阻塞等待(即把线程放到等待队列中)
3)满足一定条件时收到通知,被唤醒,同时重新尝试获取这个锁
2.3 wait()结束等待的条件
1)其他线程调用该对象的 notify() 方法(这里强调同一对象)
2)wait()方法等待时间超时 (wait() 方法会提供一个带有 timeout 参数的版本,来指定等待时间,超过这个时间,wait()就会结束)
3)其他线程调用该等待线程的 interrupted() 方法,导致 wait() 抛出 InterruptedException 异常
2.4 带参数的wait方法 —— wait(timeout)
wait() 方法也提供了一个带参数的版本,timeout参数即为指定的最大等待时间
不带参数的wait()即为死等,只有notify()方法能唤醒它
带参数的wait(timeout),则为等到最大时间还没有通知,就自己唤醒自己
2.5 wait()必须写在 synchronized 代码块里
这里需要重要注意: wait()必须写在 synchronized 代码块里!!! 两者必须搭配使用,否则会抛出异常
public class Demo1 {public static void main(String[] args) throws InterruptedException {Object obj = new Object();System.out.println("wait前:");obj.wait();System.out.println("wait后:");}
}
上述代码中,直接调用obj.wait(),并没有进行加锁,运行后,抛出 IllegalMonitorStateException,即非法的锁状态异常,运行结果如下:
【为什么会出现这种情况?】
回顾wait()需要做的事情,先进行解锁!!! 如果这把锁都没获取到,就尝试解锁,就会产生异常!(在现实生活中,如果你开门,都没有这把锁,就尝试开锁,东西都没有,咋进行开锁操作捏!所以就会抛出异常)
所以在使用wait()方法前,必须要先进行加锁,就是把wait()写在 synchronized 代码块里面!!!
【注意事项】同时需要注意,加锁的锁对象必须要和wait()的锁对象是同一个,如果加锁对象和调用wait()对象不是同一个,也会抛出IllegalMonitorStateException 异常!!!
正确使用wait()方法如下:
public class Demo1 {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()!!! 下面notify()方法闪亮登场~
3. notify()方法
3.1 notify()方法的作用
作用:把该线程唤醒,使其能够继续执行
3.2 notify()方法的用法
1)notify()方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,同时使它们重新获取该对象的对象锁
2)如果有多个线程等待,则由线程调度器随机挑选出一个呈 wait 状态的线程,并不遵循"先来后到"的原则,仍然是随机的
3)在notify()方法后,当前线程不会立刻释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出synchronized 代码块之后才会释放对象锁
在接下来的代码里,我们会更深刻理解以上3点
3.3 notify()方法必须写在 synchronized 代码块里
这里需要重要注意: notify()也必须写在 synchronized 代码块里!!! 两者必须搭配使用,否则会抛出异常
【注意事项】同时需要注意,必须先执行wait()方法,然后再执行notify()方法,此时才会有效果,试想一下,如果现在还没有执行wait(),就执行notify()方法,这不就相当于一炮打空嘛~没啥效果!没有起到实际作用,虽然没有额外的副作用,也不会抛出异常,但是代码的功能就不能正确执行了
使用wait()和notify()方法,更好理解wait()和notify()方法的用法,代码如下:
public class Demo1 {public static void main(String[] args) throws InterruptedException {Object obj = new Object();Thread t1 = new Thread(() -> {try {System.out.println("wait开始");synchronized (obj) {obj.wait();}System.out.println("wait结束");} catch (InterruptedException e) {e.printStackTrace();}});t1.start();Thread.sleep(1000); //保证t1先启动,wait()先执行Thread t2 = new Thread(() -> {synchronized (obj) {System.out.println("notify开始");obj.notify();System.out.println("notify结束");}});t2.start();}
}
分别创建 t1 和 t2 两个线程,且它们对同一个对象加锁,在此代码中让 t1 线程中执行wait(),t2 线程中执行notify(),先后启动t1和t2线程,观察代码运行结果:
【解释说明】
1)t1 线程先执行,执行到wait()方法,t1 线程的锁就被wait()方法释放,且 t1 线程自身就阻塞等待了,
2)1s之后 t2 线程开始执行,执行到notify()方法,就会通知 t1 线程,t1 线程被唤醒,继续执行
需要注意的是,notify()方法在 synchronized 代码块内部,因此,只有等 t2 线程释放锁之后,t1 线程才能再竞争到锁,t1 才能继续往下执行,所以先打印的是"notify()结束",再是打印"wait结束"
在上述代码中,虽然是 t1 线程先执行的,但是可以通过wait()方法、notify() 方法的控制,让 t2 线程先执行一部分逻辑,执行完后,t2 线程通过 notify()方法唤醒 t1 线程,使 t1 线程继续往下执行!这正是它们的意义,wait()方法、notify() 方法可以合理的协调多个线程之间的执行先后顺序,使线程执行顺序变得可控起来!
3.4 notifyAll() 方法
notify()方法只是唤醒某一个等待线程,使用notifyAll()方法可以一次唤醒等待同一对象的所有线程
存在这样一个情况:可以有多个线程,等待同一个对象,比如在 t1、t2、t3 线程中,都调用 object.wait()
1)此时在 main 线程中调用 object.notify(),就会随机唤醒上述三个线程中的一个,而另外两个线程仍然是处于 WAITING 状态,
2)但是如果调用 object.notifyAll(),此时就会把上述三个线程全部唤醒,此时这三个线程就会重新竞争锁,再依次执行
【注意】此时需要三个线程都wait()等待,再通知,不然又要空打一炮啦
4. 面试题 —— join()、sleep()方法和wait()方法的对比
4.1 join()和wait()方法的区别
4.1.1 从java包来看
【join()方法】
join()方法在java.lang.Thread 声明
【wait()方法】
wait()方法在java.lang.Object 声明
4.1.2 从作用效果来看
【join()方法】
当在 t1 线程中调用了t2.join(),这是让 t1 线程等待 t2 线程全部执行完毕才能再执行,这使得线程之间的执行从"并行"变成"串行"
【wait()方法】
wait()和notify()方法搭配使用,可以让 t2 线程执行一部分,再让 t1 线程执行一部分,t1 线程执行一部分再让 t2 线程执行一部分…
4.2 sleep()方法和wait()方法的对比
4.2.1 相同点
wait()有一个带参数版本,用来体现超时时间,超过这个时间会被自动唤醒,此时和sleep()方法类似
同时,wait()和sleep()方法都能提前被唤醒
4.2.2 不同点
1)最大的区别:在于两者的初心不同,即设计这个东西到底要解决啥问题的不同
【sleep()】sleep()方法单纯是让当前线程休眠一会
【wait()】wait()解决的是线程之间的顺序控制
2)进一步的,实现或使用上,也是有明显区别的,wait()需要搭配锁使用,而sleep不需要,sleep()方法是让程序按照指定时间短暂休眠让出CPU给其它线程,到时间自动恢复,而不带参数的wait()只有被唤醒后,线程才能重新尝试获得锁,得到锁后才能继续执行
本期内容主要讲解如何在实际中,控制线程调度的顺序~
💛💛💛本期内容回顾💛💛💛
✨✨✨本期内容到此结束啦~