【JavaEE初阶】深入理解wait和notify以及线程饿死的解决

发布于:2024-10-09 ⋅ 阅读:(49) ⋅ 点赞:(0)

前言:

🌈上期博客:【JavaEE初阶】深入解析死锁的产生和避免以及内存不可见问题-CSDN博客

🔥感兴趣的小伙伴看一看小编主页:【JavaEE初阶】深入解析死锁的产生和避免以及内存不可见问题-CSDN博客

⭐️小编会在后端开发的学习中不断更新~~~  

🥳非常感谢你的支持

 

目录

📚️1.引言

 📚️2.wait和notify

2.1wait作用

 2.2使用场景和线程饿死

 2.3wait如何使用

1.wait的锁对象

2.wait必须在synchronized内

3.调用wait的锁对象

4.notify的使用

📚️3.理解执行顺序

📚️4.注意事项

4.1wait不同的方法

4.2notify唤醒wait

4.3wait和sleep的区别

📚️5.总结


 

📚️1.引言

 OK啊!!!小伙伴们,在上期继小编讲解过死锁的问题后,本期将开始理解wait和notify的线程问题,那么废话不多说,直接步入正题,go go go~~~;

且听小编讲解,包你学会!!! 

 📚️2.wait和notify

2.1wait作用

这里的作用和上期讲解过的join有异曲同工之妙,都是在多线程随机调度中,通过引入wait和notify来实现干预不同线程的执行顺序,让后执行的线程不被调度,让先执行的线程把对应的代码执行完

 2.2使用场景和线程饿死

例如一个现实场景:取钱问题~~~

这里的ATM可能是没有钱的,那么此时当一个人去取完钱后,解锁后,其他滑稽老铁进入锁的竞争,但是此时刚刚取钱的滑稽老铁也会参与到锁的竞争,就导致此时壹号滑稽老铁由于竞争,和ATM没有钱,而一直反复获取锁,而导致其他的滑稽不能拿到锁,这就是“线程饿死” 

注意:由于已经拿到锁的滑稽老铁会处于RUNNABLE状态,但是其他线程处于阻塞状态,那么此时就是“近水楼台先得月” ,其他线程需要进行唤醒,才能参与到锁的竞争,那么就会导致壹号滑稽老铁更容易获取到锁~~~

此时就要运用到wait和notify了,让线程满足条件进行加锁执行,若不满足条件,那么就进入wait,直到满足条件后被notify唤醒~~~

下面是一个伪代码

 public static void main(String[] args) {
        while (true){
            synchronized (){
                if (ATM中有钱){
                    进行取钱的操作 
                break;
                }else {
                    进入等待;
                    wait(){
                        直到有钱然后被唤醒
                    }
                }
            }
        }
    }

所以这就是我们所需要看到的情况;

注意:wait在这里做了三件事情

1.释放锁,让其他线程获取锁

2.让线程进入阻塞状态

3.被notify唤醒后,重新参与锁的竞争

 2.3wait如何使用

1.wait的锁对象

这里和加锁是一样的都是需要锁对象,当然这里的锁对象是可以为任何对象的,小编就不再过多解释

2.wait必须在synchronized内

此时如果我们将wait写到synchronized之外,代码如下:

 public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
            System.out.println("wait 之前");
            object.wait();
            System.out.println("wait 之后");
       
    }

输出打印日志:

wait 之前
Exception in thread "main" java.lang.IllegalMonitorStateException
	at java.lang.Object.wait(Native Method)
	at java.lang.Object.wait(Object.java:502)
	at thread.testDemo17.main(testDemo17.java:7)

可以看到此时程序报错了;

注意:wait使用的前提是存在锁,所以在使用wait之前线程必须先获取锁。

3.调用wait的锁对象

在调用wait的锁对象必须和加锁的synchronized是同一个锁对象,所以wait解锁是synchronized的锁,重新唤醒后加锁也是synchronized的锁,代码如下:

public static void main(String[] args) throws InterruptedException {
        Object object = new Object();

        synchronized (object) {
            System.out.println("wait 之前");
            object.wait();
            System.out.println("wait 之后");
        }
    }

此时输出就只有wait之前,因为此时线程进入阻塞了,咱们打开jconsole可以看出到此时的线程状况,如图所示:

此时代码进入WAITING状态~~~

4.notify的使用

注意此时,notify使用与其他的线程,并且调用notify的锁对象,也必须和调用wait的锁对象必须是一样的,唤醒的即对应锁对象调用的wait方法的线程;

📚️3.理解执行顺序

这里需要注意,在使用wait和notify后的代码是如何进行执行的,代码实例如下:

public static void main(String[] args) {
        Object lock=new Object();
        //创建线程1
        Thread t1=new Thread(()->{
            synchronized (lock){
                System.out.println("wait之前");
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("wait之后");
            }
        });
        Thread t2=new Thread(()->{
            try {
                Thread.sleep(1000);//保证线程1能够成功加上锁
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            synchronized (lock){
                System.out.println("notify之前");
                lock.notify();
                System.out.println("notify之后");
            }
        });
        t1.start();
        t2.start();
    }

注意:小编这里为了保证线程1成功加上锁,那么在执行线程2时,就会进入休眠;还有wait和sleep一样,都有可能会被interrupt提前唤醒,所以这里在写wait时,需要进行异常抛出;

输出结果:

那么此时的执行顺序就是如下的:

1.首先线程1开始执行,加锁后执行打印“wait之前”,然后进入等待状态,释放锁并进入阻塞状态;

2.线程2开始执行,获取到锁后打印“notify之前”,然后唤醒线程一;

3.由于线程1处于WAITING状态,被唤醒后,由于锁的竞争会处于一个小小的阻塞,在获取锁这个阶段需要时间开销,所以先打印“notify之后”

4.最后线程1获取到锁后,最后执行最后的打印“wait之后”

📚️4.注意事项

4.1wait不同的方法

在进行wait方法的调用时,可以看到有三个其他的版本,如下图:

第一种:即死等,若没有进程进行唤醒操作,此时就会导致这个线程不再执行

第二种:即超时时间,单位为ms,若没有线程唤醒,那么到了这个时间段,就不再进行等待了;

第三种:即纳秒级别的时间,由于系统精度,可能无法准确执行,且很少使用;

4.2notify唤醒wait

1.若两个锁对象调用的这两个方法,如果锁对象是不一样的,那么就无法进行唤醒;

2.若右两个锁对象调用wait,那么调用notify的锁对象,要唤醒对应的wait,若两个wait的锁对象是一样的,那么随机唤醒

3.notifyAll用于唤醒对应线程上的所有线程;注意:被唤醒的线程中,多个wait的执行,导致锁的竞争,那么此时那个限制性,那个后执行是不确定的;

4.3wait和sleep的区别

1.这里的wait也是带有时间的超时时间的wait方法,sleep也是一样的带有时间的,那么就是到时间就会继续执行;

2.wait和sleep虽然时间没有到,但是任然可以被提前唤醒;wait是通过notify进行唤醒,而sleep是通过interrupt进行提前唤醒;

3.使用wait,是不知道等待的时间的前提下使用的,所谓的超时时间只不过是一个“兜底时间”,而sleep是要知道时间的前提下才使用,虽然也能够被提前唤醒,但是这个是一个不正常的业务流程,(异常唤醒,这是说明代码出现了BUG了)

📚️5.总结

💬💬小编本期讲解了关于wait的使用方法,对应的notify的操作,以及线程饿死的场景演示;当然还有在使用wait和notify时的使用注意事项,还附上了对应代码供uu们参考参考~~~

🌅🌅🌅~~~~最后希望与诸君共勉,共同进步!!!


💪💪💪以上就是本期内容了, 感兴趣的话,就关注小编吧。

                                               😊😊  期待你的关注~~~