首页 > 编程语言 >Java线程:wait()和notify()

Java线程:wait()和notify()

时间:2023-04-01 18:11:41浏览次数:47  
标签:Java Thread 线程 notify 执行 方法 wait

一、wait()和notify()含义

二、标准代码示例

创建两个线程Thread0和Thread1。

代码实现:

运行流程详解

三、什么时候释放锁—wait()、notify()

四、用生活故事讲懂线程的等待唤醒

1.老王和老李(专家程序员):

2.王哥和李哥(普通程序员):

3.小王和小李(新手程序员):

五、问题理解

1、执行wait()的线程,如果重新被唤醒,是从wait()代码之后继续执行的,而不是重新从该方法的头部重新执行。

2、那如果唤醒后的两个线程继续执行while循环,在某个时刻同时判断为数组没满,那不也会抛出越界异常吗?

3、Java多线程为什么使用while循环来调用wait方法

六、阿里巴巴面试题: 为什么wait()和notify()需要搭配synchonized关键字使用

 

 

简介

本文讲解Java中wait()、notify(),通过一个标准的使用实例,来讨论下这两个方法的作用和使用时注意点,这两个方法被提取到顶级父类Object对象中,地位等同于toString()方法。

一、wait()和notify()含义

  • wait()方法是让当前线程等待的,即让线程释放了对共享对象的锁,不再继续向下执行。
  • wait(long timeout)方法可以指定一个超时时间,过了这个时间如果没有被notify()唤醒,则函数还是会返回。如果传递一个负数timeout会抛出IllegalArgumentException异常。
  • notify()方法会让调用了wait()系列方法的一个线程释放锁,并通知其它正在等待(调用了wait()方法)的线程得到锁。
  • notifyAll()方法会唤醒所有在共享变量上由于调用wait系列方法而被挂起的线程。

注意:

  • 调用wait()、notify()方法时,当前线程必须要成功获得锁(必须写在同步代码块锁中),否则将抛出异常。
  • 只对当前单个共享变量生效,多个共享变量需要多次调用wait()方法。
  • 如果线程A调用wait()方法后处于堵塞状态时,其他线程中断(在其他线程调用A.interrupt()方法)A线程,则会抛出InterruptExcption异常而返回并终止。

 

二、标准代码示例

创建两个线程Thread0和Thread1。
让Thread0执行wait()方法。
此时Thread1得到锁,再让Thread1执行notify()方法释放锁。
此时Thread0得到锁,Thread0会自动从wait()方法之后的代码,继续执行。
通过上述流程,我们就可以清楚的看到,wait()和notify()各自是怎么工作的了,也可以知道两者是怎么配合的了。

public class ThreadWaitAndNotify {
    // 创建一个将被两个线程同时访问的共享对象
    public static Object object = new Object();
 
    // Thread0线程,执行wait()方法
    static class Thread0 extends Thread {
 
        @Override
        public void run() {
            synchronized (object) {
                System.out.println(Thread.currentThread().getName() + "初次获得对象锁,执行中,调用共享对象的wait()方法...");
                try {
                    // 共享对象wait方法,会让线程释放锁。
                    object.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "再次获得对象锁,执行结束");
            }
        }
 
    }
 
    // Thread1线程,执行notify()方法
    static class Thread1 extends Thread {
 
        @Override
        public void run() {
            synchronized (object) {
                // 线程共享对象,通过notify()方法,释放锁并通知其他线程可以得到锁
                object.notify();
                System.out.println(Thread.currentThread().getName() + "获得对象锁,执行中,调用了共享对象的notify()方法");
            }
        }
    }
 
    // 主线程
    public static void main(String[] args) {
        Thread0 thread0 = new Thread0();
        Thread1 thread1 = new Thread1();
        thread0.start();
        try {
            // 保证线程Thread0中的wait()方法优先执行,再执线程Thread1的notify()方法
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread1.start();
    }
 
运行结果
Thread-0初次获得对象锁,执行中,调用共享对象的wait()方法...
Thread-1获得对象锁,执行中,调用了共享对象的notify()方法
Thread-0再次获得对象锁,执行结束

  

运行流程详解
从执行的结果中,要明白线程的执行顺序:

  • Thread0调用了wait()方法后,会释放掉对象锁并暂停执行后续代码,即从wait()方法之后到run()方法结束的代码,都将立即暂停执行,这就是wait()方法在线程中的作用。
  • CPU会将对象锁分配给一直等候的Thread1线程,Thread1执行了notify()方法后,会通知其他正在等待线程(Thread0)得到锁,但会继续执行完自己锁内的代码之后,才会交出锁的控制权。
  • 因为本例只有两个线程,所以系统会在Thread1交出对象锁控制权后(Synchronized代码块中代码全部执行完后),把锁的控制权给Thread0(若还有其他线程,谁得到锁是随机的,完全看CPU心情),Thread0会接着wait()之后的代码,继续执行到Synchronized代码块结束,将对象锁的控制权交还给CPU。

三、什么时候释放锁—wait()、notify()

由于等待一个锁定线程只有在获得这把锁之后,才能恢复运行,所以让持有锁的线程在不需要锁的时候及时释放锁是很重要的。在以下情况下,持有锁的线程会释放锁:

执行完同步代码块。
在执行同步代码块的过程中,遇到异常而导致线程终止。
在执行同步代码块的过程中,执行了锁所属对象的wait()方法,这个线程会释放锁,进行对象的等待池。
除了以上情况外,只要持有锁的此案吃还没有执行完同步代码块,就不会释放锁。因此在以下情况下,线程不会释放锁:

在执行同步代码块的过程中,执行了Thread.sleep()方法,当前线程放弃CPU,开始睡眠,在睡眠中不会释放锁。
在执行同步代码块的过程中,执行了Thread.yield()方法,当前线程放弃CPU,但不会释放锁。
在执行同步代码块的过程中,其他线程执行了当前对象的suspend()方法,当前线程被暂停,但不会释放锁。但Thread类的suspend()方法已经被废弃。
避免死锁的一个通用的经验法则是:当几个线程都要访问共享资源A、B和C时,保证使每个线程都按照同样的顺序去访问他们,比如都先访问A,再访问B和C。
java.lang.Object类中提供了两个用于线程通信的方法:wait()和notify()。需要注意到是,wait()方法必须放在一个循环中,因为在多线程环境中,共享对象的状态随时可能改变。当一个在对象等待池中的线程被唤醒后,并不一定立即恢复运行,等到这个线程获得了锁及CPU才能继续运行,又可能此时对象的状态已经发生了变化。
# 调用obj的wait(), notify()方法前,必须获得obj锁,也就是必须写在synchronized(obj) {...} 代码段内。
# 调用obj.wait()后,线程A就释放了obj的锁,否则线程B无法获得obj锁,也就无法在synchronized(obj) {...} 代码段内唤醒A。
# 当obj.wait()方法返回后,线程A需要再次获得obj锁,才能继续执行。
# 如果A1,A2,A3都在obj.wait(),则B调用obj.notify()只能唤醒A1,A2,A3中的一个(具体哪一个由JVM决定)。
# obj.notifyAll()则能全部唤醒A1,A2,A3,但是要继续执行obj.wait()的下一条语句,必须获得obj锁,因此,A1,A2,A3只有一个有机会获得锁继续执行,例如A1,其余的需要等待A1释放obj锁之后才能继续执行。# 当B调用obj.notify/notifyAll的时候,B正持有obj锁,因此,A1,A2,A3虽被唤醒,但是仍无法获得obj锁。直到B退出synchronized块,释放obj锁后,A1,A2,A3中的一个才有机会获得锁继续执行。  

 

wait()/sleep()的区别

 

  前面讲了wait/notify机制,Thread还有一个sleep()静态方法,它也能使线程暂停一段时间。sleep与wait的不同点是:sleep并不释放锁,并且sleep的暂停和wait暂停是不一样的。obj.wait会使线程进入obj对象的等待集合中并等待唤醒。
  但是wait()和sleep()都可以通过interrupt()方法打断线程的暂停状态,从而使线程立刻抛出InterruptedException。
  如果线程A希望立即结束线程B,则可以对线程B对应的Thread实例调用interrupt方法。如果此刻线程B正在wait/sleep/join,则线程B会立刻抛出InterruptedException,在catch() {} 中直接return即可安全地结束线程。
  需要注意的是,InterruptedException是线程自己从内部抛出的,并不是interrupt()方法抛出的。对某一线程调用interrupt()时,如果该线程正在执行普通的代码,那么该线程根本就不会抛出InterruptedException。但是,一旦该线程进入到wait()/sleep()/join()后,就会立刻抛出InterruptedException。

四、用生活故事讲懂线程的等待唤醒

Java线程的等待唤醒机制,是通过wait()方法和notify()方法实现的,为了更好的理解,我再来举一个通俗易懂接地气的例子吧,帮不懂代码的人也能明白这两个方法的作用。

例:捡肥皂的故事
假设有两个程序员去洗澡,只带了一块肥皂,两个人怎么使用一块肥皂洗澡呢?会发生3个场景:

1.老王和老李(专家程序员):
老王和老李随机一人拿到肥皂,比如老王先拿到肥皂,然后使用肥皂,然后把肥皂让出去,自己等会再用。老李拿到了肥皂,然后使用了一会,再通知老王说:“自己不用了”,老王听到话以后,捡起肥皂从上次用的地方接着用。二者洗澡,你来我往共享一块肥皂,非常和谐。

程序语言描述:
老王随机先得到锁,然后用了一会后,调用了wait()方法,把锁交了出去,自己等待。老李拿到锁,使用后,再通过notify()通知老王,然后等老李用完以后,老王再次拿到锁,继续执行…这种方式是线程安全的,而且还能合理的分配资源的使用,这就是等待唤醒的好处。

2.王哥和李哥(普通程序员):
王哥和李哥随机一人拿到肥皂,比如王哥先拿到,然后王哥就一直霸占着,直到自己洗完了,才把肥皂给李哥。期间李哥洗澡只能干搓,根本没机会接触肥皂。我想李哥肯定觉得王哥很自私,不懂得礼让,李哥的体验不是很好。

程序语言描述:王哥和李哥就是两个线程,王哥在拿到锁以后,就一直使用,直到同步代码块中的内容完全执行完成。再把锁交给李哥使用。这种方式每次都是一个线程执行完,另一个才会执行,是线程安全的。

3.小王和小李(新手程序员):
小王和小李一开始洗澡就争抢肥皂,当肥皂在小王手上时,小王还在使用中,小李就扑上来了,于是出现了两人一起摩擦一块肥皂的场景!这种画面既不优雅,也不安全。

程序语言描述:
如果两个线程,访问同一个资源的时候,不对其进行加锁控制,就会出现混乱的场景,这就是线程不安全。两个线程可能会同时操作同一共享变量,从而使这个共享变量失控,最终结果紊乱。

五、问题理解

1、执行wait()的线程,如果重新被唤醒,是从wait()代码之后继续执行的,而不是重新从该方法的头部重新执行。
假如有两个线程,向一个数组中插入数据,插入之前先判断数组大小,如果大于等于数组长度,那么wait(),否则会向数组中插入一条数据。还有其他线程是取数据的。

插入数据方法伪代码如下

synchronized void push(int number) {

if (数组满了) {

this.wait();

}

array[++index] = xxx;

}

插入线程1插入直到数组满了,执行wait()方法,然后释放锁,插入线程2判断也满了,然后也执行wait()方法,然后释放锁,这时候假如唤醒的是线程1,那么他就会执行array[++index]==xxx;会数组越界

如果改为

while (数组满了) {
this.wait();
}

就会重新判断一次,如果满了重新wait(),就不会发生异常了

public class ThreadTest implements Runnable{
 
    private static String[] array = new String[1];
    private static int index = 0;
 
    synchronized void push(String number) {
            try {
                System.out.println(Thread.currentThread().getName() + "执行一次");
                while (array[array.length-1] != null) {
                    if(Thread.currentThread().getName().equals("t2")){
                        this.notify();
                        System.out.println(Thread.currentThread().getName() + "执行");
                    }
                    this.wait();
                    System.out.println(Thread.currentThread().getName() + "继续执行");
                }
                array[++index] = number;
                System.out.println(array[0]);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        System.out.println(Thread.currentThread().getName()+"执行完成");
    }
 
    @Override
    public void run() {
        push("a");
    }
 
    public static void main(String[] args) {
        array[0] = "s";
        ThreadTest threadTest = new ThreadTest();
        Thread  t1 = new Thread(threadTest,"t1");
        Thread  t2 = new Thread(threadTest,"t2");
        t1.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t2.start();
 
    }
}
while结果
t1执行一次
t2执行一次
t2执行
t1继续执行
 
if结果
t1执行一次
t2执行一次
t2执行
t1继续执行
Exception in thread "t1" java.lang.ArrayIndexOutOfBoundsException: 1
	at com.crane.ThreadTest.push(ThreadTest.java:27)
	at com.crane.ThreadTest.run(ThreadTest.java:37)
	at java.lang.Thread.run(Thread.java:748)

  

2、那如果唤醒后的两个线程继续执行while循环,在某个时刻同时判断为数组没满,那不也会抛出越界异常吗?
唤醒只是让线程可以去竞争锁了,同步的代码块一个时刻只会有一个线程执行

3、Java多线程为什么使用while循环来调用wait方法
这里的while相当于多次if,这么就好理解一点,防止线程醒来不进行判断,假如你用if,就只判断一次,线程醒来会继续向前走,如果用了while线程醒来会在判断一次条件,符合在走,不符合接着睡!

 

六、阿里巴巴面试题: 为什么wait()和notify()需要搭配synchonized关键字使用

阿里巴巴面试题: 为什么wait()和notify()需要搭配synchonized关键字使用_萧萧的专栏-CSDN博客

参考:Java多线程wait()和notify()系列方法使用教程(内涵故事)_五道口-CSDN博客

https://www.cnblogs.com/chenjfblog/p/7868875.html


标签:Java,Thread,线程,notify,执行,方法,wait
From: https://www.cnblogs.com/shoshana-kong/p/17279028.html

相关文章

  • 别逛了,送你一份2023年Java核心篇JVM(虚拟机)面试题整理
    Java内存区域说一下JVM的主要组成部分及其作用?JVM包含两个子系统和两个组件,两个子系统为Classloader(类装载)、Executionengine(执行引擎);两个组件为Runtimedataarea(运行时数据区)、NativeInterface(本地接口)。●Classloader(类装载):根据给定的全限定名类名(如:java.......
  • 多线程
    内容什么是线程如何创建线程线程的调度线程的一个设计模式:生产消费者模型线程池线程集合对象(侧重点)一、什么是线程进程:运行中的程序才可以称为进程,一个程序一个进程。宏观并行,微观串行。线程:1.任何一个程序都至少拥有一个线程,即主线程。但是java程序默认有两个线......
  • C# 直接在子线程中对窗体上的控件操作是会出现异常
    https://www.bbsmax.com/A/MAzA8klpd9/ Form1里privatedelegatevoidDispMSGDelegate(intindex,stringMSG);publicvoidDispMsg(intiIndex,stringstrMsg){if(this.richTextBox1.InvokeRequired==false)......
  • 线程停止
    线程停止1.建议线程正常停止--->利用次数,不建议死循环2.建议使用标志位--->设置一个标志位3.不要使用stop或者destroy等过时或者JDK不建议使用的方法//测试stop//1.建议线程正常停止--->利用次数,不建议死循环//2.建议使用标志位--->设置一个标志位//3.不要使用stop或者dest......
  • 线程休眠
    模拟网络延时放大问题的发生性//模拟网络延时:放大问题的发生性publicclassTestSleepimplementsRunnable{//票数privateintticketNums=10;@Overridepublicvoidrun(){while(true){if(ticketNums<=0){......
  • 线程
    目录线程线程概念的引入背景进程有了进程为什么要有线程进程和线程创建线程创建线程的两种方式参数和方法Thread类中的几个方法如何开启多线程进程和线程的比较1.pid不同2.开启效率的不同3.内存数据的共享不同多线程实现socket守护线程GIL锁(全局解释器锁)验证GIL的存在GIL与普通......
  • JS基础《JavaScript精粹》笔记摘录
    基础概念和语法基本类型、null、undefined、NaNJavaScript定义了一小批基本类型(primitivetype),它们包括字符串类型(string,单引号或双引号包起来)、数值类型(number,整数和小数都用这个类型)和布尔类型(boolean,值仅有true和false)。JavaScript使用两个特殊的值来表示不存在有意义的值—......
  • 红黑树及JAVA实现
     红黑树在Java中的应用红黑树在Java中有很多应用。例如,Java8中的HashMap容器和TreeMap容器都有红黑树的具体应用。HashMap在插入和查找时都需要对键进行哈希,而TreeMap则是按照键的自然顺序进行排序。因此,当需要对键进行排序时,可以使用TreeMap;当不需要排序时,可以使用HashMap......
  • JAVA-方法
    1.1方法的定义[修饰符列表] 返回值类型方法名(第一个首字母小写,后边单词大写)(形参列表){方法体};ps:方法遵循自上而下运行1.2方法调用类名.方法名(实参列表)方法调用时,压栈!结束时弹栈!先进后出!   1.2方法重载1.2.1定义......
  • JAVASE:注解与反射笔记
    JavaSE:注解与反射(Annotation&Reflection)​注解和框架是所有框架的底层,如Mybatis,spring。框架的底层实现机制就是注解和反射。注解相比于注释,除了能较为直接的表示出这部分模块的功能,也能实现一定的具体功能。01初识注解1.1什么是注解Annotation是从JDK5.0引入......