首页 > 编程语言 >JavaEE 初阶(9)——多线程7之 wait 和 notify

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

时间:2024-07-27 23:54:36浏览次数:18  
标签:初阶 等待 JavaEE 线程 notify 监视器 多线程 唤醒 wait

目录

一. 监视器锁与监视器

二. 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 唤醒,但实际上是线程终止了

标签:初阶,等待,JavaEE,线程,notify,监视器,多线程,唤醒,wait
From: https://blog.csdn.net/2301_80243321/article/details/140739103

相关文章

  • 《JavaEE》----1.<计算机是怎样工作的>
    前言:   大家好,我目前在学习java。我准备利用这个暑假,来复习之前学过的内容,并整理好之前写过的博客进行发布。如果博客中有错误或者没有读懂的地方。热烈欢迎大家在评论区进行讨论!!!   喜欢我文章的兄弟姐妹们可以点赞,收藏和评论。如果感觉有所收获可以关注我呦。......
  • 学习c语言第十五天(初阶测评)
    选择题1.下列程序输出结果为672.下列程序输出结果为 死循环打印3.i和j的值分别为什么 214.k的终值是什么905.输出结果是什么 16.正确的是    C7.C语言规定main函数位置    C8.不正确的是    D9.正确的是     c ......
  • C语言初阶(6)
    1.函数递归定义程序调用自身的编程技巧称为递归。递归做为一种算法在程序设计语言中广泛应用。一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可......
  • 【C++第九章】初阶模板
    C++模板初阶模板介绍......
  • 多线程实现阻塞队列
    今天面试被问到了,多线程实现阻塞队列,记录一下。1importjava.util.LinkedList;2importjava.util.Queue;3importjava.util.concurrent.locks.Condition;4importjava.util.concurrent.locks.ReentrantLock;56publicclassFixedSizeBlockingQueue<T>{7......
  • RT-Thread多线程
    RT-Thread启动流程分析线程的状态初始状态:线程刚开始创建,还没开始运行时就处于,初始状态。就绪状态:在就绪状态下,线程按照优先级排队,等待被执行。运行状态:线程正在运行,在单核系统中,只有rt_thread_self()函数返回的线程处于运行状态,但多核系统下,运行的线程不止一个。......
  • C++多线程基本使用方式
    一、线程创建        创建线程的函数  thread t(函数名f,函数f的参数) 或者 用lambda表达式代码:#include<iostream>#include<thread>#include<vector>usingnamespacestd;voidoutput(stringinput,inta){ cout<<input<<endl; cout<<a......
  • 【C++初阶】vector
    【C++初阶】vector......
  • 分布式集群与多线程高并发
     后台数据的处理语言有很多,Java是对前端采集的数据的一种比较常见的开发语言。互联网移动客户端的用户量特别大,大量的数据处理需求应运而生。可移动嵌入式设备的表现形式很多,如PC端,手机移动端,智能手表,Google眼镜等。Server2client的互联网开发模式比较常见,有一种新的数......
  • java多线程把数据迁移到不同数据库中
    publicvoidsync_table_test_thread()throwsSQLException,InterruptedException{    longstart=System.currentTimeMillis();    SimpleDateFormatformat=newSimpleDateFormat("yyyy-MM-ddHH:mm:ss");    //获取要迁移oracle表数据库......