Java线程:wait()和notify()

发布时间 2023-04-01 18:03:46作者: 蒹葭苍苍呀

一、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