JavaEE初阶——多线程(等待-通知机制:wait-notify)

等待-通知 机制:wait 和 notify

------能够从应用层面上,干预到多个不同线程代码的执行顺序。~这里说的干预,不是影响系统的线程调度策略(内核里调度线程,仍然是无需调度)~

相当于是在应用程序代码中,让后执行的线程,主动放弃被调度的机会,就可以让先执行的线程,先把对应的代码执行完了。

【用途】

------场景:【线程饿死/线程饥饿】

拿到锁的线程,由于条件不足,无法执行后边的代码,释放锁之后,也参与锁竞争。此时,完全有可能导致,该线程反复获取锁,但是又无法完成实质性的逻辑。其他线程又无法拿到锁。这个情况,就称为:线程饿死/线程饥饿

------可能性:【概率】这种情况出现的概率还是挺高的。

原因:原来拿到锁的线程个,处于RUNNABLE状态;其他线程因为锁冲突出现阻塞,处于BLOCKED状态。原来的线程"近水楼台先得月" ~ ~不用背唤醒,直接就能参与到锁竞争;其他线程需要被唤醒之后,才能参与到锁的竞争。(当然,这里谁能竞争到,也是一个复杂的过程)

------影响:【严重性】是bug,虽然没有死锁那么严重,但是也需要处理。

------解决:【办法:wait 和 notify】

出现这个问题的关键:这个拿到锁的线程,在发现自己要执行的逻辑,不具备前提条件时,应该主动放弃对锁的竞争(主动放弃去CPU上调度执行),进入阻塞。 一直等到,条件具备了(可能是其他线程代码逻辑导致的),此时再解除阻塞,参与锁竞争。

"主动放弃对锁的竞争/主动放弃去CPU上执行"------wait阻塞

"条件具备,解除阻塞"------notify唤醒

------例子:【打印函数】打印函数内部,也是有加锁控制的。

两个线程都要往同一个控制台上打印,如果不加锁控制,也可能会有线程安全问题。比如:打印函数要打印的内容,打印出来的日志,是连续的、完整的,而不是断断续续的出现、混着的。

【原理】

------【wait】

wait的内部做了三件事:

  1. 释放锁
  2. 进入阻塞等待
    由1.2=》其他线程就有机会拿到锁了。
  3. 当其他线程调用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类,各种方法来展开 ~ ~

  1. 线程的基本概念、线程的特性、线程和进程 的区别 ~ ~

  2. Thread类创建线程(的方法,好几种)

  3. Thread一些属性

  4. 启动线程

  5. 终止线程

  6. 等待线程

  7. 获取线程引用

  8. 线程休眠

  9. 线程状态

  10. 线程安全问题

    (1)线程安全问题产生的原因

    (2)如何解决 =>加锁 synchronized

    (3)死锁问题

    (4)内存可见性导致的线程安全问题 => volatile

  11. 线程的 等待-通知(wait-notify)控制线程的执行顺序

------多线程编程,主要就是使用到上面的这些内容 ~ ~


------(本段知识完,本节课的内容分开写,下一篇博客写本节其他内容,这样看起来知识点更清晰 ~~)

相关推荐
customer0813 小时前
【开源免费】基于SpringBoot+Vue.JS课程智能组卷系统(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
AI人H哥会Java13 小时前
【JAVA】Java项目实战—Java EE项目:企业资源规划(ERP)系统
java·spring boot·mysql·java-ee
多敲代码防脱发1 天前
数据链路层(Java)(MAC与IP的区别)
运维·服务器·网络·java-ee
♢.*1 天前
JAVA实战:借助阿里云实现短信发送功能
java·阿里云·java-ee·短信
寻找沙漠的人2 天前
网络原理04
java·网络·java-ee
所待.3833 天前
JavaEE多线程案例之阻塞队列
java·java-ee
火烧屁屁啦3 天前
【JavaEE进阶】关于Maven
java·java-ee·maven
customer083 天前
【开源免费】基于SpringBoot+Vue.JS加油站管理系统(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·maven