前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >彻底搞懂Java的等待-通知(wait-notify)机制

彻底搞懂Java的等待-通知(wait-notify)机制

作者头像
全菜工程师小辉
发布于 2019-08-16 02:33:50
发布于 2019-08-16 02:33:50
10.9K10
代码可运行
举报
运行总次数:0
代码可运行

线程的生命周期转换

  1. 新建状态(New):新建一个线程对象。
  2. 就绪/可运行状态(Runnable):线程对象创建后,其他线程调用了该对象的start方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
  3. 运行状态(Running):就绪状态的线程获得CPU并执行程序代码。
  4. 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
    1. 等待阻塞:运行的线程执行wait方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
    2. 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
    3. 其他阻塞:运行的线程执行sleep或join方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep的状态超时、join等待线程终止或者超时、以及I/O处理完毕时,线程重新转入就绪状态。
  5. 死亡状态(Dead):线程执行完成或者因异常退出run方法,该线程结束生命周期。

wait()与notify()

  • wait():使调用该方法的线程释放共享资源锁,然后从运行状态退出,进入等待队列,直到被再次唤醒。
  • wait(long):超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回。
  • wait(long,int):对于超时时间更细力度的控制,单位为纳秒。
  • notify():随机唤醒等待队列中等待同一共享资源的一个线程,并使该线程退出等待队列,进入可运行状态,也就是notify()方法仅通知一个线程。
  • notifyAll():使所有正在等待队列中等待同一共享资源的全部线程退出等待队列,进入可运行状态。此时,优先级最高的那个线程最先执行,但也有可能是随机执行,这取决于JVM虚拟机的实现。

什么是等待/通知机制

通俗来讲:

等待/通知机制在我们生活中很常见,一个形象的例子就是厨师和服务员之间就存在等待/通知机制。

  • 厨师做完一道菜的时间是不确定的,所以菜到服务员手中的时间也是不确定的。
  • 服务员就需要去“等待(wait)”。
  • 厨师把菜做完之后,按一下铃进行“通知(nofity)”。
  • 服务员听到铃声之后就知道菜做好了,就可以去端菜了。

使用专业术语讲:

等待/通知机制,是指线程A调用了对象O的wait()方法进入等待状态,而线程B调用了对象O的notify()/notifyAll()方法,线程A收到通知后退出等待队列,进入可运行状态,进而执行后续操作。上述两个线程通过对象O来完成交互,而对象上的wait()方法和notify()/notifyAll()方法的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。

多线程轮流打印代码示例

通过一道面试题就能完全明白wait/notify机制。

问题:写两个线程,一个线程打印1-52,另一个线程打印A-Z,打印结果为12A34B...5152Z

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制

class Print {

    private int flag = 1;//信号量。当值为1时打印数字,当值为2时打印字母
    private int count = 1;

    public synchronized void printNum() {
        if (flag != 1) {
            //打印数字
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.print(2 * count - 1);
        System.out.print(2 * count);
        flag = 2;
        notify();
    }

    public synchronized void printChar() {
        if (flag != 2) {
            //打印字母
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.print((char) (count - 1 + 'A'));
        count++;//当一轮循环打印完之后,计数器加1
        flag = 1;
        notify();
    }
}

public class Test {
    public static void main(String[] args) {
        Print print = new Print();
        new Thread(() -> {
            for (int i = 0; i < 26; i++) {
                print.printNum();
            }
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 26; i++) {
                print.printChar();
            }
        }).start();
    }
}

FAQ

wait、notify/notifyAll和sleep的区别与联系

  1. 前三个方法是Object的本地final方法,sleep方法是Thead类的静态方法。
  2. wait使当前线程阻塞,前提是必须先获得锁,所以只能在synchronized锁范围内里使用wait、notify/notifyAll方法,而sleep可以在任何地方使用。
  3. sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常。
  4. notify和wait的顺序不能错,如果A线程先执行notify方法,B线程在执行wait方法,那么B线程是无法被唤醒的。

notify和notifyAll的区别

  • notify方法只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现。
  • notifyAll会唤醒所有等待(对象的)线程,尽管哪一个线程将会第一个处理取决于操作系统的实现。如果当前情况下有多个线程需要被唤醒,推荐使用notifyAll方法。

sleep和wait的区别

  1. 当线程执行sleep方法时,不会释放当前的锁(如果当前线程进入了同步锁),也不会让出CPU。sleep(milliseconds)可以用指定时间使它自动唤醒过来,如果时间不到只能调用interrupt方法强行打断。
  2. 当线程执行wait方法时,会释放当前的锁,然后让出CPU,进入等待状态。只有当notify/notifyAll被执行时候,才会唤醒一个或多个正处于等待状态的线程,然后继续往下执行,直到执行完synchronized代码块的代码或是中途遇到wait() ,再次释放锁。

Thread.Sleep(0)的作用是“触发操作系统立刻重新进行一次CPU竞争”

notify/notifyAll的执行只是唤醒沉睡的线程,而不会立即释放锁,必须执行完notify()方法所在的synchronized代码块后才释放。所以在编程中,尽量在使用了notify/notifyAll()后立即退出临界区。

改变线程优先级

每个线程执行时都具有一定的优先级,优先级高的线程获得较多的执行机会,而优先级低的线程则获得较少的执行机会。每个线程默认的优先级都与创建它的父线程的优先级相同。Thread类提供了setPriority(int newPriority)来设置指定线程的优先级,提供了getPriority()来返回指定线程的优先级。

JAVA提供了10个优先级级别,但这些优先级需要操作系统支持。不同的操作系统上的优先级并不相同,而且也不能很好的和JAVA的10个优先级对应,比如:Windows 2000仅提供了7个优先级。因此,写代码的时候应该尽量避免直接为线程指定优先级,而应该使用MAX_PRIORITY、MIN_PRIORITY、NORM_PRIORITY这三个静态常量来设置优先级,这样才能保证程序有最好的可移植性。

一个线程两次调用start方法会出现什么情况?

Java的线程是不允许启动两次的,第二次调用必然会抛出IllegalThreadStateException,这是一种运行时异常。

park()与unpark()

concurrent包是基于AQS(AbstractQueuedSynchronizer)框架的,AQS框架借助于两个类:

  1. Unsafe(提供CAS操作)
  2. LockSupport(提供park/unpark操作)

LockSupport.park()和LockSupport.unpark(Thread thread)调用的是Unsafe中的native代码。

有关AQS,可以查看笔者之前的博客,快速了解基于AQS实现的Java并发工具类

park与unpark的特点

  1. park/unpark的设计原理核心是“许可”(permit):park是等待一个许可,unpark是为某线程提供一个许可。permit不能叠加,也就是说permit的个数要么是0,要么是1。也就是不管连续调用多少次unpark,permit也是1个。线程调用一次park就会消耗掉permit,再一次调用park又会阻塞住。如果某线程A调用park,那么除非另外一个线程调用unpark(A)给A一个许可,否则线程A将阻塞在park操作上。
  2. unpark可以先于park调用。在使用park和unpark的时候可以不用担心park的时序问题造成死锁。相比之下,wait/notify存在时序问题,wait必须在notify调用之前调用,否则虽然另一个线程调用了notify,但是由于在wait之前调用了,wait感知不到,就造成wait永远在阻塞。
  3. park和unpark调用的时候不需要获取同步锁。

park与unpark的优点

与Object类的wait/notify机制相比,park/unpark有两个优点:

  1. 以thread为操作对象更符合阻塞线程的直观定义。
  2. 操作更精准,可以准确地唤醒某一个线程(notify随机唤醒一个线程,notifyAll唤醒所有等待的线程),增加了灵活性。

底层实现原理

Linux系统下,是用的Posix线程库pthread中的mutex(互斥量),condition(条件变量)来实现的。 mutex和condition保护了一个_counter的变量,当park时,这个变量被设置为0,当unpark时,这个变量被设置为1。

更多内容,欢迎关注微信公众号:全菜工程师小辉~

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-08-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 全菜工程师小辉 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
1 条评论
热度
最新
wait也可能会抛出异常
wait也可能会抛出异常
回复回复点赞举报
推荐阅读
编辑精选文章
换一批
Java多线程学习(四)等待/通知(wait/notify)机制
我自己总结的Java学习的系统知识点以及面试问题,目前已经开源,会一直完善下去,欢迎建议和指导欢迎Star: https://github.com/Snailclimb/Java-Guide
用户2164320
2018/06/22
2.1K3
Java多线程学习(四)等待/通知(wait/notify)机制
Java多线程学习(五)——等待通知机制
方法wait()的作用是使当前线程进行等待,wait()方法是Object类的方法,该方法用来将当前线程放到“预执行队列”,并在wait()所在的代码处停止执行,直到接到通知或中断为止。只能在同步方法或同步快中使用wait()方法,执行wait()后,当前线程释放锁。
小森啦啦啦
2019/07/15
8890
java多线程之六种状态
其中,RUNNABLE状态包括 【运行中】 和 【就绪】; BLOCKED(阻塞态)状态只有在【等待进入synchronized方法(块)】和 【其他Thread调用notify()或notifyAll(),但是还未获得锁】才会进入;
用户9854323
2022/06/25
2840
Java并发编程之wait、notify和join原理
使用while循环去循环判断一个条件,而不是使用if只判断一次条件;即wait()要在while循环中
冬天vs不冷
2025/01/21
940
Java并发编程之wait、notify和join原理
Java多线程系列——线程间通信
Java多线系列文章是Java多线程的详解介绍,对多线程还不熟悉的同学可以先去看一下我的这篇博客Java基础系列3:多线程超详细总结,这篇博客从宏观层面介绍了多线程的整体概况,接下来的几篇文章是对多线程的深入剖析。
说故事的五公子
2019/12/10
7460
Java多线程系列——线程间通信
线程间通信wait---notify
一、基本知识 wait()方法可以使调用该方法的线程释放共享资源的锁,然后从运行状态退出,进入等待队列,直到被再次唤醒 notify()方法可以随机唤醒等待队列中等待同一共享资源的“一个”线程,并使该线程退出等待队列,进入可运行状态,也就是notify()方法仅通知“一个”线程。 notifyAll()方法可以使所有正在等待队列等待同一共享资源的“全部”线程从等待状态退出,进入可运行状态。此时,优先级最高的那个线程最先执行,但也有可能是随机执行,因为这要取决于JVM虚拟机的实现 二、示例 创建一个MyLis
凡人飞
2020/09/21
4550
Android并发编程 多线程与锁
该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,如果能给各位看官带来一丝启发或者帮助,那真是极好的。
LoveWFan
2018/11/30
9000
超强图文|并发编程【等待/通知机制】就是这个feel~
你有一个思想,我有一个思想,我们交换后,一个人就有两个思想 If you can NOT explain it simply, you do NOT understand it well enough
用户4172423
2020/03/25
5160
java多线程编程核心技术——第三章总结
用户1134788
2018/01/05
7980
java多线程编程核心技术——第三章总结
面试必答题“聊聊Java中线程的生命周期状态”如何破?
👆点击“博文视点Broadview”,获取更多书讯 “聊聊Java中线程的生命周期状态吧!” 这几乎是一道面试必答题,这道题怎么答才是最佳答案呢?本文就带大家来破解一下! 01 一张图说明线程生命周期 JVM源码中将线程的生命周期分为新建(New)、可运行(Runnable)、阻塞(Blocked)、等待(Waiting)、超时等待(Timed_Waiting)和终止(Terminated)这6种状态。 在系统运行过程中不断有新的线程被创建,老的线程在执行完毕后被清理,线程在排队获取共享资源或者锁时将被
博文视点Broadview
2022/07/04
3130
面试必答题“聊聊Java中线程的生命周期状态”如何破?
Java线程间通讯之wait()、notify()、notifyAll()-等待通知机制(经常面试:锁的释放问题)
wait方法是Object类的方法。调用此方法会使当前线程进入“预执行队列”中,并在wait所在代码行处停止执行,直到被其他线程通知(notify、notifyAll)或被中断为止。
崔认知
2023/06/19
3190
Java线程间通讯之wait()、notify()、notifyAll()-等待通知机制(经常面试:锁的释放问题)
【多线程】之线程通讯wait和notify的使用
1、定义 等待/通知机制,是指一个线程A调用了对象object的wait()方法进入等待状态,而另一个线程B调用了对象object的notify或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而还行后续操作。 使用wait和notify方法实现线程之间的通信,这两个方法是Object类的方法。 注意细节: 1.1 调用wait()方法,会释放锁,线程状态由RUNNING->WAITNG,当前线程进入对象等待; 1.2 调用notify()/notifyAll()方法不会立马释放锁,notify()方法是将等待队列中的线程移到同步队列中,而notifyAll()则是全部移到同步队列中, 被移出的线程状态WAITING-->BLOCKED; 重点注意,等待队列和同步队列的转换;wait()后进入等待队列;notify()/notifyAll(),线程进入同步队列; 1.3 当前调用notify()/notifyAll()的线程释放锁了才算释放锁,才有机会唤醒wait线程; 1.4 从wait()返回的前提是必须获得调用对象锁,也就是说notify()与notifyAll()释放锁之后,wait()进入BLOCKED状态,如果其他线程 有竞争当前锁的话,wait线程继续争取锁资格。可以理解为,从同步队列中的线程抢占锁执行; 1.5 使用wait()、notify()、notifyAll()方法时需要先调对象加锁。这就是跟synchronized关键字配置使用; 2、代码运行过程
用户5640963
2021/06/09
3950
Java线程等待、唤醒通信机制详解
要想实现多个线程之间的协同,如:线程执行先后顺序、获取某个线程执行的结果等。 涉及到线程之间相互通信,分为如下四类:
JavaEdge
2022/11/30
8810
Java线程等待、唤醒通信机制详解
Java并发编程,Condition的await和signal等待通知机制
Object类是Java中所有类的父类, 在线程间实现通信的往往会应用到Object的几个方法: wait(),wait(long timeout),wait(long timeout, int nanos)与notify(),notifyAll() 实现等待/通知机制,同样的, 在Java Lock体系下依然会有同样的方法实现等待/通知机制。 从整体上来看Object的wait和notify/notify是与对象监视器配合完成线程间的等待/通知机制,Condition与Lock配合完成等待/通知机制, 前者是Java底层级别的,后者是语言级别的,具有更高的可控制性和扩展性。 两者除了在使用方式上不同外,在功能特性上还是有很多的不同:
李红
2019/06/03
1.2K0
LockSupport秘籍:新手入门,高手精通,玩转同步控制
java.util.concurrent.locks.LockSupport 是 Java 并发编程中的一个非常有用的线程阻塞工具类,它包含可以阻塞和唤醒线程的方法。这个类是Java并发编程中的基础工具之一,通常用于构建锁或其他同步组件。LockSupport的所有方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。
公众号:码到三十五
2024/03/19
2200
LockSupport秘籍:新手入门,高手精通,玩转同步控制
Java中线程的状态变化
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,有几种状态呢?在API中java.lang.Thread.State这个枚举中给出了六种线程状态:
绿水长流z
2024/06/13
1270
Java中线程的状态变化
Java 线程基础
简言之,进程可视为一个正在运行的程序。它是系统运行程序的基本单位,因此进程是动态的。进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动。进程是操作系统进行资源分配的基本单位。
静默虚空
2019/12/26
4780
Java 线程基础
【小家java】并发编程中wait/notify await/singal notify/notifyAll sleep/yield 的区别以及死锁案例
调用sleep和yield的时候不释放当前线程所获得的锁,但是调用await/wait的时候却释放了其获取的锁并阻塞等待。
YourBatman
2019/09/03
8820
【小家java】并发编程中wait/notify  await/singal  notify/notifyAll sleep/yield 的区别以及死锁案例
JUC在深入面试题——三种方式实现线程等待和唤醒(wait/notify,await/signal,LockSupport的park/unpark)
在多线程的场景下,我们会经常使用加锁,来保证线程安全。如果锁用的不好,就会陷入死锁,我们以前可以使用Object的wait/notify来解决死锁问题。也可以使用Condition的await/signal来解决,当然最优还是LockSupport的park/unpark。他们都是解决线程等待和唤醒的。下面来说说具体的优缺点和例子证明一下。
掉发的小王
2022/07/11
7380
JUC在深入面试题——三种方式实现线程等待和唤醒(wait/notify,await/signal,LockSupport的park/unpark)
wait和notify实现线程之间的通信
终有救赎
2023/10/16
2520
wait和notify实现线程之间的通信
推荐阅读
相关推荐
Java多线程学习(四)等待/通知(wait/notify)机制
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验