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

目录

🚩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.rhkb.cn/news/290443.html

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

相关文章

Redis中的客户端(三)

客户端 身份验证 客户端状态的authenticated属性用于记录客户端是否通过了身份验证: typedef struct redisClient {// ...int authenticated;// ... } redisClient;如果authnticated的值为0&#xff0c;那么表示客户端未通过身份验证&#xff1b;如果authenticated的值为1&a…

【JDBC编程】基于MySql的Java应用程序中访问数据库与交互数据的技术

꒰˃͈꒵˂͈꒱ write in front ꒰˃͈꒵˂͈꒱ ʕ̯•͡˔•̯᷅ʔ大家好&#xff0c;我是xiaoxie.希望你看完之后,有不足之处请多多谅解&#xff0c;让我们一起共同进步૮₍❀ᴗ͈ . ᴗ͈ აxiaoxieʕ̯•͡˔•̯᷅ʔ—CSDN博客 本文由xiaoxieʕ̯•͡˔•̯᷅ʔ 原创 CSDN …

javaWeb项目-火车票订票信息系统功能介绍

项目关键技术 开发工具&#xff1a;IDEA 、Eclipse 编程语言: Java 数据库: MySQL5.7 框架&#xff1a;ssm、Springboot 前端&#xff1a;Vue、ElementUI 关键技术&#xff1a;springboot、SSM、vue、MYSQL、MAVEN 数据库工具&#xff1a;Navicat、SQLyog 1、Spring Boot框架 …

【微服务】认识Dubbo+基本环境搭建

认识Dubbo Dubbo是阿里巴巴公司开源的一个高性能、轻量级的WEB和 RPC框架&#xff0c;可以和Spring框架无缝集成。Dubbo为构建企业级微服务提供了三大核心能力&#xff1a; 服务自动注册和发现、面向接口的 远程方法调用&#xff0c; 智能容错和负载均衡官网&#xff1a;https…

如何使用Zabbix监控MySQL的MGR群集状态

MySQL的MGR&#xff08;MySQL Group Replication&#xff09;是MySQL官方提供的一种高可用性和高可靠性的集群解决方案。MGR通过使用基于组复制的方式&#xff0c;实现了多个MySQL实例之间的数据同步和故障转移&#xff0c;从而提供了自动故障恢复和负载均衡的功能。本文将介绍…

基于kalman的单目标追踪,以及demo测试(Python and C++)

一.卡尔曼滤波简单介绍 我们可以在任何含有不确定信息的动态系统中的使用卡尔曼滤波&#xff0c;对系统的下一步动作做出有根据的猜测。猜测的依据是预测值和观测值&#xff0c;首先我们认为预测值和观测值都符合高斯分布且包含误差&#xff0c;然后我们预设预测值的误差Q和观测…

Android 12中配置Selinux相关权限问题

1. 从logcat中过滤avc信息 avc: denied { read write } for comm"vendor.demo" name"ttyHW5" dev"tmpfs" ino610 scontextu:r:hal_gnss_default:s0 tcontextu:object_r:device:s0 tclasschr_file permissive1 avc: denied { ioctl } for comm&q…

python实战之PyQt5桌面软件

一. 演示效果 二. 准备工作 1. 使用pip 下载所需包 pyqt5 2. 下载可视化UI工具 QT Designer 链接&#xff1a;https://pan.baidu.com/s/1ic4S3ocEF90Y4L1GqYHPPA?pwdywct 提取码&#xff1a;ywct 3. 可视化UI工具汉化 把上面的链接打开, 里面有安装和汉化包, 前面的路径还要看…

基于Python微博舆情数据爬虫可视化分析系统(NLP情感分析+爬虫+机器学习)

这里写目录标题 基于Python微博舆情数据爬虫可视化分析系统(NLP情感分析爬虫机器学习)一、项目概述二、微博热词统计析三、微博文章分析四、微博评论分析五、微博舆情分析六、项目展示七、结语 基于Python微博舆情数据爬虫可视化分析系统(NLP情感分析爬虫机器学习) 一、项目概…

HarmonyOS 应用开发之Want的定义与用途

Want 是一种对象&#xff0c;用于在应用组件之间传递信息。 其中&#xff0c;一种常见的使用场景是作为 startAbility() 方法的参数。例如&#xff0c;当UIAbilityA需要启动UIAbilityB并向UIAbilityB传递一些数据时&#xff0c;可以使用Want作为一个载体&#xff0c;将数据传递…

OSPF GTSM(通用TTL安全保护机制)

目录 GTSM的定义 使用GTSM的目的 GTSM的原理 配置OSPF GTSM实例 组网需求 配置思路 操作步骤 1. 配置各接口的IP地址 2.配置OSPF基本功能 3.配置OSPF GTSM 4. 验证配置结果 GTSM的定义 GTSM&#xff08;Generalized TTL Security Mechanism&#xff09;&#xff0c;…

增长超500%!亚马逊卖疯的旅行箱,赛盈分销浅析今年企业出海布局方向!

箱包行业迎来了新的发展契机&#xff0c;一方面是在工艺与技术创新下&#xff0c;另一方面&#xff0c;旅游经济复苏的推动下&#xff0c;全球箱包行业取得飞速发展。 Euromonitor & 华泰研究针对2018-2028这十年间的箱包市场进行了调研&#xff0c;数据显示2023年全球箱包…

Java 面试宝典:什么是大 key 问题?如何解决?

大家好&#xff0c;我是大明哥&#xff0c;一个专注「死磕 Java」系列创作的硬核程序员。 本文已收录到我的技术网站&#xff1a;https://skjava.com。有全网最优质的系列文章、Java 全栈技术文档以及大厂完整面经 回答 Redis 大 key 问题是指某个 key 对应的 value 值很大&am…

路由的完整使用

多页面和单页面 多页面是指超链接等跳转到另一个HTML文件,单页面是仍是这个文件只是路由改变了页面的一部分结构. 路由的基本使用 使用vue2,则配套的路由需要是第3版. 1)下载vue-router插件 2)引入导出函数 3)new 创建路由对象 4)当写到vue的router后只能写路由对象,因此只…

Webpack常见插件和模式

目录 目录 目录认识 PluginCleanWebpackPluginHtmlWebpackPlugin自定义模版 DefinePlugin的介绍 ( 持续更新 )Mode 配置 认识 Plugin Loader是用于特定的模块类型进行转换&#xff1b; Plugin可以用于执行更加广泛的任务&#xff0c;比如打包优化、资源管理、环境变量注入等 …

国内IP切换软件:解锁网络世界的新钥匙

在数字化快速发展的今天&#xff0c;互联网已成为我们生活中不可或缺的一部分。然而&#xff0c;伴随着网络使用的深入&#xff0c;许多用户逐渐意识到&#xff0c;不同的IP地址可能会带来截然不同的网络体验。为了应对这一问题&#xff0c;国内IP切换软件应运而生&#xff0c;…

Java与Go:字符串转IP

在本文中&#xff0c;我们将了解如何将简单的对比Java和Go是如何将字符串解析为IP地址。 Java 在Java中&#xff0c;将字符串转换为IP地址最无脑的一个方法&#xff1a; import java.net.InetAddress; import java.net.UnknownHostException;public class Main {public stat…

深圳区块链交易所app系统开发,撮合交易系统开发

随着区块链技术的迅速发展和数字资产市场的蓬勃发展&#xff0c;区块链交易所成为了数字资产交易的核心场所之一。在这个快速发展的领域中&#xff0c;区块链交易所App系统的开发和撮合交易系统的建设至关重要。本文将探讨区块链交易所App系统开发及撮合交易系统的重要性&#…

数据库系统概论(超详解!!!) 第四节 关系数据库标准语言SQL(Ⅱ)

1.数据查询 SELECT [ ALL | DISTINCT] <目标列表达式>[&#xff0c;<目标列表达式>] … FROM <表名或视图名>[&#xff0c; <表名或视图名> ] … [ WHERE <条件表达式> ] [ GROUP BY <列名1> [ HAVING <条件表达式> ] ] [ ORDER BY…

【数据结构 | 图论】如何用链式前向星存图(保姆级教程,详细图解+完整代码)

一、概述 链式前向星是一种用于存储图的数据结构&#xff0c;特别适合于存储稀疏图&#xff0c;它可以有效地存储图的边和节点信息&#xff0c;以及边的权重。 它的主要思想是将每个节点的所有出边存储在一起&#xff0c;通过数组的方式连接&#xff08;类似静态数组实现链表…