作者:acupt,专注Java,架构师社区合伙人!


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

简介

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

12345
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的区别:

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
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]                + "\t" + Thread.currentThread().getName() + " " + s);    }}

/* 输出:

1 00:13:23  Thread-0 进入了同步方法,并开始睡觉,1s2 00:13:24 Thread-0 睡好了,但没事做,有事叫我,等待2s3 00:13:24    Thread-1 进入了同步方法,并开始睡觉,1s4 00:13:25 Thread-1 睡好了,但没事做,有事叫我,等待2s5 00:13:26    Thread-0 我要走了,但我要再睡一觉,10s6 00:13:36 Thread-0 走了7 00:13:36 Thread-1 我要走了,但我要再睡一觉,10s8 00:13:46 Thread-1 走了

*/

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

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

notify和notifyAll

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

区别在于:

  • notify:唤醒一个线程,其他线程依然处于wait的等待唤醒状态,如果被唤醒的线程结束时没调用notify,其他线程就永远没人去唤醒,只能等待超时,或者被中断

  • notifyAll:所有线程退出wait的状态,开始竞争锁,但只有一个线程能抢到,这个线程执行完后,其他线程又会有一个幸运儿脱颖而出得到锁

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

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061
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]                + "\t" + Thread.currentThread().getName() + " " + s);    }

}

/* 输出:

1 00:59:32    Thread-0 进入了同步方法,开始wait2 00:59:32  Thread-9 进入了同步方法,开始wait3 00:59:32  Thread-8 进入了同步方法,开始wait4 00:59:32  Thread-7 进入了同步方法,开始wait5 00:59:32  Thread-6 进入了同步方法,开始wait6 00:59:32  Thread-5 进入了同步方法,开始wait7 00:59:32  Thread-4 进入了同步方法,开始wait8 00:59:32  Thread-3 进入了同步方法,开始wait9 00:59:32  Thread-2 进入了同步方法,开始wait10 00:59:32 Thread-1 进入了同步方法,开始wait11 00:59:33 Thread-0 wait结束12 00:59:33    Thread-6 wait结束13 00:59:33    Thread-7 wait结束14 00:59:33    Thread-8 wait结束15 00:59:33    Thread-9 wait结束

*/

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

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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
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]                + "\t" + Thread.currentThread().getName() + " " + s);    }

}

/* 输出:

1 01:03:24  Thread-0 进入了同步方法,开始wait2 01:03:24  Thread-4 进入了同步方法,开始wait3 01:03:24  Thread-3 进入了同步方法,开始wait4 01:03:24  Thread-2 进入了同步方法,开始wait5 01:03:24  Thread-1 进入了同步方法,开始wait6 01:03:25  Thread-1 wait结束7 01:03:25 Thread-2 wait结束8 01:03:25 Thread-3 wait结束9 01:03:25 Thread-4 wait结束10 01:03:25    Thread-0 wait结束

*/

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

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

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

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

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

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

长按订阅更多精彩▼

如有收获,点个在看,诚挚感谢

终于搞懂了sleep/wait/notify/notifyAll相关推荐

  1. wait放弃对象锁_终于搞懂了sleep/wait/notify/notifyAll,真的是不容易

    sleep/wait/notify/notifyAll分别有什么作用?它们的区别是什么?wait时为什么要放在循环里而不能直接用if? 简介 首先对几个相关的方法做个简单解释,Object中有几个用于 ...

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

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

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

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

  4. 关于子网掩码怎么计算!!!!我终于搞懂了!!!!

    今天终于搞明白了子网掩码啥的是啥意思了!!!我写几个就我自己看懂的! 1.首先ip呢都是XXX.XXX.XXX.XXX这样组成的然后一般来说就是255.255.255.255,对应的二进制文件就是11 ...

  5. HTTPS 终于搞懂了 !

    点击上方"芋道源码",选择"设为星标" 管她前浪,还是后浪? 能浪的浪,才是好浪! 每天 10:33 更新文章,每天掉亿点点头发... 源码精品专栏 原创 | ...

  6. 蚊子凭啥只咬你?科学家用 5 年造出一批“脑子发光”的蚊子,终于搞懂背后机制

    为了发篇 Nature,他们一天用自己喂了 3000 只蚊子?! 听起来有些不可思议,但却是普林斯顿大学一群研究人员在做的事情 -- 他们试图找出这些蚊子专门吸人血的原因. 我们都知道,有不少蚊子会吸 ...

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

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

  8. 蚊子凭啥只咬你?他们用5年造出一批“脑子发光”的蚊子,终于搞懂背后机制丨Nature...

    明敏 萧箫 发自 凹非寺 量子位 | 公众号 QbitAI 为了发篇Nature,他们一天用自己喂了3000只蚊子?! 听起来有些不可思议,但却是普林斯顿大学一群研究人员在做的事情-- 他们试图找出这 ...

  9. 我花了一个五一终于搞懂了OpenLDAP

    轻型目录访问协议(英文: Lightweight Directory Access Protocol,缩写: LDAP)是一个开放的,中立的,工业标准的应用协议,通过IP协议提供访问控制和维护分布式信 ...

最新文章

  1. leetcode算法题--零钱兑换
  2. [云炬创业基础笔记]第十一章创业计划书测试14
  3. 昨天终于收到《.Net Web服务编程》
  4. (47)FPGA面试题LATCH和DFF的区别
  5. 挣多少钱让你觉得生存无忧,有底气做感兴趣的事?
  6. stm32的串口DMA空闲中断接收不等长数据,stm32F1的usart1-DMA-IDLE收发
  7. 根据姓名判断性别-人工智能
  8. Win7/8/10系统下Protel 99 SE不能添加元件库 File is not recognized
  9. 具有相关关系的数据处理:线性混合模型与广义线性混合模型
  10. 总结各种RGB转YUV的转换公式
  11. java springboot activemq 邮件短信微服务,解决国际化服务的国内外兼容性问题,含各服务商调研情况...
  12. 应用市场显示服务器错误的是,win10应用商店打不开服务器出错怎么办
  13. 网页中使用阿里图标iconfont
  14. PP视频(PPTV聚力)web接口分析
  15. 深度学习论文: Avoiding Overfitting: A Survey on Regularization Methods for Convolutional Neural Networks
  16. 必火CTF闯关(1)
  17. c语言中数组作为参数传参
  18. echarts X轴文字竖向排列 ,一行两字竖向排列或旋转角度排列
  19. ionic 应用在iOS上打开相机拍照闪退、百度地图/高德地图定位失败(解决方案)
  20. java网课|File类递归

热门文章

  1. 关于学习Python的一点学习总结(13->浅复制和深复制)
  2. 关于学习Python的一点学习总结(2->列表)
  3. 关于python和anaconda的一些基础认识
  4. html input type=quot;filequot;,科技常识:关于type=quot;filequot;的input框样式修改小结...
  5. mysql with语句_MySQL列举数据库(SHOW DATABASES语句)
  6. HTML保存节点,{HTML5}DOM节点操作-第一节
  7. mysql用外键链接两个表_可能做一个MySQL外键的两个可能的表之一?
  8. php statements,PHP PDOStatement::setAttribute讲解
  9. SOJ 4543 4542
  10. mysql 8+ 忘记root密码 解决方案