等待-通知 机制:wait 和 notify
------能够从应用层面上,干预到多个不同线程代码的执行顺序。~这里说的干预,不是影响系统的线程调度策略(内核里调度线程,仍然是无需调度)~
相当于是在应用程序代码中,让后执行的线程,主动放弃被调度的机会,就可以让先执行的线程,先把对应的代码执行完了。
【用途】
------场景:【线程饿死/线程饥饿】
拿到锁的线程,由于条件不足,无法执行后边的代码,释放锁之后,也参与锁竞争。此时,完全有可能导致,该线程反复获取锁,但是又无法完成实质性的逻辑。其他线程又无法拿到锁。这个情况,就称为:线程饿死/线程饥饿
------可能性:【概率】这种情况出现的概率还是挺高的。
原因:原来拿到锁的线程个,处于RUNNABLE状态;其他线程因为锁冲突出现阻塞,处于BLOCKED状态。原来的线程"近水楼台先得月" ~ ~不用背唤醒,直接就能参与到锁竞争;其他线程需要被唤醒之后,才能参与到锁的竞争。(当然,这里谁能竞争到,也是一个复杂的过程)
------影响:【严重性】是bug,虽然没有死锁那么严重,但是也需要处理。
------解决:【办法:wait 和 notify】
出现这个问题的关键:这个拿到锁的线程,在发现自己要执行的逻辑,不具备前提条件时,应该主动放弃对锁的竞争(主动放弃去CPU上调度执行),进入阻塞。 一直等到,条件具备了(可能是其他线程代码逻辑导致的),此时再解除阻塞,参与锁竞争。
"主动放弃对锁的竞争/主动放弃去CPU上执行"------wait阻塞
"条件具备,解除阻塞"------notify唤醒
------例子:【打印函数】打印函数内部,也是有加锁控制的。
两个线程都要往同一个控制台上打印,如果不加锁控制,也可能会有线程安全问题。比如:打印函数要打印的内容,打印出来的日志,是连续的、完整的,而不是断断续续的出现、混着的。
【原理】
------【wait】
wait的内部做了三件事:
- 释放锁
- 进入阻塞等待
由1.2=》其他线程就有机会拿到锁了。 - 当其他线程调用notify的时候,wait解除阻塞,并重新获取到锁
wait 和 join 的区别:
join: 等待另一个线程执行完,才继续执行
wait: 等待另一个线程通过notify进行通知(不要求另一个线程执行完)
注意:
wait进入阻塞,只能说明自己释放锁了。是否有其他线程拿到锁,另当别论(如果代码中只有一个线程......)
阻塞,产生的原因有好几种:
1.sleep: TIMED_WAITING
2.join/wait: WAITING
3.synchronized: BLOCKED
wait提供了两个版本:
死等:wait()("鲁棒性"差,容错性差,一般不用死等这个版本)
带有超时时间的等待:wait(long timeout) [ 单位:毫秒,ms]。如果这个时间内,没人进行notify,就不等了。("鲁棒性"好,容错能力更强,即使出现一些错误,也不会有太大影响,甚至能自动恢复。)
(其实还有一个:wait(long timeout, int nanos),不过,这个是ns为单位,做不到这么精准)
【使用】随便拿一个对象,都可以进行wait。
关系对应一致 ------synchronized、wait、notify,都是用同一个对象。
可以想象成,每个对象里都有一把锁。
调用wait的对象,必须和synchronized中的锁对象是一致的。
因此,wait解锁,必然是解的synchronized的同一把锁;后续,wait被唤醒之后,重新获取锁,当然还是获取到的同一把锁。
先拿到锁,wait才能释放锁 ------wait必须放到synchronized里面使用
直接调用wait,会出现异常。
改正:
注意:
1.唤醒-重新参与锁竞争------
如果有其他线程也尝试获取锁,wait被唤醒之后,也是需要重新参与到锁竞争中的。(并不是唤醒之后就直接拿到之前的锁)
可以用jconsole对wait时的线程状态进行观测:
2.可能由interrupt导致异常------
wait和sleep、join都是一类的。都肯能会被interrupt提前唤醒。记得用"try-catch"包裹起来
------【notify】
notify也要放到synchronized里边
Java中的特别约定。
与wait不同:
wait必须放------先加上锁,wait才能释放锁;notify其实可以不放------不需要先加锁。
操作系统原生的api,也有wait和notify,但是,原生的wait需要先加锁,原生的notify就不需要。这里只是Java特别约定。
------【代码示例:wait和notify联合使用】
public class ThreadDemo12 {
public static void main(String[] args) {
Object lock = new Object();
Thread t1 = new Thread(()->{
synchronized (lock) {
System.out.println("wait 之前:");//在代码中写一些日志,会在执行结果中显示出这些日志,可用于"定位""监测"......
try {
lock.wait();
System.out.println("wait 之后");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
Thread t2 = new Thread(()->{
try {
Thread.sleep(5000);
//注意synchronized的位置:放到了try里边,和sleep一起,这样保证不会再sleep被打断的时候,跟t1竞争锁导致t1中的wait在t2中的notify之后执行而导致错误。
synchronized (lock) {
System.out.println("notify之前");
lock.notify();
System.out.println("notify之后");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
t1.start();
t2.start();
}
}
t1线程启动,就会调用wait,进入阻塞等待。
t2线程启动,就会先sleep,sleep时间到了之后,再进行notify唤醒t1(注意:唤醒t1之后,t1不是从头开始执行的,而是从wait位置开始,继续往下执行)
注意:sleep写到synchronized外面。否则,由于t1,t2执行顺序不确定,就可能导致t2先拿到锁,此时t1就没执行到wait,t2就先notify了,结果就不符合预期了。
(需要确保,代码是先执行wait,后执行notify)如果先notify,虽然不会有副作用(不会出现异常之类的)但是wait就无法唤醒了,逻辑上是有问题的。
------【notifyAll】唤醒这个对象上所有等待的线程。
[ 注意:这些线程在wait返回的时候,要重新获取锁,就会因为锁竞争,使这些线程实际上是一个一个串行执行的。 谁先拿到锁,谁后拿到,也是不确定的 ]
相比之下,还是更倾向于使用notify。notifyAll,全部唤醒之后,不太好控制 ~ ~
------wait和notify,不仅锁对象要对应。数量也有影响。
(1)
object1.wait();
object2.notify();
此时无法唤醒。必须是两个对象一致才能唤醒。
(2)
如果这两个wait,使用同一个对象调用,notify随机唤醒其中一个。
------【wait和sleep的区别 ~ ~】
相同点:
wait提供了一个带有超时时间的版本。
sleep也是能指定时间 ~ ~
都是到时间,就继续执行,解除阻塞了。
不同点:
wait和sleep都可以被提前唤醒(虽然时间没到,但是也能提前唤醒)
wait通过notify唤醒。(wait也能被interrupt唤醒哈 ~)
sleep通过interrupt唤醒。
但是!!!
二者的"场景期待"不同。
(1)使用wait,最主要的目标,一定是不知道要等多久的前提下,利用所谓的"超时时间"进行**"兜底"**
[ 例子 ]多数情况下,wait都是在超时时间内就被唤醒了。例如:wait设计的超时时间是1000ms,多数情况下,100ms或200ms就唤醒了,只有非常少(小于1%)的情况下,才会触发1000ms超时。
(2)使用sleep,一定是知道要等多久的前提下。虽然能提前唤醒,但是通过异常唤醒,这个操作不应该作为"正常的业务流程"。(通过异常唤醒,说明程序应该是出现一些特殊情况了)
[ 例子 ]比如写了sleep1000,。对于sleep,多数情况下,是希望1000ms准时,被唤醒。少数极端情况,通过特殊手段提前唤醒 ~ ~(而sleep提前唤醒,是通过异常的方式)
正常的业务流程不应该依赖异常处理。异常处理认为是在进行一些补救措施 ~ ~
[综合举例]
开门------
a.常规手段:用钥匙开门,从门进去
b.非常手段:忘记带钥匙,钥匙锁屋里了 ~ ~ 请个开锁师傅,帮忙把门打开。
=>除非是万不得已,才能使用非常规手段,正常当然是使用常规手段了 ~ ~
[ 最后 ]上述所说的wait 和 sleep 的应用场景,当然也不绝对。如果硬要使用wait来替代sleep,也不是不行。不过这,种做法写出的代码,并非大家都认同的代码 ~ ~(非常少见)
小结(关于多线程的一些基础用法)
围绕Thread类,各种方法来展开 ~ ~
-
线程的基本概念、线程的特性、线程和进程 的区别 ~ ~
-
Thread类创建线程(的方法,好几种)
-
Thread一些属性
-
启动线程
-
终止线程
-
等待线程
-
获取线程引用
-
线程休眠
-
线程状态
-
线程安全问题
(1)线程安全问题产生的原因
(2)如何解决 =>加锁 synchronized
(3)死锁问题
(4)内存可见性导致的线程安全问题 => volatile
-
线程的 等待-通知(wait-notify)控制线程的执行顺序
------多线程编程,主要就是使用到上面的这些内容 ~ ~
------(本段知识完,本节课的内容分开写,下一篇博客写本节其他内容,这样看起来知识点更清晰 ~~)