【多线程】线程的等待通知机制-wait与notify

💐个人主页:初晴~

📚相关专栏:多线程 / javaEE初阶


        我们都知道,线程在系统调度上是随机的,因此线程之间执⾏的先后顺序难以预知。但在实际开发中有时我们希望控制多个线程执行某个逻辑的先后顺序,就可以让后执行的逻辑使用wait,先执行的线程完成某些逻辑后,再通过notify唤醒对应的线程,从而使多个线程以一定的顺序运行。那么本篇文章就让我们深入地去探讨wait与notify的特点与应用吧

目录

一、wait()

二、notify()

三、notifyAll()

四、等待通知机制的简单应用

五、wait和sleep的对比

六、线程饿死


一、wait()

wait 做的事情:
  • 使当前执⾏代码的线程进⾏等待. (把线程放到等待队列中)
  • 释放当前的锁
  • 满⾜⼀定条件时被唤醒, 重新尝试获取这个锁.
注意: wait 要搭配 synchronized 来使⽤. 脱离 synchronized 使⽤ wait 会直接抛出异常
我们发现抛出了一个 “IllegalMonitorStateException”异常   大致意思就是“非法的锁状态异常”。就是说调用wait时,当前锁的状态是非法的(不正常的)。因为调用wait()后,会进行一个针对locker对象先解锁的操作,所以,必须要在synchronized代码块中,才能够使用wait(),因为显然要先上锁,才能够正常地解锁
wait 结束等待的条件:
其他线程调⽤该对象的 notify ⽅法告诉该对象不用再等了.
wait 等待时间超时 (wait ⽅法提供⼀个带有 timeout 参数的版本, 来指定等待时间).
其他线程调⽤该等待线程的 interrupted ⽅法, 导致 wait 抛出 InterruptedException 异常.

 wait()应用示例:

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

    }
}

 这时main线程就会进入阻塞等待的状态,等待其它线程的唤醒。

我们可以通过jconsole来观察到此时main线程的状态:


wait默认是 死等,这容易导致线程永久陷入阻塞状态,并不是非常合适的
因此wait还提供了带参数的版本,指定 超时时间
如果wait达到了设定的最大时间,即使还没有收到notify通知也 不会继续等待了,而是会重新恢复到 RUNNABLE状态,重新加入锁竞争的行列。
public class Main {
    public static void main(String[] args) throws InterruptedException {
        Object locker=new Object();
        synchronized (locker){
            System.out.println("wait 之前");
            locker.wait(100000);
            System.out.println("wait 之后");
        }

    }
}

·注意·实际开发一般都会根据需要写入等待时间

执行wait要进行解锁,也要进行阻塞等待。

注意:这两步操作是 同时执行的(符合 原子性的)
那么如果不是原子性的会出现什么问题呢?

二、notify()

 notify方法是通过另一个线程,去通知处于WAITING状态的线程被唤醒

我们观察一下以下代码:

public class Main {
    private static Object locker=new Object();

    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            synchronized (locker){
                System.out.println("t1 wait 之前");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t1 wait 之后");
            }
        });
        Thread t2=new Thread(()->{
            System.out.println("t2 notify 之前");
            Scanner in=new Scanner(System.in);
            in.next();  //此处输入内容不重要,主要是构成一个阻塞,保证t1能进入wait
            locker.notify();
            System.out.println("t2 notify 之后");
        });

        t1.start();
        t2.start();
    }
}

我们可以发现又抛出了那个熟悉的“IllegalMonitorStateException”异常,也就是非法的锁状态异常,因为在多线程中,一个线程加锁,另一个线程不加锁,是没有意义的,不会产生任何阻塞效果。不光要对执行wait的线程加锁,对执行notify的线程也要加锁才行:

public class Main {
    private static Object locker=new Object();

    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            synchronized (locker){
                System.out.println("t1 wait 之前");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t1 wait 之后");
            }
        });
        Thread t2=new Thread(()->{
            synchronized (locker){
                System.out.println("t2 notify 之前");
                Scanner in=new Scanner(System.in);
                in.next();  //此处输入内容不重要,主要是构成一个阻塞,保证t1能进入wait
                locker.notify();
                System.out.println("t2 notify 之后");
            }

        });

        t1.start();
        t2.start();
    }
}

  • ⽅法notify()也要在同步⽅法或同步块中调⽤,该⽅法是⽤来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
  • 如果有多个线程等待,则有线程调度器随机挑选出⼀个呈 wait 状态的线程。(并没有 "先来后到")
  • 在notify()⽅法后,当前线程不会⻢上释放该对象锁,要等到执⾏notify()⽅法的线程将程序执⾏完,也就是退出同步代码块之后才会释放对象锁。

三、notifyAll()

notify只是唤醒wait中的某个线程notifyAll会唤醒wait中的所有线程

不过一个一个唤醒,整个程序的执行是相对有序的,如果一下唤醒所有线程,这些线程就会开始无序的进行锁竞争

比如在面试的时候,可能有多个房间,每个房间一个面试官。所有候选人在大厅等待。

  • notify就相当于是让HR喊XXX进入某个房间面试
  • notifyAll就相当于HR喊某个房间有空位了,你们快去,先到先得。

显然notify会更加井然有序一些。因此在日常开发中,一般都会采用notify来一个一个唤醒

不过这时可能会有人担心,使用notify是不是得注意不能用太多了。当notify数量超过wait线程的数量是否会出现bug呢?

答案是不会的,我们把之前写的t2线程多加入几个notify试试:

最后的运行结果依然是没有问题的:

四、等待通知机制的简单应用

在前文中我们提到过,等待通知机制可以控制多个线程执行某个逻辑的先后顺序,接下来我们就来看看具体的实现方式吧:

场景描述:创建三个线程分别打印A、B、C,通过wait、notify来控制线程的打印顺序,先打印A,再打印B,最后打印C

public class Main {
    private static Object locker1=new Object();
    private static Object locker2=new Object();
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            System.out.println("A");
            synchronized (locker1){
                locker1.notify();
            }
        });
        Thread t2=new Thread(()->{
            synchronized (locker1){
                try {
                    locker1.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("B");
            synchronized (locker2){
                locker2.notify();
            }

        });
        Thread t3=new Thread(()->{
            synchronized (locker2){
                try {
                    locker2.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("C");
        });
        t1.start();
        t2.start();
        t3.start();
    }
}

但我们会发现这样的代码可能会出现bug:

如果这段代码是先执行t2的wait,后执行t1的notify,那么就不会出现问题。但由于线程三个线程是并发执行的,所以一定概率上,t1先完成了打印和notify的操作,然后t2才执行wait,这样t2就会错过t1的通知,后面也没有线程来唤醒t2,就导致一直处于阻塞等待的wait状态了

解决方法也很简单,在t1中加入sleep,让t1先等待一段时间,保证t2,t3都进入wait状态后,再继续执行notify发送通知就行了:

运行结果:


五、wait和sleep的对比

wait(1000)与sleep(1000)看起来好像是十分相似的,都是让线程进入阻塞状态,但事实上它们是有着本质区别的

  • sleep就是固定时间的阻塞,不涉及唤醒操作。而wait则可以被notify提前唤醒
  • wait必须搭配synchronized使用,并且wait会先释放锁,同时进行阻塞等待
  • sleep与锁无关,不加锁也能正常使用。如果加了锁,sleep并不会释放锁,在阻塞期间其它线程是无法拿到锁的
  • wait 是 Object 的⽅法 ,sleep 是 Thread 的静态⽅法
  • wait主要是用来线程间通信的,而sleep就是单纯的阻塞操作

六、线程饿死

        由于多线程是抢占式并发执行的,在加锁后,多个线程会去同时竞争这把锁。假设这时线程a抢到了锁1,但是由于缺乏一些必要条件导致CPU并没有真正地执行线程a,这时线程a会释放锁1并重新开始与其它线程竞争锁1。如果在极端情况下,一直都是线程a抢到了锁1,其它线程就会一直处于阻塞状态,就像抢不到食物而被饿死了一样。这种现象就被称为“线程饿死”。这会大大降低线程的执行效率,导致所有的线程都无法按预期正常工作。

        这时也可以利用等待通知机制来解决这一问题。当线程a抢到锁后,会判断当前是否符合执行条件,如果不符合,就会主动通过wait方法释放锁进入阻塞队列,并放弃对锁(CPU资源)的竞争,这样就不会出现“线程饿死”问题了。直到其它线程判断符合其执行条件时,会调用notify方法通知阻塞线程a,此时线程a才会重新参与锁的竞争


那么本篇文章就到此为止了,如果觉得这篇文章对你有帮助的话,可以点一下关注和点赞来支持作者哦。作者还是一个萌新,如果有什么讲的不对的地方欢迎在评论区指出,希望能够和你们一起进步✊

评论 31
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值