sleep/wait/notify/notifyAll分别有什么作用?它们的区别是什么?wait时为什么要放在循环里而不能直接用if?

简介

首先对几个相关的方法做个简单解释,Object中有几个用于线程同步的方法:wait、notify、notifyAll。

public class Object { public final native void wait(long timeout) throws InterruptedException; public final native void notify(); public final native void notifyAll();}
  • wait: 释放当前锁,阻塞直到被notify或notifyAll唤醒,或者超时,或者线程被中断(InterruptedException)
  • notify: 任意选择一个(无法控制选哪个)正在这个对象上等待的线程把它唤醒,其它线程依然在等待被唤醒
  • notifyAll: 唤醒所有线程,让它们去竞争,不过也只有一个能抢到锁
  • sleep: 不是Object中的方法,而是Thread类的静态方法,让当前线程持有锁阻塞指定时间

sleep和wait

sleep和wait都可以让线程阻塞,也都可以指定超时时间,甚至还都会抛出中断异常InterruptedException。

而它们最大的区别就在于,sleep时线程依然持有锁,别人无法进当前同步方法;wait时放弃了持有的锁,其它线程有机会进入该同步方法。多次提到同步方法,因为wait必须在synchronized同步代码块中,否则会抛出异常IllegalMonitorStateException,notify也是如此,可以说wait和notify是就是为了在同步代码中做线程调度而生的。

下面一个简单的例子展现sleep和wait的区别:

import java.util.Date;import java.util.concurrent.atomic.AtomicInteger;public class Main { // 日志行号记录 private AtomicInteger count = new AtomicInteger(); public static void main(String[] args) throws InterruptedException { Main main = new Main(); // 开启两个线程去执行test方法 new Thread(main::test).start(); new Thread(main::test).start(); } private synchronized void test() { try { log("进入了同步方法,并开始睡觉,1s"); // sleep不会释放锁,因此其他线程不能进入这个方法 Thread.sleep(1000); log("睡好了,但没事做,有事叫我,等待2s"); //阻塞在此,并且释放锁,其它线程可以进入这个方法 //当其它线程调用此对象的notify或者notifyAll时才有机会停止阻塞 //就算没有人notify,如果超时了也会停止阻塞 wait(2000); log("我要走了,但我要再睡一觉,10s"); //这里睡的时间很长,因为没有释放锁,其它线程就算wait超时了也无法继续执行 Thread.sleep(10000); log("走了"); notify(); } catch (InterruptedException e) { e.printStackTrace(); } } // 打印日志 private void log(String s) { System.out.println(count.incrementAndGet() + " " + new Date().toString().split(" ")[3] + "" + Thread.currentThread().getName() + " " + s); }}/* 输出:1 00:13:23Thread-0 进入了同步方法,并开始睡觉,1s2 00:13:24Thread-0 睡好了,但没事做,有事叫我,等待2s3 00:13:24Thread-1 进入了同步方法,并开始睡觉,1s4 00:13:25Thread-1 睡好了,但没事做,有事叫我,等待2s5 00:13:26Thread-0 我要走了,但我要再睡一觉,10s6 00:13:36Thread-0 走了7 00:13:36Thread-1 我要走了,但我要再睡一觉,10s8 00:13:46Thread-1 走了*/

对输出做个简单解释(已经看懂代码的童鞋可以跳过):

1 00:13:23Thread-0 进入了同步方法,并开始睡觉,1s // Thread-0首先进入同步方法,Thread-1只能门外候着2 00:13:24Thread-0 睡好了,但没事做,有事叫我,等待2s // Thread-0 sleep 1秒这段时间,Thread-1没进来,证明sleep没有释放锁3 00:13:24Thread-1 进入了同步方法,并开始睡觉,1s // Thread-0开始wait后Thread-1马上就进来了,证明wait释放了锁4 00:13:25Thread-1 睡好了,但没事做,有事叫我,等待2s // Thread-1也打算wait 2秒(2秒后真的能醒来吗?)5 00:13:26Thread-0 我要走了,但我要再睡一觉,10s // Thread-0已经wait超时醒来了,这次准备sleep 10s6 00:13:36Thread-0 走了 // 10s过去了Thread-0都sleep结束了,那个说要wait 2s的Thread-1还没动静,证明超时也没用,还得抢到锁7 00:13:36Thread-1 我要走了,但我要再睡一觉,10s // Thread-0退出同步代码后,Thread-1才终于得到了锁,能行动了8 00:13:46Thread-1 走了

notify和notifyAll

同样是唤醒等待的线程,同样最多只有一个线程能获得锁,同样不能控制哪个线程获得锁。

区别在于:

  • notify:唤醒一个线程,其他线程依然处于wait的等待唤醒状态,如果被唤醒的线程结束时没调用notify,其他线程就永远没人去唤醒,只能等待超时,或者被中断
  • notifyAll:所有线程退出wait的状态,开始竞争锁,但只有一个线程能抢到,这个线程执行完后,其他线程又会有一个幸运儿脱颖而出得到锁

如果觉得解释的不够明白,代码来一波:

import java.util.Date;import java.util.concurrent.ArrayBlockingQueue;import java.util.concurrent.atomic.AtomicInteger;public class Main { private AtomicInteger count = new AtomicInteger(); public static void main(String[] args) throws InterruptedException { Main main = new Main(); // 开启两个线程去执行test方法 for (int i = 0; i < 10; i++) { new Thread(main::testWait).start(); } Thread.sleep(1000); for (int i = 0; i < 5; i++) { main.testNotify(); } } private synchronized void testWait() { try { log("进入了同步方法,开始wait"); wait(); log("wait结束"); } catch (InterruptedException e) { e.printStackTrace(); } } private synchronized void testNotify() { notify(); } private void log(String s) { System.out.println(count.incrementAndGet() + " " + new Date().toString().split(" ")[3] + "" + Thread.currentThread().getName() + " " + s); }}/* 输出:1 00:59:32Thread-0 进入了同步方法,开始wait2 00:59:32Thread-9 进入了同步方法,开始wait3 00:59:32Thread-8 进入了同步方法,开始wait4 00:59:32Thread-7 进入了同步方法,开始wait5 00:59:32Thread-6 进入了同步方法,开始wait6 00:59:32Thread-5 进入了同步方法,开始wait7 00:59:32Thread-4 进入了同步方法,开始wait8 00:59:32Thread-3 进入了同步方法,开始wait9 00:59:32Thread-2 进入了同步方法,开始wait10 00:59:32Thread-1 进入了同步方法,开始wait11 00:59:33Thread-0 wait结束12 00:59:33Thread-6 wait结束13 00:59:33Thread-7 wait结束14 00:59:33Thread-8 wait结束15 00:59:33Thread-9 wait结束*/

例子中有10个线程在wait,但notify了5次,然后其它线程一直阻塞,这也就说明使用notify时如果不能准确控制和wait的线程数对应,可能会导致某些线程永远阻塞。

使用notifyAll唤醒所有等待的线程:

import java.util.Date;import java.util.concurrent.ArrayBlockingQueue;import java.util.concurrent.atomic.AtomicInteger;public class Main { private AtomicInteger count = new AtomicInteger(); public static void main(String[] args) throws InterruptedException { Main main = new Main(); // 开启两个线程去执行test方法 for (int i = 0; i < 5; i++) { new Thread(main::testWait).start(); } Thread.sleep(1000); main.testNotifyAll(); } private synchronized void testWait() { try { log("进入了同步方法,开始wait"); wait(); log("wait结束"); } catch (InterruptedException e) { e.printStackTrace(); } } private synchronized void testNotifyAll() { notifyAll(); } private void log(String s) { System.out.println(count.incrementAndGet() + " " + new Date().toString().split(" ")[3] + "" + Thread.currentThread().getName() + " " + s); }}/* 输出:1 01:03:24Thread-0 进入了同步方法,开始wait2 01:03:24Thread-4 进入了同步方法,开始wait3 01:03:24Thread-3 进入了同步方法,开始wait4 01:03:24Thread-2 进入了同步方法,开始wait5 01:03:24Thread-1 进入了同步方法,开始wait6 01:03:25Thread-1 wait结束7 01:03:25Thread-2 wait结束8 01:03:25Thread-3 wait结束9 01:03:25Thread-4 wait结束10 01:03:25Thread-0 wait结束*/

只需要调用一次notifyAll,所有的等待线程都被唤醒,并且去竞争锁,然后依次(无序)获取锁完成了后续任务。

为什么wait要放到循环中使用

一些源码中出现wait时,往往都是伴随着一个循环语句出现的,比如:

private synchronized void f() throws InterruptedException { while (!isOk()) { wait(); } System.out.println("I'm ok");}

既然wait会被阻塞直到被唤醒,那么用if+wait不就可以了吗?其他线程发现条件达到时notify一下不就行了?

理想情况确实如此,但实际开发中我们往往不能保证这个线程被notify时条件已经满足了,因为很可能有某个无关(和这个条件的逻辑无关)的线程因为需要线程调度而调用了notify或者notifyAll。此时如果样例中位置等待的线程不巧被唤醒,它就会继续往下执行,但因为用的if,这次被唤醒就不会再判断条件是否满足,最终程序按照我们不期望的方式执行下去。

wait放弃对象锁_终于搞懂了sleep/wait/notify/notifyAll,真的是不容易相关推荐

  1. 终于搞懂了sleep/wait/notify/notifyAll

    作者:acupt,专注Java,架构师社区合伙人! sleep/wait/notify/notifyAll分别有什么作用?它们的区别是什么?wait时为什么要放在循环里而不能直接用if? 简介 首先对 ...

  2. 请列举你了解的分布式锁_终于搞懂分布式锁是什么了!

    当下在互联网技术架构中,最流行的莫过于分布式架构了.为什么大家纷纷都采用分布式架构呢? 1.高效低廉,将部署在高性能机的程序分散在多个小型机中部署: 2.扩展性强,可随着业务的扩展而横向扩展系统的性能 ...

  3. cad布局怎么用_终于搞懂CAD的布局是个什么玩意儿了!原来布局要这样用

    很多初学的小伙伴都没搞懂CAD布局是怎么一回事儿,其实也没你想像的那么难.今天小编就来跟大家说一说,关于如何新建布局和比例设置等内容,希望对大家有所帮助. 一.布局视口如何定义 1.命令 模型定义布局 ...

  4. 什么是分布式_终于搞懂分布式锁是什么了

    当下在互联网技术架构中,最流行的莫过于分布式架构了.为什么大家纷纷都采用分布式架构呢? 1.高效低廉,将部署在高性能机的程序分散在多个小型机中部署: 2.扩展性强,可随着业务的扩展而横向扩展系统的性能 ...

  5. 实时监测tcp链接状态_终于搞懂了 TCP 的 11 种状态,太不容易了…

    后台回复"666",获取新资料 本来想写运维过程中,nginx 服务器中 time_wait 的相关测试及解决方法的,然后发现TCP 的状态需要先铺垫一下,于是就整理了这篇文章. ...

  6. shell脚本回车换行_终于搞懂了回车与换行的区别

    关于换行和回车其实平时我们不太在意,所以关于两者的区别也不太清楚,在平时开发时可能会遇到一些文件处理的问题,放到不同的操作系统上出现各种坑.那么回车和换行到底有哪些区别呢?今天咱们就来总结一下. 1. ...

  7. python换行和回车的区别_终于搞懂了回车与换行的区别

    关于换行和回车其实平时我们不太在意,所以关于两者的区别也不太清楚,在平时开发时可能会遇到一些文件处理的问题,放到不同的操作系统上出现各种坑.那么回车和换行到底有哪些区别呢?今天咱们就来总结一下. 1. ...

  8. IntelliJ IDEA 部署 Web 项目,终于搞懂了!

    IntelliJ IDEA 部署 Web 项目,终于搞懂了! 这篇牛逼:Java 程序员必备的 Intellij IDEA 插件 IDEA 中最重要的各种设置项,就是这个 Project Struct ...

  9. 电压和电流反馈判别及例子,绝对让你通透,其实也没有那么难,一次就看懂!从此终于搞懂了电压反馈和电流反馈!

    电压和电流反馈判别及例子,其实也没有那么难,绝对让你通透,一次就看懂!从此终于搞懂了电压反馈和电流反馈! 一个简单粗暴的判断方法: 先看反馈是否直接连到Uo输出端(若不是直接从输出端引出,则为电流反馈 ...

最新文章

  1. Python3 的urllib实例
  2. mysql 的select语句_MySQLSELECT语句_MySQL
  3. AIX中一些常用的命令汇总
  4. 函数式编程语言python-10分钟学会python函数式编程
  5. 编写可靠 bash 脚本的一些技巧
  6. python连连看小游戏_请用PYTHON编一个小游戏,如五子棋,连连看,贪吃蛇,扫雷,计算器等等...
  7. rmi远程代码执行漏洞_【最新漏洞简讯】WebLogic远程代码执行漏洞 (CVE202014645)
  8. mysql ip比较大小_MySQL优化/面试,看这一篇就够了
  9. linux配置ssh免密码登录
  10. 发那科机器人寄存器Ar_浅谈发那科机器人与TP参数之间的关系
  11. [Realtek sdk-3.4.14b]升级iptables以支持IPv6 DHCPV6 NAT6的MASQUERADE属性(原厂默认iptables不支持NAT6)
  12. “人人皆可成为AI开发者”!百度世界大会官宣百度松果学堂成立
  13. 递归解决卖桃子问题java
  14. python:从excel中提取高频词生成词云
  15. QT控件最上层或最下层显示
  16. 年底裁员潮,你有没有被N+1?
  17. 无线网卡无法与计算机usb,台式电脑主机安装USB无线网卡使用不了WiFi网络怎么办?...
  18. Apollo坐标系转换
  19. 关于docker容器启动后修改或添加端口
  20. 专题10:如何应对面试官的拷问—你了解python的装饰器吗?

热门文章

  1. 一起谈.NET技术,C#序列化与反序列化(Serializable and Deserialize)
  2. Microsoft Forefront TMG(ISA2008)简体中文商业版(MBE)发布
  3. 军职在线c语言程序设计答案,2018事业单位联考职测C真题与答案解析.docx
  4. linux编译安装的好处,Linux学习—源码安装
  5. matlab最优控制实验报告_第十二篇 章 用MATLAB解最优控制问题及应用实例 最优控制课件.ppt...
  6. python中plot的图像类型_Python绘图问题:Matplotlib中指定图片大小和像素
  7. laravel 数据库获取值的常用方法
  8. 代码与html混合,自定义的标签与html的标签混合应用_css
  9. 如何采集指定年份的poi_房价关键影响因素分析:从数据采集到建模全过程
  10. CIKM 2021 | 图模型在广告检索(Ad Retrieval)中的应用