【并发基础】线程的通知与等待:obj.wait()、obj.notify()、obj.notifyAll()详解

目录

〇、先总结一下这三个方法带来的Java线程状态变化

一、obj.wait()

1.1 作用

1.2 wait()方法到底会让哪个线程阻塞?

1.3 wait(long timeout)方法如何实现自动唤醒

1.3.1 同步队列和等待队列

1.4 使用前需要持有线程共享对象的锁

1.5 使用技巧

二、obj.notify(All)()

2.1 notify() 方法

2.1.1 调用notify()或notifyAll()不会释放线程的锁

2.2 notifyAll() 方法

2.3 使用技巧

三、使用实例

四、wait()/notify()/notifyAll() 为什么定义在 Object 类中


wait()、notify/notifyAll() 方法都是Object的本地final方法,无法被重写。这些方法都是native方法。

〇、先总结一下这三个方法带来的Java线程状态变化

当Java线程调用wait()方法后,该线程会进入等待队列,并且会释放占用的锁资源。线程状态会变为WAITING或TIMED_WAITING。该线程不会被挂起到外存,而是在内存中等待被唤醒。线程等待的条件通常是由其他线程调用notify()或notifyAll()方法来唤醒该线程。

当线程被唤醒时,它会重新尝试获取锁资源并从wait()方法返回。线程状态会变为BLOCKED,直到它获得了锁资源为止。如果成功获取锁资源,线程状态会变为RUNNABLE,然后可以继续执行。如果获取锁资源失败,则线程会继续等待,并且状态会维持在BLOCKED或WAITING或TIMED_WAITING状态,直到它再次被唤醒。

需要注意的是,线程在等待期间会消耗一定的资源,因此应该避免过多的线程等待。另外,线程在等待期间不会占用CPU时间片,因此可以减少CPU的利用率,提高系统的性能。

一、obj.wait()

1.1 作用

wait()是Object里面的方法,Object是所有对象的父类,即所有对象都可以调用wait()方法。wait方法还有可以传入等待时长的,可以让线程等待指定的时间后自动被唤醒。调用wait()会使Java线程进入到WAITING状态,调用wait(long time)会使Java线程进入到TIMED_WAITING状态(WAITING和TIMED_WAITING状态就是阻塞状态)

当一个线程调用一个共享变量的wait()方法时,该线程会阻塞(等待)。直到发生以下几种情况才会恢复执行:

  • 其他线程调用了该共享对象的 notify() 方法或者 notifyAll() 方法(继续往下走)
  • 其他线程调用了该线程的 interrupt() 方法,该线程会 InterruptedException 异常返回

等待线程:假设调用的是obj对象的wait()方法,wait的执行线程,也就是被暂停的线程,就称为对象obj上的等待线程。对象的wait方法可能被不同的线程执行,所以同一个对象可能会有多个等待线程。

1.2 wait()方法到底会让哪个线程阻塞?

当一个线程调用某个对象的wait()方法时,这个线程会进入该对象的等待队列,等待被其他线程唤醒。

具体来说,当一个线程调用对象的wait()方法时,该线程会释放该对象的锁并进入等待状态(WAITING/TIMED_WAITING),直到其他线程调用该对象的notify()或notifyAll()方法唤醒。在等待过程中,该线程不会占用 CPU 资源,它会被阻塞在内存中,直到被唤醒后才会有机会重新进入运行状态。

因此,调用了wait()方法的线程会被阻塞,并释放该对象的锁。被唤醒后,它会重新尝试获取该对象的锁并从wait()方法返回,进入BLOCKED状态,直到它获得了锁资源为止,当他重新获取到之前调用对象的锁资源后,就可以继续进入到运行状态。我们就可以理解为在那个线程环境中执行了某个对象的wait()方法,那么这个线程环境就会被阻塞,进入到等待状态,也就是调用了wait()方法时所在的当前线程会被阻塞。

需要注意的是,被阻塞的是当前线程,而不是该对象。当调用了wait()方法后,当前线程会进入等待状态,而该对象依然存在,并不会被阻塞。其他线程仍然可以访问该对象,只是在当前线程持有该对象的锁时,其他线程不能获取该对象的锁。之所以要强调这一点,是因为有的时候可能当前线程在调用某个对象的wait()方法时,这个对象是另一个线程的对象,也就是在线程A中调用线程B对象的wait()方法,这种情况我们一定不要搞混乱,一定要直到被阻塞的是线程A,并不是线程B,线程B不会受到影响

1.3 wait(long timeout)方法如何实现自动唤醒

当一个线程内调用某个对象的wait(long timeout)方法时,它将被阻塞,并释放该对象的锁。底层实现中,系统会在等待一定时间后自动唤醒该线程,使其重新进入对象的同步队列中。当该线程被唤醒时,它会重新尝试获取对象的锁,成功获取到锁后就会继续执行。

具体来说,当线程调用wait(long timeout)方法时,系统会记录下当前时间(假设为t1),并将线程加入到对象的等待队列中。接下来,系统会使该线程进入阻塞状态,直到下面任意一个条件发生:

  • 其他线程调用了该对象的notify()或notifyAll()方法,并且该线程被唤醒。
  • 指定的等待时间已经过去,并且该线程被唤醒。

如果是第一种情况:该线程被唤醒后会尝试重新获取该对象的锁,如果获取成功,则继续执行。如果获取不成功,该线程将重新进入等待队列中,等待下一次被唤醒。

如果是第二种情况:系统会在指定的等待时间(timeout)到达时,自动唤醒该线程,将其移除等待队列,然后使其重新进入对象的同步队列中去尝试获取该对象的锁。

需要注意的是,在等待时间到达之前,如果其他线程调用了该对象的notify()或notifyAll()方法,则该线程会被唤醒并尝试重新获取锁,而不必等待指定的等待时间过去。

总之,调用wait(long timeout)方法时,底层系统会自动实现等待指定时间后自动唤醒该线程的功能,而无需用户手动干预。自动唤醒的操作是在底层JVM中实现的,并没有显式地调用notify()或notifyAll()方法。wait方法是一个本地的native方法。

wait()无参方法其实底层还是调用的wait(long timeout)有参方法,只不过传入的是0,只要传入的是0,就是没有等待时常,只要没有其他线程显式调用notify()或notifyAll()方法来唤醒该线程的话,该线程就会一直在等待(阻塞)状态。

1.3.1 同步队列和等待队列

在上面的论述中,提到了同步队列和等待队列,这里单独讲一下它俩避免搞混乱。

等待队列和同步队列是同一个概念,都是指的同步器(锁)的内部队列。当线程调用了wait(long timeout)方法后,会将当前线程加入到同步器的等待队列中。如果等待时间超过了设定的timeout,系统会自动将该线程从等待队列中移除,然后重新加入到同步队列中等待获取锁。

等待队列和同步队列是同一个队列,只是同步队列是等待获取锁的线程所在的队列,而等待队列是等待被唤醒的线程所在的队列

这里要注意等待队列和同步队列虽然都是指的同一个队列,但是它们是在不同的状态下使用的,具有不同的作用。

  • 等待队列是指等待被唤醒的线程所在的队列,它的作用是存放调用了Object.wait()方法后被阻塞的线程。当一个线程调用了wait方法后,它就会被放入到等待队列中,等待被唤醒。
  • 同步队列是指等待获取锁的线程所在的队列,它的作用是存放那些正在等待获取锁的线程。当一个线程试图获取一个对象的锁时,如果锁已经被其他线程占用,那么它就会被放入同步队列中,等待获取锁。

因此,在调用wait(long timeout)方法时,如果等待的时间超过了timeout设定的时间,系统会自动将该线程从等待队列中移除,然后重新加入到同步队列中等待获取锁。这里的移除和重新加入都是在同一个队列中完成的,也就是同步队列。

可能到了这里你就会迷惑了,怎么两个作用不同的队列是同一个队列呢,这样不同的线程混在同一个队列中岂不都乱了。

实际上,等待被唤醒的线程和等待获取锁的线程是分别存放在同一个队列的不同部分中的,而不是混合在一起的在 Java 中,同步队列的实现通常是一个双向链表,链表中的每个节点都可以代表一个线程或者一个等待状态当一个线程等待获取对象的锁时,它会被加入到队列的尾部,同时释放持有的锁;当一个线程调用 wait() 方法进入等待状态时,它会被加入到队列的头部

因此,当一个线程调用了 wait() 方法后,它会被放到队列的头部等待被唤醒,而等待获取锁的线程则在队列的尾部等待。这样就保证了等待被唤醒的线程和等待获取锁的线程的位置是不会混淆的,不会出现混乱的情况。

1.4 使用前需要持有线程共享对象的锁

在使用wait()、notify()和notifyAll()方法方法前,需要先持有锁。如果调用线程共享对象的wait()、notify()和notifyAll()方法的线程没有事先获取该对象的监视器锁,调用线程会抛出IllegalMonitorStateException 异常。当线程调用wait() 之后,就会释放该对象的监视器锁

使用wait()notify()notifyAll()方法方法前,需要先持有锁:

  • 表象:wait、notify(ALL)方法需要调用 monitor 对象
  • 本质:Java的线程通信实质上是共享内存,而不是直接通信

那么,一个线程如何才能获取一个共享变量的监视器锁?

1、执行synchronized 同步代码块,使用该共享变量作为参数。

synchronized(共享变量) {
    // TODO
}

2、调用该共享变量的同步方法(synchronized 修饰)

synchronized void sum(int a, int b) {
    // TODO
}

 如下代码示例,线程A与线程B,在线程A中调用共享变量obj的wait()方法,在线程B中进行唤醒notify()。

/**
 * Object的Wati()方法的使用
 */
@Slf4j
public class WaitTest {
 
    public static void main(String[] args) {
 
        // 定义一个共享变量
        Object obj = new Object();
 
        // 创建线程A
        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                log.info("线程" + Thread.currentThread().getName()+"开始执行");
                try {
                    // 获取共享变量的对象锁
                    synchronized(obj){
                        // 线程A 等待
                        log.info("线程" + Thread.currentThread().getName()+"等待");
                        // 调用wait(),线程A阻塞,并且释放掉获取到的obj的对象锁
                        obj.wait();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
 
                log.info("线程" + Thread.currentThread().getName()+"执行结束");
            }
        },"A");
 
 
        // 创建线程B
        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                log.info("线程" + Thread.currentThread().getName()+"开始执行");
                // 获取共享变量锁
                synchronized (obj){
                    //  线程B 唤醒或者中断  调用obj的唤醒操作或者使A线程中断的操作都可以将正在阻塞的A线程唤醒
                    log.info("线程" + Thread.currentThread().getName()+"唤醒");
                    obj.notify(); // 唤醒操作
                    // threadA.interrupted(); // 中断操作
                }
                log.info("线程" + Thread.currentThread().getName()+"执行结束");
            }
        },"B");
 
 
        // 启动线程A
        threadA.start();
        try {
            // 等待200ms,让线程B获取资源,在这200ms期间A就被阻塞了,释放了obj对象锁
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
 
        // 启动线程B
        threadB.start();
    }
}

执行结果:

线程A开始执行

线程B开始执行

线程B执行结束

线程A执行结束

可以看到主程序线程A启动之后,休眠了200ms让出cup执行权,线程B开始执行后调用notify()方法对阻塞线程A进行唤醒。

故:当一个线程调用一个共享变量的wait()方法时,该调用线程会被阻塞挂起,直到发生下面几件事情之一才返回:(1)其他线程调用了该共享对象的notify()或者notifyAll()方法;(2)其他线程调用了该线程的interrupt()方法,该线程抛出InterruptedException异常返回。

1.5 使用技巧

  • wait()方法一般配合while使用
    • 被唤醒后会重新竞争锁,之后从上次wait位置重新运行
    • while 多次判断,防止在wait这段时间内对象被修改

二、obj.notify(All)()

notify()和notifyAll()方法也是Object里面的方法,Object是所有对象的父类,即所有对象都可以调用notify()和notifyAll()方法。但是线程中共享变量在调用这两个方法前,该线程需要获取到这个共享变量的锁才可以,否则会抛出异常。

2.1 notify() 方法

一个线程调用共享对象的 notify() 方法后,会唤醒一个在该共享变量上调用 wait(...) 系列方法后阻塞的线程。

通知线程:调用notify/notifyAll方法时所在的线程叫做通知线程。

2.1.1 调用notify()notifyAll()不会释放线程的锁

当线程调用notify()或notifyAll()方法时,它不会释放掉线程持有的锁。

在Java中,每个对象都有一个相关联的锁,也称为监视器锁。当一个线程需要访问被该锁保护的对象时,它必须先获得该锁的所有权。所以只有获得锁的线程才能调用wait()、notify()和notifyAll()方法。

当线程调用notify()或notifyAll()方法时,它仅仅是唤醒等待在该对象上的一个或多个线程,以便它们可以继续执行。它不会释放线程持有的锁。因此,其他线程仍然无法访问被该锁保护的对象,直到调用notify()或notifyAll()方法的线程释放锁资源。

在多线程编程中,必须小心地管理锁,以避免死锁和竞争条件等问题。通常,为了确保线程安全和避免死锁,必须确保在访问共享资源时只有一个线程持有锁。当然,这也需要合理地使用wait()、notify()和notifyAll()方法来协调线程的执行顺序。

值得注意的是:

  • 一个共享变量上可能会有多个线程在等待,notify()具体唤醒哪个等待的线程是随机的
  • 被唤醒的线程不能马上从wait()方法返回并继续执行,它必须在获取了共享对象的监视器锁后才可以返回,等到唤醒它的线程释放了共享变量上的监视器锁后,被唤醒的线程也不一定会获取到共享对象的监视器锁,这是因为该线程还需要和其他线程一起竞争该锁,只有该线程竞争到了共享变量的监视器锁后才可以继续执行

2.2 notifyAll() 方法

notifyAll() 方法则会唤醒所有在该共享变量上由于调用wait系列方法而被挂起的线程。

2.3 使用技巧

尽量让notify / notifyAll()靠近临界区结束的地方。免得等待线程因为没有获得对象的锁,而又进入等待状态。

三、使用实例

比较经典的就是生产者和消费者的例子。

在生产者消费者模型中,推荐使用notifyAll,因为notify唤醒的线程不确定是生产者或消费者。

public class NotifyWaitDemo {
       // 共享变量队列的最大容量
    public static final int MAX_SIZE = 1024;
    // 共享变量
    public static Queue queue = new Queue();

    public static void main(String[] args) {
        // 生产者
        Thread producer = new Thread(() -> {
            // 获取共享变量的锁才能调用wait()方法
            synchronized (queue) {
                // 一般wait()都配合着while使用,因为线程唤醒后需要不断地轮循来尝试获取锁
                while (true) {
                    // 当队列满了之后就挂起当前线程(生产者线程)
                    // 并且,释放通过queue的监视器锁,让消费者对象获取到锁,执行消费逻辑
                    if (queue.size() == MAX_SIZE) {
                        try {
                            // 阻塞生产者线程,并且使当前线程释放掉共享变量的锁
                            queue.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    // 空闲则生成元素,并且通知消费线程
                    queue.add();
                    // 唤醒消费者来消费,建议用notifyAll(),因为notify()无法确定会唤醒哪一个线程
                    queue.notifyAll();
                }
            }
        });

        // 消费者
        Thread consumer = new Thread(() -> {
            // 需要先获取锁
            synchronized (queue) {
                while (true) {
                    // 当队列已经空了之后就挂起当前线程(消费者线程)
                    // 并且,释放通过queue的监视器锁,让生产者对象获取到锁,执行生产逻辑
                    if (queue.size() == 0) {
                        try {
                            // 阻塞消费者线程,并释放共享对象的锁
                            queue.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    // 空闲则消费元素,并且通知生产线程
                    queue.take();
                    queue.notifyAll();
                }
            }
        });


        // 先执行生产者线程
        producer.start();
        try {
            // 将当前线程睡眠1000ms,让生产者先将队列生产满,然后wait阻塞起来,并且释放持有的锁。为了后续能执行消费者线程
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 执行消费者线程
        consumer.start();
    }


    // 共享变量
    static class Queue {
        private int size = 0;
        public int size() {
            return this.size;
        }
        // 生产操作
        public void add() {
            // TODO
            size++;
            System.out.println("执行add 操作,current size: " +  size);
        }
        // 消费操作
        public void take() {
            // TODO
            size--;
            System.out.println("执行take 操作,current size: " +  size);
        }
    }
}

 

四、wait()/notify()/notifyAll() 为什么定义在 Object 类中?

调用这些方法来等待和唤醒时必须持有同一个锁,而锁可以是任意对象的锁(即也有可能是Thread线程对象的锁),所以这三个方法有可能被任何一个对象调用,而Object类是所有Java对象的顶级父类,既然有了任意对象都可能调用这三个方法的需求,所以可以被任意对象调用的方法通常是定义在Object类中。

Thread类继承了Object类,所以Thread也可以调用者三个方法。


 相关文章:【并发基础】一篇文章带你彻底搞懂睡眠、阻塞、挂起、终止之间的区别
                  【并发基础】Java中线程的创建和运行以及相关源码分析           
                  【并发基础】线程,进程,协程的详细解释
                 【并发基础】操作系统中线程/进程的生命周期与状态流转以及Java线程的状态流转详解

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值