【JavaEE初阶系列】——带你了解volatile关键字以及wait()和notify()两方法背后的原理

news/2024/4/29 10:56:04/文章来源:https://blog.csdn.net/m0_74438843/article/details/136952927

目录

🚩volatile关键字

🎈volatile 不保证原子性

🎈synchronized 也能保证内存可见性

🎈Volatile与Synchronized比较

🚩wait和notify

🎈wait()方法

💻wait(参数)方法

🎈notify()方法

🎈解决线程饿死方式

🎈notifyAll()方法


🚩volatile关键字

volatile修饰的变量 能保证内存可见性
在学习Java多线程编程Q里, volatile 关键字 保证内存可见性的要点时,看到网上有些资料是这么说的: 线程修改一个变量,会把这个变量先从主内存读取到工作内存;然后修改工作内存中的值,最后再写回到主内存。
 
内存可见性问题 的表述为: t1 频繁读取主内存(内存),效率比较低,就被优化成直接读自己的工作内存(cpu寄存器);t2 修改了主内存的结果,但由于 t1 没有读主内存,导致修改不能被识别到,最终导致代码出现bug。
计算机运行的程序/代码,经常要访问数据。
这些依赖的数据 往往会存储在内存中去~(定义一个变量,变量就是在内存中)
  • cpu使用这个变量的时候,就会把这个内存中的数据,先读出来,放到cpu的寄存器中。再参与运算(load) 
  • cpu读取内存的这个操作,其实非常慢!(快,慢 都是相对的)
  • cpu进行大部分操作,都很快,一旦操作到读/写内存,此时的速度就降下来了。
  • 读内存 >> 读硬盘 快几千倍,上万倍
  • 读寄存器 >> 读内存  快几千倍,上万倍
结论:为了解决上述的问题,提高效率,此时编译器,就可能对代码做出优化,把一些本来要读内存的操作,优化成读取寄存器。减少读内存的次数,也就可以提高整体程序的效率了。

此时我们进行下面的代码段,首先我们默认isQuit是0,t2线程输入isQuit的值,t1线程中如果isQuit一直都是0的话,一直死循环,如果isQuit !=0的时候,我们才判断t1线程结束。

public class test1 {private static int isQuit=0;public static void main(String[] args) {Thread t1=new Thread(()->{while (isQuit==0){}System.out.println("t1线程退出");});Thread t2=new Thread(()->{System.out.println("请输入isQuit: ");Scanner scanner=new Scanner(System.in);//一旦用户输入的值,不为0,此时就会使t1线程执行结束isQuit=scanner.nextInt();});t1.start();t2.start();}
}

此时我输入isQuit是1,然后t1线程理想的结果是跳出循环,然后输出t1线程退出。

但是,当我真正输入1的时候,此时t1线程并没有结束,t1线程正在执行,并且是RUNNABLE状态。很明显,实际效果和预期效果是不一样的,由于多线程引起的,也是线程安全的问题。之前是俩个线程同时修改同一个变量,现在是一个线程读,一个线程修改,也可能出现问题。此处 的问题,就是"内存可见性"情况引起的。

  • 1> load读取内存中的isQuit的值到寄存器中
  • 2>通过cmp指令比较寄存器的值是否是0,决定是否要继续循环

因为读寄存器的速度>>读内存的速度,所以短时间内,就会进行大量的循环,也就是进行大量的load和cmp操作。此时,编译器jvm就发现了,虽然进行了这么多次load但是load出现的结果都是一样的,并且load操作又非常的消耗时间,一次load花的时间相当于上万次的cmp了。所以编译器就做出了优化,只是第一次循环的时候,才读内存,后面都不再读内存了,而是直接从寄存器中,取出isQuit即可。

原本是load读取到内存中到寄存器中,然后cmp指令在寄存器中比较,依次来,但是由于cmp指令速度太快了大于load操作。编译器的初心是好的,它是希望提高程序的效率,但是提高效率的前提是保证逻辑不变。此时由于修改isQuit代码是另一个线程的操作,编译器没有正确的判定,所以编译器以为没人修改isQuit,就做出了上述优化,也就进一步引起了bug了。

后续 t2线程修改isQuit之后,t1感知不到isQuit变量的变化(感知不到内存的变化),所以一直比较,一直死循环。

解决上述 这个问题,volatile就是解决方案,在多线程的环境下,编译器对于是否要进行这样的优化,判定不一定准。就需要程序员通过volatile关键字告诉编译器,你不要优化(优化是算的快,但是算不准)。

这也告诉我们编译器也不是万能的,也会有一些短板的地方,此时就需要程序员进行补充了。只需要给isQuit加volatile关键字修饰,此时编译器自然就会禁止上述优化过程。

此时,程序就可以顺利退出了。


但是还有一种方式,就是让cmp指令比较的速度变慢,让处于休眠状态,这时候,load操作的开销就不大了,优化就没必要了。

public class test1 {private static int isQuit=0;public static void main(String[] args) {Thread t1=new Thread(()->{while (isQuit==0){try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("t1线程退出");});Thread t2=new Thread(()->{System.out.println("请输入isQuit: ");Scanner scanner=new Scanner(System.in);//一旦用户输入的值,不为0,此时就会使t1线程执行结束isQuit=scanner.nextInt();});t1.start();t2.start();}
}

但是我们编译器什么时候对其进行优化这是说不清楚的事情,所以用volatile修饰是最靠谱的事情。


🎈volatile 不保证原子性

volatile synchronized 有着本质的区别 . synchronized 能够保证原子性 , volatile 保证的是内存可见性. 代码示例
这个是最初的演示线程安全的代码 .
  • increase 方法去掉 synchronized
  • count 加上 volatile 关键字.
class Counter{volatile public int count = 0;void increase() {count++;}
}public  class Test2 {public static void main(String[] args) throws InterruptedException {Counter counter = new Counter();Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase();}});t1.start();t2.start();t1.join();t2.join();System.out.println(counter.count);}}

此时的结果依旧不是1w。所以可以证明volatile是不能保证原子性的。


🎈synchronized 也能保证内存可见性

synchronized 既能保证原子性 , 也能保证内存可见性 . 对上面的代码进行调整:
  • 去掉 isQuit  volatile
  • t1 的循环内部加上 synchronized, 并借助object对象加锁.
public class Test3 {private static  int isQuit=0;public static void main(String[] args) {Object object=new Object();Thread t1=new Thread(()->{while (true){synchronized (object){if(isQuit!=0){break;}}}System.out.println("t1线程退出");});Thread t2=new Thread(()->{System.out.println("请输入isQuit: ");Scanner scanner=new Scanner(System.in);//一旦用户输入的值,不为0,此时就会使t1线程执行结束isQuit=scanner.nextInt();});t1.start();t2.start();}
}


🎈Volatile与Synchronized比较

  • Volatile是轻量级的synchronized,因为它不会引起上下文的切换和调度,所以Volatile性能更好。
  • Volatile只能修饰变量,synchronized可以修饰方法,静态方法,代码块。
  • Volatile对任意单个变量的读/写具有原子性,但是类似于i++这种复合操作不具有原子性。而锁的互斥执行的特性可以确保对整个临界区代码执行具有原子性。
  • 多线程访问volatile不会发生阻塞,而synchronized会发生阻塞。
  • volatile是变量在多线程之间的可见性,synchronize是多线程之间访问资源的同步性。

🚩wait和notify

我们之前学的join方法,它是让一个线程执行完之后,再执行另一个线程,这就是哪个线程调用了join,哪个线程就阻塞。

join控制的是结束的先后顺序,但是理想情况下,是希望在结束前,先后顺序的控制。由于线程之间是抢占式执行的, 因此线程之间执行的先后顺序难以预知. 但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序。

就比如在打篮球的时候,球场上每个运动员都是独立的"执行流",可以认为是一个"线程“,而完成一个具体的进攻得分动作,则需要多个运动员相互配合,按照一定的顺序执行一定的动作,线程1先"传球",线程2才能"扣球",然后线程1就等待时机。阻塞等待又被唤醒继续执行,这种操作就是再一直的执行程序,而不是先完成一个线程之后第二线程再完成,然后就结束了。

比如,t1和t2两个线程,希望t1先执行,执行的差不多了,在让t2来干.就可以让t2先wait(阻塞,主动放弃cpu),等t1执行的差不多了,再通过notify来通知t2,把t2唤醒,让t2接着干.

  • 使用join,则必须让t1彻底执行完,t2才能运行.如果是希望t1先干50%的活,就让t2开始行动,此时join无能为力.
  • 使用sleep,指定一个休眠的时间.但是t1执行完这些代码,到底花了多少时间,不好估计.
  • 使用wait和notify可以更好的解决上述的问题.

🎈wait()方法

wait进行阻塞, 某个线程调用wait方法,就会进入阻塞(无论是通过哪个对象wait的),此时就处在WAITING状态.

wait,notify和notifyAll这几个类都是Object类的方法,所以Java里随便一个对象,都可以有这三种方法.

  • wait() / wait(long timeout): 让当前线程进入等待状态.
  • notify() / notifyAll(): 唤醒在当前对象上等待的线程.
注意: wait, notify, notifyAll 都是 Object 类的方法.

注意,wait也需要这个异常,这个异常,很多带有阻塞功能的方法都带.这些方法都是可以被interrupt方法通过这个异常给唤醒的。后续会再阻塞队列中讲到。

此时抛出异常, 非法的监视器状态异常。监视器是synchronized。
我们首先要知道 wait在执行的时候要进行三步骤:
  • 1.释放当前的锁
  • 2.让线程进入阻塞
  • 3.当线程被唤醒的时候,重新获取到锁
但是首先我们在执行这段代码的时候,我们是释放谁的锁呢?synchronized加锁其实就是把对象头的标记进行操作了, 释放锁的前提是加锁。就比如找工作,你再学校中,学校不让我出去找工作,所以我就不找了,但是前提是你得找到工作了,你才有选择去不去的选择,没有拿到offer之前就想着拒绝去。还比如,一个男生追一个女生,还没追到手都想到了以后和他在一起后孩子的名字都想好了,前提是你得追到手啊,追不到手你取再多名字都不行。
所以我们要让wait放进synchronized锁里面调用,这样就可以确保wait拿到了锁,你才有释放锁的能力。
public class wait_notify_test {public static void main(String[] args) throws InterruptedException {Object object=new Object();synchronized (object){System.out.println("wait之前");// 将wait放到synchronized里面调用,保证确实拿到了这个锁,才能释放锁object.wait();System.out.println("wait之后");}}
}

此时没有报错现象,打印了wait之前代码后,调用wait之后,程序就进入了阻塞状态,因为wait()这种方法无参的是保持死等待的 ,只有等到notify()唤醒才可以执行wait()方法后的程序。
wait 结束等待的条件:
  • 其他线程调用该对象的 notify 方法.
  • wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).
  • 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常.

💻wait(参数)方法

wait除了默认的无参数版本之外,还有一个带参数的版本。带参数的版本就是指定超时时间,避免wait无休止的等待时间,等到一定的时间,就不会再等待了。

public class notify_wait_test2 {public static void main(String[] args) {Object object=new Object();Thread t1=new Thread(()->{synchronized (object){System.out.println("wait之前");try {object.wait(5000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("wait之后");}});t1.start();}
}


🎈notify()方法

我们设计下面的代码,线程t1进行wait(),线程t2进行唤醒wait(),因为wait的唤醒需要其他线程调用该对象的notify方法.首先t2线程睡眠3s,让ti线程阻塞等待一会,之后notify()唤醒了wait(),就开始进行wait()方法后的程序了。

public class notify_wait_test2 {public static void main(String[] args) {Object object=new Object();Thread t1=new Thread(()->{synchronized (object){System.out.println("wait之前");try {object.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("wait之后");}});Thread t2=new Thread(()->{try {Thread.sleep(3000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (object){System.out.println("进行通知之前");object.notify();}});t1.start();t2.start();}
}


🎈解决线程饿死方式

就拿ATM机来举例子,1号滑稽进去之后,就要取钱,发现ATM里面没钱了,取不了,当1号滑稽释放锁之后,此时其他滑稽开始尝试竞争这个锁,但是刚才的1号滑稽,也能参与竞争这个锁。

所以每次都是1号滑稽进去之后,取不了钱,然后又进去,又取不了钱,又进去,其他线程等待锁,都是阻塞状态,没在cpu上执行,当1号滑稽释放锁之后,这些滑稽想去cpu,还需要有一个系统调度的过程,而1号自身,已经在cpu上执行,没有这个调度的过程了,1号近水楼台先得月,更容易拿到锁得。这就导致了一直是1号滑稽进入ATM机中,循环此处,每次都是取不了钱,但是还是1号滑稽占用了这个线程,这样长此以往就形成了”线程饿死“的状态。

针对上述情况,同样可以使用wait和notify解决,让1号滑稽,在发现没钱的时候,就进行wait(wait内部本身就会释放锁,并且进入阻塞),1号滑稽就不会参与后续的锁竞争了,也把锁释放出来让别人获取。就给其他的滑稽提供了机会了。
wait的过程是等,等待运钞车把钱送过来,运钞车的线程就相当于调用了notify唤醒的线程,这个等的过程,是阻塞的,但是不会占据cpu。

🎈notifyAll()方法

        notify方法只是唤醒某一个等待线程. 使用notifyAll方法可以一次唤醒所有的等待线程. 范例:使用notifyAll() 方法唤醒所有等待线程 , 在上面的代码基础上做出修改
调用wait不一定就只有一个线程调用,N个线程都可以调用wait,此时,当有多个线程调用的时候,这些线程都会进入阻塞状态。

        唤醒的方式就有2种方法。notifyAll唤醒的时候,wait要涉及到一个重新获取锁的过程,也是需要串行执行的而并不是并行执行。虽然提供了notifyAll,相比之下notify更可控,用的更多一些。


🚩wait 和 sleep 的对比(面试题)

其实理论上 wait sleep 完全是没有可比性的,因为一个是用于线程之间的通信的,一个是让线程阻塞一段时间。
唯一的相同点就是都可以让线程放弃执行一段时间.
当然为了面试的目的,我们还是总结下:
  • 1. wait 需要搭配 synchronized 使用. sleep 不需要.
  • 2. wait Object 的方法 sleep Thread 的静态方法

人拥有可以反复尝试的自由,也拥有停步或者回头的权利。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_1028105.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

8.HelloWorld小案例

文章目录 一、Java程序开发运行流程如何理解编译&#xff1f; 二、HelloWorld案例的编写1、新建文本文档文件&#xff0c;修改名称为HelloWorld.java。2、用记事本打开HelloWorld.java文件&#xff0c;输写程序内容。代码要跟我编写的完全保持一致。3、ctrl s 保存&#xff0c…

JavaScript基础练习题之计算数组元素的和与平均值

一、如何使用JavaScript计算数组元素的和与平均值&#xff1f; 二、正确的源程序 <!DOCTYPE html> <html><head><meta charset"UTF-8"><title>计算数组元素的和与平均值</title></head><body><h1>计算数组元…

AIGC重塑金融 | 大模型在金融行业的应用场景和落地路径

作者&#xff1a;林建明 来源&#xff1a;IT阅读排行榜 本文摘编自《AIGC重塑金融&#xff1a;AI大模型驱动的金融变革与实践》&#xff0c;机械工业出版社出版 目录 01 大模型在金融领域的 5 个典型应用场景 02 大模型在金融领域应用所面临的风险及其防范 03 AIGC 技术的科…

蓝桥杯 - 小明的背包3(多重背包)

解题思路&#xff1a; 动态规划 多重背包问题需要在01背包问题&#xff08;不重复&#xff09;的基础上多加一层循环进行遍历&#xff0c;并且dp[ j ]的式子也需要修改 import java.util.Scanner;public class Main {public static void main(String[] args) {Scanner scan …

CSS及javascript

一、CSS简介 css是一门语言&#xff0c;用于控制网页的表现。 cascading style sheet:层叠样式表 二、css的导入方式 css代码与html代码的结合方式 &#xff08;1&#xff09;css导入html有三种方式&#xff1a; 1.内联样式&#xff1a;<div style"color:red&quo…

element-ui autocomplete 组件源码分享

紧接着 input 组件的源码&#xff0c;分享带输入建议的 autocomplete 组件&#xff0c;在 element-ui 官方文档上&#xff0c;没有这个组件的 api 目录&#xff0c;它的 api 是和 input 组件的 api 在一起的&#xff0c;看完源码之后发现&#xff0c;源码当中 autocomplete 组件…

MySQL中的基本SQL语句

文章目录 MySQL中的基本SQL语句查看操作创建与删除数据库和表修改表格数据库用户管理 MySQL中的基本SQL语句 查看操作 1. 查看有哪些数据库 show databases; 2.切换数据库 use 数据库名;比如切换至 mysql数据库 use mysql;3.查看数据库中的表 show tables;4.查看表中…

新能源汽车驱动电机振动噪音分析

驱动电机示例图 驱动电机的噪声主要分为空气动力噪声、电磁噪声和机械噪声。其中在高速运转时空气动力噪声是主要噪声&#xff0c;中低速运转时电磁噪声为主要噪声。 1、空气动力噪声&#xff1a; 空气噪声主要由于风扇转动&#xff0c;使空气流动、撞击、摩擦而产生&#x…

ARM-按键中断实验

代码 #include "stm32mp1xx_gic.h" #include "stm32mp1xx_exti.h" extern void printf(const char *fmt, ...); unsigned int i 0; void do_irq(void) {//获取要处理的中断的中断号unsigned int irqnoGICC->IAR&0x3ff;switch (irqno){case 99:pr…

2023年后端面试总结

备注&#xff1a;这篇文章是我在2023年年初在自己的网站上写的&#xff0c;最近在迁移技术文章&#xff0c;我感觉这个也是和咱程序员相关&#xff0c;所以今天就决定把它迁移过来。 .......................................................................分割线..........…

AJAX-综合

文章目录 同步代码和异步代码回调函数地狱解决回调函数地狱Promise-链式调用async函数和awaitasync函数和await-捕获错误 事件循环宏任务与微任务Promise.all静态方法 同步代码和异步代码 同步代码&#xff1a;逐步执行&#xff0c;需原地等待结果后&#xff0c;才继续向下执行…

后端常问面经之计算机网络

一台机器理论上能创建多少条TCP连接&#xff1f; Linux每维护一条TCP连接都要花费内存资源的&#xff0c;每一条静止状态&#xff08;不发送数据和不接收数据&#xff09;的 TCP 连接大约需要吃 3.44K 的内存&#xff0c;那么 8 GB 物理内存的服务器&#xff0c;最大能支持的 …

微服务day07 -- 搜索引擎 ( 数据聚合 + 自动补全 + 数据同步 + ES集群 )

1.数据聚合 聚合&#xff08;aggregations&#xff09;可以让我们极其方便的实现对数据的统计、分析、运算。例如&#xff1a; 什么品牌的手机最受欢迎&#xff1f; 这些手机的平均价格、最高价格、最低价格&#xff1f; 这些手机每月的销售情况如何&#xff1f; 实现这些…

R语言基础入门

1.保存或加载工作空间 改变工作目录——进行文件读写&#xff0c;默认去指定文件进行操作。&#xff08;使用R时&#xff0c;最好先设定工作目录&#xff08;setwd(),getwd()&#xff09;&#xff09; setwd(“工作文件路径”)&#xff1a;建立工作目录 getwd&#xff08;&…

OpenGL 实现“人像背景虚化“效果

手机上的人像模式,也被人们称作“背景虚化”或 ”双摄虚化“ 模式,也称为 Bokeh 模式,能够在保持画面中指定的人或物体清晰的同时,将其他的背景模糊掉。突出画面的主体部分,主观上美感更强烈。 人像模式的一般实现原理是,利用双摄系统获取景深信息,并通过深度传感器和图…

C语言与sqlite3入门

c语言与sqlite3入门 1 sqlite3数据类型2 sqlite3指令3 sqlite3的sql语法3.1 创建表create3.2 删除表drop3.3 插入数据insert into3.4 查询select from3.5 where子句3.6 修改数据update3.7 删除数据delete3.8 排序Order By3.9 分组GROUP BY3.10 约束 4 c语言执行sqlite34.1 下载…

计算机毕业设计Hadoop+Spark+Hive租房推荐系统 贝壳租房数据分析 租房爬虫 租房可视化 租房大数据 大数据毕业设计 大数据毕设 机器学习

毕业技术方向调查表 姓名&#xff1a; 李昌福 课题方向 房无忧房屋租赁平台 开发语言&#xff1a; Java 前端框架&#xff1a; VUE 数据库&#xff1a; MySQL 服务器端 框架&#xff1a; SpringCloud 其他技术&#xff1a; Hadoop、HDFS 方向…

HTML网站的概念

目录 前言&#xff1a; 1.什么是网页&#xff1a; 2.什么是网站&#xff1a; 示例&#xff1a; 3.服务器&#xff1a; 总结&#xff1a; 前言&#xff1a; HTML也称Hyper Text Markup Language&#xff0c;意思是超文本标记语言&#xff0c;同时HTML也是前端的基础&…

二叉树初阶数据结构C

文章目录 一、树的概念及结构&#xff1f;1.树的概念2.树的相关概念3.树的表示4.树在实际生活的应用&#xff08;表示文件系统的目录树结构&#xff09; 二、二叉树的概念及结构1.概念2.特殊的二叉树3.二叉树的性质4.二叉树的存储结构 三、二叉树链式结构的实现(顺序结构之前讲…

maven 依赖机制

安全工程师为啥关注maven依赖 log 4j事件之后&#xff0c;大家开始更加关注开源组件安全漏洞这个事。纷纷引入SCA 软件成分分析工具来识别项目中存在的开源组件和漏洞。 在sca工具扫描之后&#xff0c;会报出一大堆组件&#xff0c;review这个事就是安全团队投入时间来研判了…