JavaEE 初阶(9)——多线程7之 wait 和 notify

目录

一. 监视器锁与监视器

二. wait ()  

三. notify () 和 notifyAll ()

3.1 notify()

 3.2 notifyAll()

3.3 wait等待 和 sleep休眠 的对比(面试题)


wait (等待)/ notify (通知)

由于线程在操作系统上是“随机调度,抢占式执行”的,因此线程之间执行的先后顺序难以预知。但是实际开发中,有时候我们希望合理的协调多个线程之间执行的先后顺序,就可以让后执行的逻辑使用 wait 进行阻塞先执行的线程 完成某些逻辑之后,通过 notify 唤醒同一把锁下的wait


 形象的比喻

大家排队到ATM取款机取款,每次进去一个人后,门就会锁上。

当1号发现ATM中没钱了,就会出来等待运钞车存钱。但1号害怕其他人又进去,之后会立即回去,但此时ATM中依旧没钱,又会出去.....1号就这样进进出出,导致堵塞。

             

像这种情况 属于“线程饿死”,是概率性问题,和调度器具体的策略直接相关。

针对上述问题,同样也可以使用 wait 和 notify  来解决

1号拿到锁的时候,进行判定:当前能否执行 “取钱” 操作。如果能执行,就正常执行;如果不能执行,就需要主动释放锁,并且“阻塞等待”(通过调用wait),此时这个线程就不会在后续参与锁的竞争了。一直阻塞到 "取钱" 条件具备了,此时,再由运钞车通知机制 (notify) 唤醒1号的 wait,1号重新获得锁,这样就能进去取钱了。 


一. 监视器锁与监视器

监视器锁(Monitor Lock):监视器锁是一个较低层次的抽象,它仅仅是一个锁,用于控制对共享资源的访问。每个对象都可以作为监视器锁,线程通过获取这个锁来进入同步块或方法

监视器(Monitor):监视器是一个更高级的抽象概念,它不仅包括,还包括条件变量线程可以在这个变量上等待或被唤醒)和等待队列当线程调用wait时,它会被放入监视器的等待队列中)。

监视器锁只负责同步监视器不仅负责线程同步,还负责线程间的通信。线程可以通过调用wait(),  notify(),和notifyAll() 方法在监视器上进行协作。

二. wait ()  

wait() 方法是 0bject 类的一部分,用于线程同步。当一个线程执行到一个对象实例的 wait()方法时,该线程会暂停执行,并放弃监视器,也就是释放它所持有的锁,然后等待其他线程在相同的对象上调用 notify() 或 notifyAll () 方法来唤醒它。 

public class WaitDemo1 {
    //Java 多线程中涉及到的阻塞操作,很多都会抛出InterruptedException
    //如果另一个线程里调用Interrupt方法就能触发这个异常,表示等待被中断
    public static void main(String[] args) throws InterruptedException {
        Object obj = new Object();
        System.out.println("wait 之前");
        obj.wait();
        System.out.println("wait 之后");
    }
}

运行结果:

IllegalMonitorStateException:

这个异常表明一个线程试图执行一个操作,但是这个操作只有在当前线程持有某个对象的 监视器锁 时才是合法的,而实际上当前线程并没有持有这个锁。 

由于 wait() 必须要先释放锁,才能“阻塞等待”,因此,线程必须持有对象的监视器锁才能调用 wait()方法。

public class WaitDemo2 {
    public static void main(String[] args) throws InterruptedException {
        Object obj = new Object();
        System.out.println("wait 之前");
        //必须先持有锁
        synchronized (obj){
            obj.wait();
        }
        System.out.println("wait 之后");
    }
}

运行结果: 

通过jconsole我们发现,此时线程处在一个没有超时间等待的状态(WAITING)。

 

wait() 结束等待的条件: 

  • 其他线程调用该对象的 notify方法
  • wait等待时间超时——wait方法提供一个带有timeout参数的版本:void wait(long timeout),可指定等待时间
  • 其他线程调用该等待线程的 interrupt方法,导致 wait 抛出 InterruptedException 异常

在调用 wait() 的时候 :1)释放锁 2)执行等待(为了收到通知)--> 必须同时执行(打包成“原子”的)。


 * 如果不是“原子”的,会怎样呢? 如果“释放锁”和“执行等待”两个操作不是“原子”的,这两者之间,就可能发生线程切换。

   比如:当1号 释放锁 但还未执行等待,此处 运钱的线程 穿插进来了,执行完放钱操作,并会通知所有等待他的线程来取钱。由于1号还没有执行等待操作,因此上述运钱线程的通知,不会被1号感知到....

   此时,当1号切换回来时,继续执行等待.......由于错过了通知,这个等待就会持续下去,无法被及时唤醒了....


wait 使调用的线程进入阻塞,notify 则是通知 wait 的线程被唤醒(另一个线程调用的)。被唤醒的 wait 会重新竞争锁,并且在拿到锁之后,再继续执行。

因此,wait() 一共做了三件事:

  1. 释放锁 
  2. 进入阻塞状态,准备接收通知 
  3. 收到通知之后,被唤醒,并且尝试重新获取锁

wait() 使用步骤

  1. 获取监视器锁:线程必须持有对象的监视器锁才能调用wait()方法。
  2. 调用 wait() 方法:当线程执行到 wait() 方法时,会释放锁并执行等待操作
  3. 等待被唤醒:线程进入对象的等待队列等待被其他线程通过 notify() 或 notifyAll() 唤醒。
  4. 重新获取锁:一旦线程被唤醒,它必须重新获取监视器锁才能继续执行

三. notify () 和 notifyAll ()

3.1 notify()

 notify() 方法是Object类的一个方法,用于线程同步。用来唤醒正在等待这个对象的监视器的一个或多个线程。

public class WaitDemo2 {
    public static void main(String[] args) {
        Object locker = new Object();
        Thread t1 = new Thread(()->{
            System.out.println("wait 执行之前");
            synchronized (locker){
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("wait 执行之后");
        });
        Thread t2 = new Thread(()->{
            System.out.println("notify 执行之前");
            Scanner scanner = new Scanner(System.in);
            //t2 线程需要先阻塞一会,等待t1线程执行wait,否则t1线程接受不到通知
            scanner.next();
            //notify也必须加锁,否则也会抛出IllegalMonitorStateException
            //要加与wait相同的监视器锁,否则wait不会被通知到
            synchronized (locker){
                locker.notify();
            }
            System.out.println("notify 执行之后");
        });
        t1.start();
        t2.start();
    }
}

运行结果:

* 如果对方没有线程wait或者只有一个线程wait,但是另一个线程notify多次,会怎样呢?——不会怎样~~notify通知的时候,如果无人wait,不会有任何副作用!! 

要点:

  • notify() 也要在同步方法或同步块中调用(搭配synchronized使用,否则抛出IllegalMonitorStateException),该方法是用来通知那些等待该对象的 “对象锁” 的其它线程,对其发出通知notify,并使它们重新获取该对象的 “对象锁”
  • 如果有多个线程等待,则由线程调度器随机唤醒一个线程中的wait。(“随机调度,抢占式执行”)
  • 执行完 notify() 后,当前线程不会马上释放该对象锁,要等到执行 notify() 方法的线程将程序执行完,也就是退出同步代码块之后才会释放 “对象锁”
  • 必须确保 “对象锁” 是同一个才能正确执行
  • notify通知的时候,如果无人wait,不会有任何副作用
 3.2 notifyAll()

和notify相对的还有一个操作——notifyAll唤醒所有等待的线程

但是大部分的情况 使用唤醒一个的notify。因为一个一个唤醒(多执行几次notify)整个程序执行过程是比较有序的;如果一下唤醒所有,这些被唤醒的线程,就会无序地竞争锁。 

public class NotifyAllTask {
    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        Thread t1 = new Thread(()->{
            synchronized (locker){
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("t1 wait结束");
        });
        Thread t2 = new Thread(()->{
            synchronized (locker){
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("t2 wait结束");
        });
        Thread t3 = new Thread(()->{
            synchronized (locker){
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("t3 wait结束");
        });
        t1.start();
        t2.start();
        t3.start();
        // 等待t1、t2、t3都wait结束
        Thread.sleep(10);
        synchronized (locker){
            locker.notifyAll();
        }
    }
}

运行结果: 

3.3 wait等待 和 sleep休眠 的对比(面试题)

其实理论上,wait 和 sleep 完全是没有可比性的,因为一个是用于线程之间的通信,一个是让线程阻塞一段时间。

对比:

  • 定义:wait是Object的方法,sleep是Thread的静态方法
  • 锁的释放wait 释放锁,而 sleep 不释放锁
  • 调用位置:wait 必须在同步代码块中调用(搭配synchronized使用,否则抛出IllegalMonitorStateException ),sleep则可以在任何位置调用
  • 唤醒方式:wait需要其他线程显式地唤醒(调用notify),sleep则在指定时间后自动唤醒
  • 用途wait通常用于线程之间的通信,等待另一个线程的通知sleep通常用于模拟延时或减少CPU的使用率,阻塞一段时间

相同点:

  • 都可以让线程放弃执行一段时间
  • 都可以被 Interrupt 唤醒,但实际上是线程终止了
  • 11
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值