JavaEE-多线程初阶(5)
目录
1. wait/notify
1.1 线程饿死
1.2 wait/notify的使用
1.2.1 wait
1.2.2 notify
1.3 notify必须要在wait之后
1.4 wait和sleep的区别
2. 当有多个贤臣在同一个对象上wait
2.1 随机唤醒
2.2 notifyAll
3. 作业题
1. wait/notify
wait/notify 等待/通知
协调线程之间的执行逻辑的顺序的。
可以让后执行的逻辑,等待先执行的逻辑先跑
虽然无法直接干预调度器的调度顺序
但是可以让后执行的逻辑(线程)等待,等待到先执行的逻辑跑完
然后通知一下后执行的线程,让他继续执行
类比一下join:
join:是等另一个线程彻底执行完,才继续走
wait:等待到另一个程序执行notify,才继续走(不需要另一个线程执行完)
1.1 线程饿死
当多个线程竞争同一把锁的时候,获取到锁的线程如果释放了,下一个拿到锁的线程是哪一个,这是不确定的,取决于操作系统的调度(随机)。
也就是说,当前这个释放锁的程序,很大概率能够再次拿到这个锁。
此时其他的线程就会一直处于阻塞状态(拿不到锁)。
这种情况,就称为线程饿死。
打一个比方:
把每个线程想象成鸟宝宝,cpu是鸟妈妈。鸟妈妈捉虫子给鸟宝宝吃,当同一个鸟宝宝一直吃鸟妈妈捉的虫子,那么其他鸟宝宝就会饿死。
上述场景,就是wait/notify使用的经典场景。
当拿到锁的线程,发现要执行的任务时机还不成熟的时候,就是用wait阻塞等待。
等到实际成熟了再使用notify通知它执行。
1.2 wait/notify的使用
wait 和 notify 都是 Object 的方法,Java中任意对象都提供了wait和notify
Java标准库中,每个产生阻塞的方法,都会抛出这个异常
意味着随时可能会被 Interrupt 方法给唤醒
1.2.1 wait
【案例】
简单创建一个线程,在该线程内执行wait方法:
public static void main(String[] args) throws InterruptedException {
Object locker=new Object();
Thread t1=new Thread(()->{
try {
locker.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
t1.start();
}
执行代码,代码报错:
报错原因:
使用 locker.wait() ,这个方法做的第一件事,就是先释放locker对象对应的锁
而能够释放锁的前提是,locker对象应处于加锁状态,才能被释放
【注意】要求 synchronized 的锁对象和 wait 的对象是同一个
为什么 wait 要先释放锁:
wait 这个等待,最关键的一点就是,要先释放锁,才能给其他线程获取锁的机会(防止其他线程一直竞争不到锁,解决线程饿死问题)
代码进入 wait ,就会先释放锁,并且阻塞等待
如果其他线程做完了必要的工作,就调用 notify 唤醒这个 wait 线程
wait 就会解除阻塞,重新获取到锁,继续执行并返回
1.2.2 notify
【案例】
让t1线程执行,并且t1线程内有wait方法阻塞,然后在main线程里调用sleep休眠两秒再执行notify唤醒t1线程,进程结束:
public static void main(String[] args) throws InterruptedException {
Object locker=new Object();
Thread t1=new Thread(()->{
try {
synchronized (locker){
locker.wait();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
t1.start();
Thread.sleep(2000);
locker.notify();
}
执行代码:
代码报错,并且仍然在持续执行,说明t1线程还在阻塞状态。
报错原因与上述wait的报错原因一样:非法的锁状态异常
原因:
notify 方法与 wait 一样,需要先拿到锁,再进行 notify(属于是Java中给出的限制)
解释:
wait操作必须要搭配锁来进行,是因为 wait 会先释放锁
notify 操作,原则上说不涉及到加锁解锁操作
但是在Java中,也强制要求 notify 搭配 synchronized
wait 和 notify 是针对同一个对象,才能生效的
这个相同的对象,是这俩线程沟通的桥梁
如果是两个不同的对象,则没有任何相互影响和作用
1.3 notify必须要在wait之后
务必要确保,先 wait,后 notify,才有作用
如果是先 notify,后 wait ,此时wait无法被唤醒
notify 的这个线程,没有副作用(notify 唤醒一个没有 wait 的线程,不会报错)
【案例】
让线程t1先sleep(5000),在此期间t2进行locker.notify操作,模拟先notify后wait的场景:
public class Demo14 {
public static void main(String[] args) throws InterruptedException {
Object locker=new Object();
Thread t1=new Thread(()->{
try {
Thread.sleep(5000);
System.out.println("wait之前");
synchronized (locker){
locker.wait();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("wait之后");
});
Thread t2= new Thread(()->{
System.out.println("输入任意内容,唤醒t1");
Scanner scanner=new Scanner(System.in);
scanner.next();
synchronized (locker){
locker.notify();
}
});
t1.start();
t2.start();
}
}
执行代码:
从结果可以看出,t1线程还在阻塞状态中,可以上述的结论:
如果是先 notify,后 wait ,此时wait无法被唤醒
此处的next就是一个带有阻塞的操作,等待用户在控制台输入:
如果把t1线程的sleep(5000)删除,t1线程可以正常被唤醒:
1.4 wait和sleep的区别
wait和join类似,也是提供了“死等”版本和“超时时间”版本:
wait 引入超时时间后,直观看起来,跟 sleep 很像:
wait 有等待时间
sleep 也有等待时间
wait 可以使用 notify 提前唤醒
sleep 也可以用 Interrupt 提前唤醒
(Interrupt 看起来是唤醒 sleep ,其实本身的作用是通知线程终止)
wait 和 sleep 最主要的区别,在于针对锁的操作:
(1)wait 必须搭配锁。先加锁,才能用 wait,sleep 则不需要
(2)如果都是在 synchronized 内部使用,wait 会释放锁,sleep 则不会释放锁
(sleep 是抱着锁睡,此时其他线程是无法获得这个锁的)
2. 当有多个贤臣在同一个对象上wait
2.1 随机唤醒
如果有多个线程在同一个对象上 wait ,进行 notify 的时候是随机唤醒其中一个线程。
但是,实际上同一个对象 wait 多次这种情况
一般这些 wait 的线程都是干同样的工作的,唤醒谁效果都一样。
2.2 notifyAll
notifyAll 可以一次唤醒所有的 wait 线程
【案例】
现有两个线程t1和t2,两个线程内部分别对locker对象加锁,并且在内部进行 wait 操作
启动两个线程,在 1s 后使用 notifyAll 唤醒所有用 locker 对象加锁的线程:
执行代码:
虽然同时唤醒了t1和t2,由于 wait 唤醒后,要重新加锁
其中某个线程,先加上锁,开始执行,另一个线程因为加锁失败,再次阻塞等待
等到先走的线程解锁了,后走的线程才能加上锁,继续执行
3. 作业题
有三个线程,分别只能打印A,B和C
要求按照要求打印ABC,打印十次
输出示例:
ABC
ABC
ABC
ABC
...
public static void main(String[] args) throws InterruptedException {
Object locker1=new Object();
Object locker2=new Object();
Object locker3=new Object();
Thread t1=new Thread(()->{
try {
for (int i = 0; i < 10; i++) {
synchronized (locker1){
locker1.wait();
}
System.out.print("A");
synchronized (locker2){
locker2.notify();
}
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
Thread t2=new Thread(()->{
try {
for (int i = 0; i < 10; i++) {
synchronized (locker2){
locker2.wait();
}
System.out.print("B");
synchronized (locker3){
locker3.notify();
}
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
Thread t3=new Thread(()->{
try {
for (int i = 0; i < 10; i++) {
synchronized (locker3){
locker3.wait();
}
System.out.println("C");
synchronized (locker1){
locker1.notify();
}
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
// 启动t1 t2 t3 线程
t1.start();
t2.start();
t3.start();
//等待三个线程都wait
Thread.sleep(1000);
//手动唤醒t1线程
synchronized (locker1){
locker1.notify();
}
}
【注意】
当三个线程启动的时候,这三个线程都处于 wait(阻塞)状态,需要在 mian 线程使用 notify 唤醒
线程 t1 ,才能够运行程序。
完
如果哪里有疑问的话欢迎来评论区指出和讨论,如果觉得文章有价值的话就请给我点个关注还有免费的收藏和赞吧,谢谢大家