Java中的等待唤醒机制—至少50%的工程师还没掌握!
这是一篇走心的填坑笔记,自学Java的几年总是在不断学习新的技术,一路走来发现自己踩坑无数,而填上的坑却屈指可数。突然发现,有时候真的不是几年工作经验的问题,有些东西即使工作十年,没有用心去学习过也不过是一个10年大坑罢了(真实感受)。
刚开始接触多线程时,就知道有等待/唤醒这个东西,写过一个demo就再也没有看过了,至于它到底是个什么东西,或者说它能解决什么样的问题,估计大多数人和我一样都是模棱两可。这次笔者就尝试带你搞懂等待/唤醒机制,读完本文你将get到以下几点:
- 循环等待带来什么样的问题
- 用等待唤醒机制优化循环等待
- 等待唤醒机制中的被忽略的细节
一,循环等待问题
假设今天要发工资,强老板要去吃一顿好的,整个就餐流程可以分为以下几个步骤:
- 点餐
- 窗口等待出餐
- 就餐
public static void main(String[] args) {
// 是否还有包子
AtomicBoolean hasBun = new AtomicBoolean();
// 包子铺老板
new Thread(() -> {
try {
// 一直循环查看是否还有包子
while (true) {
if (hasBun.get()) {
System.out.println("老板:检查一下是否还剩下包子...");
Thread.sleep(3000);
} else {
System.out.println("老板:没有包子了, 马上开始制作...");
Thread.sleep(1000);
System.out.println("老板:包子出锅咯....");
hasBun.set(true);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
System.out.println("小强:我要买包子...");
try {
// 每隔一段时间询问是否完成
while (!hasBun.get()) {
System.out.println("小强:包子咋还没做好呢~");
Thread.sleep(3000);
}
System.out.println("小强:终于吃上包子了....");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
在上文代码中存在一个很大的问题,就是老板需要不断的去检查是否还有包子,而客户则需要隔一段时间去看催一下老板,这显然是不合理的,这就是典型的循环等待问题。
这种问题的代码中通常是如下这种模式:
while (条件不满足) {
Thread.sleep(3000);
}
doSomething();
对应到计算机中,则暴露了一个问题:不断通过轮询机制来检测条件是否成立, 如果轮询时间过小则会浪费CPU资源,如果间隔过大,又导致不能及时获取想要的资源。
二,等待/唤醒机制
为了解决循环等待消耗CPU以及信息及时性问题,Java中提供了等待唤醒机制。通俗来讲就是由主动变为被动, 当条件成立时,主动通知对应的线程,而不是让线程本身来询问。
2.1 基本概念
等待/唤醒机制,又叫等待通知(笔者更喜欢叫唤醒而非通知),是指线程A调用了对象O的wait()方法进入了等待状态,而另一个线程调用了O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。
上诉过程是通过对象O,使得线程A和线程B之间进行通信, 在线程中调用了对象O的wait()方法后线程久进入了阻塞状态,而在其他线程中对象O调用notify()或notifyAll方法时,则会唤醒对应的阻塞线程。
2.2 基本API
等待/唤醒机制的相关方法是任意Java对象具备的,因为这些方法被定义在所有Java对象的超类Object中。
notify: 通知一个在对象上等待的线程,使其从wait()方法返回,而返回的前提是该线程获取到对象的锁
notifyAll: 通知所有等待在该对象上的线程
wait: 调用此方法的线程进入阻塞等待状态,只有等待另外线程的通知或者被中断才会返回,调用wait方法会释放对象的锁
wait(long) : 等待超过一段时间没有被唤醒就超时自动返回,单位是毫秒。
2.3 用等待唤醒机制优化循环等待
public static void main(String[] args) {
// 是否还有包子
AtomicBoolean hasBun = new AtomicBoolean();
// 锁对象
Object lockObject = new Object();
// 包子铺老板
new Thread(() -> {
try {
while (true) {
synchronized (lockObject) {
if (hasBun.get()) {
System.out.println("老板:包子够卖了,打一把王者荣耀");
lockObject.wait();
} else {
System.out.println("老板:没有包子了, 马上开始制作...");
Thread.sleep(3000);
System.out.println("老板:包子出锅咯....");
hasBun.set(true);
// 通知等待的食客
lockObject.notifyAll();
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
System.out.println("小强:我要买包子...");
try {
synchronized (lockObject) {
if (!hasBun.get()) {
System.out.println("小强:看一下有没有做好, 看公众号cruder有没有新文章");
lockObject.wait();
} else {
System.out.println("小强:包子终于做好了,我要吃光它们....");
hasBun.set(false);
lockObject.notifyAll();
System.out.println("小强:一口气把店里包子吃光了, 快快乐乐去板砖了~~");
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
上述流程,减少了轮询检查的操作,并且线程调用wait()方法后,会释放锁,不会消耗CPU资源,进而提高了程序的性能。
三,等待唤醒机制的基本范式
等待、唤醒是线程间通信的手段之一,用来协调多个线程操作同一个数据源。实际应用中通常用来优化循环等待的问题,针对等待方和通知方,可以提炼出如下的经典范式。
需要注意的是,在等待方执行的逻辑中,一定要用while循环来判断等待条件,因为执行notify/notifyAll方法时只是让等待线程从wait方法返回,而非重新进入临界区
/**
* 等待方执行的逻辑
* 1. 获取对象的锁
* 2. 检查条件,如果条件不满足,调用对象的wait方法,被通知后重新检查条件
* 3. 条件满足则执行对应的逻辑
*/
synchronized(对象){
while(条件不满足){
对象.wait()
}
doSomething();
}
/**
* !! 通知方执行的逻辑
* 1. 获取对象的锁
* 2. 改变条件
* 3. 通知(所有)等待在对象上的线程
*/
synchronized(对象){
条件改变
对象.notify();
}
这个编程范式通常是针对典型的通知方和等待方,有时双方可能具有双重身份,即使等待方又是通知方,正如我们上文中的案例一样。
四,notify/notifyAll不释放锁
相信这个问题有半数工程师都不知道,当执行wait()方法,锁自动被释放;但执行完notify()方法后,锁不会释放,而是要执行notify()方法所在的synchronized代码块后才会释放。这一点很重要,也是很多工程师容易忽略的地方。
lockObject.notifyAll();
System.out.println("小强:一口气把店里包子吃光了, 快快乐乐去板砖了~~");
案例代码中,故意设置成先notifyAll,然后在打印;上文图中的结果也印证了了我们的描述,感兴趣的小伙伴可以动手执行一下案例代码哦。
五,等待、唤醒必须先获取锁
在等待、唤醒编程范式中的wait,notify,notifyAll方法往往不能直接调用, 需要在获取锁之后的临界区执行
并且只能唤醒等待在同一把锁上的线程。
当线程调用wait方法时会被加入到一个等待队列,当执行notify时会唤醒队列中第一个等待线程(等待时间最长的线程),而调用notifyAll时则会唤醒等待线程中所有的等待线程。
六,sleep不释放锁 而wait 释放
在用等待唤醒机制优化循环等待的过程中,有一个重要的特征就是原本的sleep()方法用wait()方法取代,他们的最大的区别在于wait方法会释放锁,而sleep不会,除此之外,还有个重要的区别,sleep是Thread的方法,可以在任意地方执行;而wait是Object对象的方法,必须在synchronized代码块中执行。
Java中的等待唤醒机制—至少50%的工程师还没掌握!的更多相关文章
- Java 中的等待唤醒机制透彻讲解
线程的状态 首先了解一下什么是线程的状态,线程状态就是当线程被创建(new),并且启动(start)后,它不是一启动就进入了执行状态(run),也不是一直都处于执行状态. 这里说一下Java 的Thr ...
- 多线程之Java中的等待唤醒机制
多线程的问题中的经典问题是生产者和消费者的问题,就是如何让线程有序的进行执行,获取CPU执行时间片的过程是随机的,如何能够让线程有序的进行,Java中提供了等待唤醒机制很好的解决了这个问题! 生产者消 ...
- java基础知识回顾之java Thread类学习(八)--java多线程通信等待唤醒机制经典应用(生产者消费者)
*java多线程--等待唤醒机制:经典的体现"生产者和消费者模型 *对于此模型,应该明确以下几点: *1.生产者仅仅在仓库未满的时候生产,仓库满了则停止生产. *2.消费者仅仅在有产品的时 ...
- Java学习:等待唤醒机制
等待唤醒机制 线程的状态 NEW 至今尚未启动的线程处于这种状态 RUNNABLE 正在Java虚拟机中执行的线程处于这种状态 BLOCKED 受阻塞并等待某个监视器锁的线程处于这种状态 WA ...
- java基础知识回顾之java Thread类学习(七)--java多线程通信等待唤醒机制(wait和notify,notifyAll)
1.wait和notify,notifyAll: wait和notify,notifyAll是Object类方法,因为等待和唤醒必须是同一个锁,不可以对不同锁中的线程进行唤醒,而锁可以是任意对象,所以 ...
- java基础-多线程 等待唤醒机制
/** * @param args * 等待唤醒机制 */ public static void main(String[] args) { final Printer p = new ...
- java锁在等待唤醒机制中作用
等待的线程放在线程池wait().notify().notifyall()都使用在同步中,因为要对持有监视器(锁)的线程操作.所以要使用在同步中,因为只有同步才具有锁. 为什么这些操作的线程的方法要定 ...
- java多线程的等待唤醒机制及如何解决同步过程中的安全问题
/* class Person{ String name; String sex; boolean flag = true; public void setPerson(String name, St ...
- Android(java)学习笔记71:生产者和消费者之等待唤醒机制
1. 首先我们根据梳理我们之前Android(java)学习笔记70中关于生产者和消费者程序思路: 2. 下面我们就要重点介绍这个等待唤醒机制: (1)第一步:还是先通过代码体现出等待唤醒机制 pac ...
随机推荐
- netty服务端的创建
服务端的创建 示例代码 netty源码中有一个netty-example项目,不妨以经典的EchoServer作为楔子. // 步骤1 EventLoopGroup bossGroup = new N ...
- [ISE调试] 在ISE调试过程中,遇到过的warning以及消除办法
1.在generate的过程中,在Map环节遇到了下面这个warning 意思:告诉你是黑盒子,而且他本来在设计里面就是黑盒子. 解决办法:点击图中的,会弹出一个代码文件,是声明这个IP核的文件,代码 ...
- c#控制台玩飞行棋游戏
using System; namespace Game{ class Program { //用静态字段模拟全局变量 public static int[] Maps = new int[100]; ...
- 【SpringBoot | Redis】SpringBoot整合Redis
SpringBoot整合Redis 1. pom.xml中引入Redis相关包 请注意,这里我们排除了lettuce驱动,采用了jedis驱动 <!-- redis的依赖 --> < ...
- 【PostMan】批量参数化的用法 之 text/csv
目的:批量参数化,单个循环多次使用不同的参数请求. 测试数据准备 新建txt文件,输入格式: 首行 --->参数名 其他行 --->测试数据(不同测试数据需要换行) 如下所示,Number ...
- 反汇编分析objc函数枢纽objc_msgSend
在分析objc_msgSend之前,先来搞清楚另一个问题. 函数是什么?可能会答 void foo(void) {} 像这样就是一个函数.或者函数包括函数原型和函数定义,是一段执行某样功能的机器代码. ...
- 《算法导论中文版》PDF 下载
电子版仅供预览及学习交流使用,下载后请24小时内删除,支持正版,喜欢的请购买正版书籍 在有关算法的书中,有一些叙述非常严谨,但不够全面:另一些涉及了大量的题材,但又缺乏严谨性.本书将严谨性和全面性融为 ...
- [转]shell 特殊字符
下面这篇博文对特殊字符总结的非常齐全.这里做一下mark.另外补充一些例子. https://blog.csdn.net/K346K346/article/details/51819236 假设我们定 ...
- dom4j的测试例子和源码详解(重点对比和DOM、SAX的区别)
目录 简介 DOM.SAX.JAXP和DOM4J xerces解释器 SAX DOM JAXP DOM解析器 获取SAX解析器 DOM4j 项目环境 工程环境 创建项目 引入依赖 使用例子--生成xm ...
- Apache Thrift 的魅力
WhyApacheThrift 因为最近在项目中需要集成进来一个Python编写的机器学习算法,但是我的后端主要使用的是SpringCloud技术栈. 于是面临着异构语言之间的通信实现方式的抉择. 因 ...