浅析Java中volatile关键字

认识volatile关键字

        Java中的volatile关键字用于修饰一个变量,当这个变量被多个线程共享时,这个变量的值如果发生更新,每个线程都能获取到最新的值。volatile关键字在多线程环境下还会禁止指令重排序,确保变量的赋值操作按照代码的顺序执行。需要注意是它不能保证变量操作的原子性。

       简而言之, Java中的volatile关键字保证可见性,禁止指令重排序(有序性)。

volatile与内存屏障

        volatile通过内存屏障来保证可见性与有序性。

认识内存屏障

         内存屏障(也称内存栅栏,内存栅障,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序。内存屏障其实就是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令,volatile实现了Java内存模型中的可见性和有序性,但volatile无法保证原子性。

        内存屏障能阻止屏障两边的指令重排序,在写数据时加入屏障,强制将线程私有工作内存的数据刷回主物理内存,读数据加入屏障,线程私有工作内存的数据会失效,重新到主物理内存中获取最新数据。

内存屏障指令

volatile可见性

        volatile关键字保证可见性,当一个线程修改了volatile修饰的变量的值,这个变量修改后的值会立即刷新到主内存中,其他线程读取volatile修饰的变量的值时,会重新将主内存中该变量最新的值读取到线程自己的工作内存中。

    // 验证volatile的可见性private static volatile boolean flag = true;public static void main(String[] args) {// 开启一个线程new Thread(()->{// 如果为false,跳出循环并输出打印内容// 线程中读取的时候,每次读取都会去主内存中读取共享变量最新的值,// 然后将其复制到工作内存while (flag){}System.out.println("flag标志更新,线程结束");}).start();try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}// 主线程中修改volatile修饰的变量的值true->false// 主线程中修改了工作内存中变量的副本,修改之后会立即刷新到主内存flag = false;}

运行结果: 

volatile的读写过程分析

Java内存模型中定义的8种工作内存与主内存之间的原子操作

read(读取)→load(加载)→use(使用)→assign(赋值)→store(存储)→write(写入)→lock(锁定)→unlock(解锁)

read:作用于主内存,将变量的值从主内存传输到工作内存,主内存到工作内存
load:作用于工作内存,将read从主内存传输的变量值放入工作内存变量副本中,即数据加载
use:作用于工作内存,将工作内存变量副本的值传递给执行引擎,每当JVM遇到需要该变量的字节码指令时会执行该操作
assign:作用于工作内存,将从执行引擎接收到的值赋值给工作内存变量,每当JVM遇到一个给变量赋值字节码指令时会执行该操作
store:作用于工作内存,将赋值完毕的工作变量的值写回给主内存
write:作用于主内存,将store传输过来的变量值赋值给主内存中的变量
由于上述只能保证单条指令的原子性,针对多条指令的组合性原子保证,没有大面积加锁,所以,JVM提供了另外两个原子指令:
lock:作用于主内存,将一个变量标记为一个线程独占的状态,只是写时候加锁,就只是锁了写变量的过程。
unlock:作用于主内存,把一个处于锁定状态的变量释放,然后才能被其他线程占用

volatile禁止指令重排序

认识重排序

        重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段,有时候会改变程序语句的先后顺序(不存在数据依赖关系,可以重排序;存在数据依赖关系,禁止重排序),但重排后的指令绝对不能改变原有的串行语义!这点在并发设计中必须要重点考虑!

        数据依赖性是指两个操作访问同一变量,且这两个操作中有一个为写操作,此时两操作间就存在数据依赖性。

重排序的分类和执行流程

编译器优化的重排序: 编译器在不改变单线程串行语义的前提下,可以重新调整指令的执行顺序。
指令级并行的重排序: 处理器使用指令级并行技术来讲多条指令重叠执行,若不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
内存系统的重排序: 由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是乱序执行。

volatile禁止指令重排序的行为

        当第一个操作为volatile读时,不论第二个操作是什么,都不能重排序。这个操作保证了volatile读之后的操作不会被重排到volatile读之前。

        当第二个操作为volatile写时,不论第一个操作是什么,都不能重排序。这个操作保证了volatile写之前的操作不会被重排到volatile写之后。

        当第一个操作为volatile写时,第二个操作为volatile读时,不能重排。

四大内存屏障插入

        写操作

        当前volatile写操作之前插入StoreStore屏障,禁止前面的普通写或volatile写操作与当前volatile写发送生重排序,并保证前面所有的普通写操作将数据已经刷新到主内存中。

        当前volatile写操作之前插入StoreLoad屏障,禁止后面的volatile读/写或普通写操作与当前volatile写重排序,并保证当前volatile写操作将数据刷新到内存中。

       读操作 

        当前volatile读操作之后插入LoadLoad屏障,禁止后面的普通读、volatile读和当前volatile都发生重排序。

        当前volatile读操作之后插入LoadStore屏障,禁止后面的普通写、volatile写和当前volatile都发生重排序。

volatile不保证原子性 

        原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。

        volatile变量的复合操作(如i++)不具有原子性,让我们从以下代码具体分析一下为什么 volatile变量的复合操作(如i++)不具有原子性。

        简单的代码示例如下,首先声明一个volatile修饰的int变量,在方法中对这个变量进行i++复合操作。

private volatile int i;public void add(){i++;}

        通过javap -c <classname>命令对class文件进行反编译,如下:

 public void add();Code:0: aload_01: dup2: getfield      #2                  // Field i:I 5: iconst_16: iadd7: putfield      #2                  // Field i:I10: return}

         从编译过后可以看到,i++被拆分了三步执行:执行getfield指令获取字段i;执行iadd指令对i进行+1操作;执行putfield指令把累加后的值写回。如果有另一线程在当前线程读取旧值和写回新值期间读取i的域值,那么另一线程就会与当前线程一起看到同一个值,并执行相同值的加1操作,这也就造成了线程不安全,值操作从i=2变成了i=1,如果要保证线程安全,对于add方法就必须使用synchronized修饰。

        再往jvm层面深入分析,volatile为了保证内存可见性对其中的一些指令做了特殊处理,read(读取) - load(加载) - use(使用)必须连续出现以保证读的可见性;assign(赋值) - store(存储) - write(写入)必须连续出现将值写入主内存中。在这两组关联操作中存有极小的一段真空期,有可能变量值会被其他线程读取,导致写数据的丢失。

        
         

        《深入理解Java虚拟机》提到,由于volatile只能保证可见性,在不符合以下两条规则的运算场景中,我们仍然要通过加锁(使用synchronized、java.util.concurrent中的锁或原子类)来保证原子性:

  • 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
  •  变量不需要与其他的状态变量共同参与不变约束。

 volatile的正确使用

复合运算赋值不能使用volatile关键字,会出现线程不安全问题。

    // 正确用法private volatile int count = 10;private volatile boolean flag = true;

可以作为状态标志,判断业务是否结束。

    private static volatile boolean flag = true;public static void main(String[] args) {new Thread(()->{while (flag){}System.out.println("flag标志更新,线程结束");}).start();try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}flag = false;}

当读远多于写,结合使用内部锁和 volatile 变量来减少同步的开销。

    private volatile int i;private int getVal(){return i; //利用volatile保证读取操作的可见性}public synchronized int increment(){return i++; //利用synchronized 保证复合操作的原子性} 

DCL双端锁实现单例

    private volatile static SafeDoubleCheckSingleton safeDoubleCheckSingleton;private SafeDoubleCheckSingleton(){}public static SafeDoubleCheckSingleton getInstance(){if (safeDoubleCheckSingleton == null){synchronized (SafeDoubleCheckSingleton.class){// 如果第一行不加volatile,在多线程环境下,//由于重排序,该对象可能还未完成初始化就被其他线程读取if (safeDoubleCheckSingleton == null){safeDoubleCheckSingleton = new SafeDoubleCheckSingleton();}}}return safeDoubleCheckSingleton;}

总结:

  • volatile是一种轻量级锁的实现,它针对的仅仅是共享变量,不会对线程加锁,更不会造成线程的阻塞。
  • volatile保证内存可见性,当变量值更新时,其他共享该变量的线程从主内存获取最新的值。
  • volatile通过内存屏障禁止指令重排序。
  • volatile对于复合赋值操作不保证原子性,如果需要保证原子性,还是需要使用synchronized、java.util.concurrent中的锁或原子类。

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

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

相关文章

VS Code + Python + Selenium 自动化测试基础-01

VS Code Python Selenium 自动化测试基础-01 让我们来讲一个故事为什么要写自动化开发前的准备工作牛刀小试开常用的web DriverAPI-定位元素id定位&#xff1a;find_element_by_id()name 定位&#xff1a;find_element_by_name()class 定位&#xff1a;find_element_by_class…

机器视觉技术与应用实战(平均、高斯、水平prewitt、垂直prewitt、水平Sobel、垂直Sobel、拉普拉斯算子、锐化、中值滤波)

扯一点题外话&#xff0c;这一个月经历了太多&#xff0c;接连感染了甲流、乙流&#xff0c;人都快烧没了&#xff0c;乙流最为严重&#xff0c;烧了一个星期的38-39度&#xff0c;咳嗽咳到虚脱。还是需要保护好身体&#xff0c;感觉身体扛不住几次连续发烧&#xff01;&#x…

MySQL---多表查询综合练习

创建dept表 CREATE TABLE dept ( deptno INT(2) NOT NULL COMMENT 部门编号, dname VARCHAR (15) COMMENT 部门名称, loc VARCHAR (20) COMMENT 地理位置 ); 添加dept表主键 mysql> alter table dept add primary key(deptno); Query OK, 0 rows affected (0.02 s…

对#多种编程语言 性能的研究和思考 go/c++/rust java js ruby python

对#多种编程语言 性能的研究和思考 打算学习一下rust 借着这个契机 简单的写了计算圆周率代码的各种语言的版本 比较了一下性能 只比拼单线程简单计算能力 计算十亿次循环 不考虑多线程 go/c/rust java js ruby python 耗时秒数 1:1:1:22:3:250:450 注&#xff1a;能启用则启…

web蓝桥杯真题--11、蓝桥知识网

介绍 蓝桥为了帮助大家学习&#xff0c;开发了一个知识汇总网站&#xff0c;现在想设计一个简单美观的首页。本题请根据要求来完成一个首页布局。 准备 开始答题前&#xff0c;需要先打开本题的项目代码文件夹&#xff0c;目录结构如下&#xff1a; ├── css │ └──…

Stream toList不能滥用以及与collect(Collectors.toList())的区别

Stream toList()返回的是只读List原则上不可修改&#xff0c;collect(Collectors.toList())默认返回的是ArrayList,可以增删改查 1. 背景 在公司看到开发环境突然发现了UnsupportedOperationException 报错&#xff0c;想到了不是自己throw的应该就是操作collection不当。 发…

spawn_group | spawn_group_template | linked_respawn

字段介绍 spawn_group | spawn_group_template 用来记录与脚本事件或boss战斗有关的 creatures | gameobjects 的刷新数据linked_respawn 用来将 creatures | gameobjects 和 boss 联系起来&#xff0c;这样如果你杀死boss&#xff0c; creatures | gameobjects 在副本重置之前…

测试覆盖与矩阵

4. Coverage - 衡量测试的覆盖率 我们已经掌握了如何进行单元测试。接下来&#xff0c;一个很自然的问题浮现出来&#xff0c;我们如何知道单元测试的质量呢&#xff1f;这就提出了测试覆盖率的概念。覆盖率测量通常用于衡量测试的有效性。它可以显示您的代码的哪些部分已被测…

【网络安全】【密码学】【北京航空航天大学】实验五、古典密码(中)【C语言实现】

实验五、古典密码&#xff08;中&#xff09; 实验目的和原理简介参见博客&#xff1a;古典密码&#xff08;上&#xff09; 一、实验内容 1、弗纳姆密码&#xff08;Vernam Cipher&#xff09; &#xff08;1&#xff09;、算法原理 加密原理&#xff1a; 加密过程可以用…

【跳槽面试】Redis中分布式锁的实现

分布式锁常见的三种实现方式&#xff1a; 数据库乐观锁&#xff1b;基于Redis的分布式锁&#xff1b;基于ZooKeeper的分布式锁。 本地面试考点是&#xff0c;你对Redis使用熟悉吗&#xff1f;Redis中是如何实现分布式锁的。 在Redis中&#xff0c;分布式锁的实现主要依赖于R…

对比一下HelpLook和Bloomfire知识库软件:谁更胜一筹?

在当今知识经济的浪潮中&#xff0c;知识库工具作为企业不可或缺的利器&#xff0c;对于提高工作效率、加强团队协作和优化员工培训等方面起着至关重要的作用。HelpLook和Bloomfire是众多知识库工具中的两款佼佼者&#xff0c;它们各自拥有独特的优势和特点。 一、HelpLook&…

解决 java.lang.NoClassDefFoundError: org/apache/poi/POIXMLTypeLoader 报错

在使用POI导出Excel表格的时候&#xff0c;本地运行导出没问题&#xff0c;但是发布到服务器后提示 “java.lang.NoClassDefFoundError: org/apache/poi/POIXMLTypeLoader” 下面是pom.xml中的配置 <dependency><groupId>org.apache.poi</groupId><art…

【算法详解】力扣162.寻找峰值

​ 目录 一、题目描述二、思路分析 一、题目描述 力扣链接&#xff1a;力扣162.寻找峰值 峰值元素是指其值严格大于左右相邻值的元素。 给你一个整数数组 nums&#xff0c;找到峰值元素并返回其索引。数组可能包含多个峰值&#xff0c;在这种情况下&#xff0c;返回 任何一个…

大创项目推荐 深度学习验证码识别 - 机器视觉 python opencv

文章目录 0 前言1 项目简介2 验证码识别步骤2.1 灰度处理&二值化2.2 去除边框2.3 图像降噪2.4 字符切割2.5 识别 3 基于tensorflow的验证码识别3.1 数据集3.2 基于tf的神经网络训练代码 4 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x…

gin介绍及helloworld

1. 介绍 Gin是一个golang的微框架&#xff0c;封装比较优雅&#xff0c;API友好&#xff0c;源码注释比较明确&#xff0c;具有快速灵活&#xff0c;容错方便等特点 对于golang而言&#xff0c;web框架的依赖要远比Python&#xff0c;Java之类的要小。自身的net/http足够简单&…

未来 AI 可能给哪些产业带来哪些进步与帮助?

AI时代如何要让公司在创新领域领先吗&#xff1f;拥抱这5种创新技能&#xff0c;可以帮助你的公司应对不断变化。包括人工智能、云平台应用、数据分析、 网络安全和体验设计。这些技能可以帮助你提高业务效率、保护公司知识资产、明智决策、满足客户需求并提高销售额。 现在就加…

Redis 面试题 | 01.精选Redis高频面试题

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

低代码技术杂谈

一、探讨低代码的定义 “Low-Code”是什么&#xff1f;身为技术人员听到这种技术名词&#xff0c;咱们第一反应就是翻看维基百科 或者其他相关技术论文&#xff0c;咱们想看维基百科的英文介绍&#xff1a; A low-code development platform (LCDP) provides a development env…

数据仓库简介

一、数仓概念 数据仓库&#xff0c;英文名称为Data Warehouse&#xff0c;可简写为DW或DWH。数据仓库&#xff0c;是为企业所有级别的决策制定过程&#xff0c;提供所有类型数据支持的战略集合。它是单个数据存储&#xff0c;出于分析性报告和决策支持目的而创建。 为需要业务…

套接字通信(附带单线程TCP套接字通信代码)

套接字-Socket 1. 概念 1.1 局域网和广域网 局域网&#xff08;LAN&#xff09;和广域网&#xff08;WAN&#xff09;是两种不同范围的计算机网络&#xff0c;它们用于连接多台计算机以实现数据共享和通信。 局域网&#xff08;LAN&#xff09;&#xff1a; 定义&#xff1…