概述

今天的主角是volatile变量,在讲它之前我会稍微提一些必要的前置概念,例如:Java内存模型及其相关操作;如果你对这一部分很熟悉了可以直接跳到第二部分。

一、内存模型

物理机内存模型

当物理机中进行计算任务时,处理器与物理内存之间的I/O交互效率低已经成为桎梏,所以引入了高速缓存,处理器计算期间主要与高速缓存进行交互,计算完成才会把计算结果写入主内存。

Java内存模型

Java内存模型存在的意义是 让Java程序在各种平台下都能达到一致的内存访问效果 ;它的主要目的是定义程序中变量访问规则,即从内存到变量到处理器计算,最后回到内存的过程中的一些规则; Java内存模型的目的是让JVM的内充分利用各种平台的物理资源(寄存器、高速缓存等等)。

下文提到的主内存都是Java内存模型概念中的主内存

Java内存模型中有如下的规定:

  • 所有变量必须保存在主内存中,从下图结构来说Java内存模型的主内存,类似物理机的主内存,但是它实际上只是Java堆的一部分。

  • 每条线程都拥有自己独立的工作内存空间,从下图的结构来说,工作内存类似物理机高速缓存的概念;但是实际上它对应的应该是虚拟机栈中的部分区域(内存自动管理阶段提到过栈帧,这个应该很容易理解)。

  • 工作内存中需要保存操作变量的主内存副本;

  • 线程对变量的操作只能在工作内存中进行,而不能直接操作主内存;

  • 不同线程不能访问对方的工作内存中的变量副本。

操作

为了更好支撑Java内存模型提出的规定,Java内存模型定义了如下的操作,它们都是原子操作:

  • lock(锁定):作用于主内存的变量,一个变量在同一时间只能一个线程锁定,该操作表示这条线成独占这个变量
  • unlock(解锁):作用于主内存的变量,表示这个变量的状态由处于锁定状态被释放,这样其他线程才能对该变量进行锁定
  • read(读取):作用于主内存变量,表示把一个主内存变量的值传输到线程的工作内存,以便随后的load操作使用
  • load(载入):作用于线程的工作内存的变量,表示把read操作从主内存中读取的变量的值放到工作内存的变量副本中(副本是相对于主内存的变量而言的)
  • use(使用):作用于线程的工作内存中的变量,表示把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时就会执行该操作
  • assign(赋值):作用于线程的工作内存的变量,表示把执行引擎返回的结果赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的字节码指令时就会执行该操作
  • store(存储):作用于线程的工作内存中的变量,把工作内存中的一个变量的值传递给主内存,以便随后的write操作使用
  • write(写入):作用于主内存的变量,把store操作从工作内存中得到的变量的值放入主内存的变量中

二、Volatile变量

上面介绍了Java内存模型,现在进入正题;Volatile变量—— 字面意思,它是变量,它是特殊的变量(废话,他不特殊为什么要单独讲它呢)。

volatile变量是Java虚拟机提供的最轻量级的同步机制,它拥有两个特性:

  • 定义为volatile的变量,拥有能够保证对所有线程的可见的特性(注意:只保证可见性);

  • 定义为volatile的变量,拥有 禁止指令重排序的特性

volatile修改变量后保证所有线程对其可见性

注意:

讨论可见性的语义环境:多个线程之间操作同一个volatile变量

所谓可见性:

  • 当多个线程执行一段代码(一个方法)包含一个volatile变量T值为X,假设线程:A、B、C一起执行,线程A改变T的值为Y,那么线程A对这个变量的修改,可能需要写回主内存,同时也需要立即通知其它也在执行这段代码(这个方法)的所有其它线程,把它们的工作内存中关于volate变量T的副本的值变更为新的值Y;

这里需要注意一个问题,上述Java内存模型概念中讲述的几个操作:

  • read(write)
  • load(store)
  • use(assign),

结合上述概念理解,如下过程

  1. 读过程
graph LR
A(主内存变量) -- read -->B(放入工作内存中 )-- load -->C(保存到工作内存的变量副本)-- use -->D(传给执行引擎)
  1. 写过程
graph RL
E(执行引擎计算结果)-- assign -->F(赋值变量副本)-- store -->G(变量进入主内存中 ) -- write -->H(把值写主内存变量)

既然volatile变量的特性描述说的是,只保证可见性,结合Java内存模型:

	公共的主内存,线程独占的工作内存;

为了要保证可见性,那么volatile变量每次在进行write操作时,不仅仅需要把计算后的值写入主内存分配的变量空间中,同时需要去操作其它各个线程的工作内存中的副本,需要把这些副本全部清理,修改为更新后的值。

细心的同学可能发现问题了,当废弃那些线程的副本时可能出现这样的情况:该volatile变量被废弃(其它线程对原值进行了修改)前已经被use操作给到了执行引擎,当前线程此次计算得到的计算结果是基于已经被废弃变量计算得到,如果将该基于废弃值得到的计算结果写入主内存,可能就会造成结果错误。(例如当多个线程对一个volatile变量进行累加操作时,就可能会发生错误)。

$ 应该还有汇编指令层面的更细粒度的细节说明volatile的线程安全问题。

所以说:volatile变量只保证可见性。

所以关于volatile变量的使用场景需要满足如下规则:

  • 计算结果与当前值(当前线程的副本变量)无关; 或者可以确保只有单一线程修改这个变量的值;

    例如:

    1. 表示开关的状态volatile变量T,有状态值a、b、c,此时此刻它是a,但是无法决定下一刻它该b还是c,此时volatile可以保证线程安全;
    2. 如果在累加操作中volatile变量T,此时它的值是5,那么通过计算,不论由哪一个线程执行,它的下一个状态的值则必须为6,这样的运算volatile变量无法保证线程安全。
  • 变量不需要与其它状态量共同参与不变约束;

volatile禁止指令重排序

有如下代码:

int a = 1,b = 2,c;//(1)
a = a +1;//(2)
c = a + 2;//(3)
b = b + 3;//(4)

当在汇编层面执行时

  • (1) 是声明,必须最先执行;
  • (3) 执行结果依赖(1),他们执行顺序不会被重排序
  • (4) 只依赖变量b,该指令可以被重排序提前执行,不影响最终结果。

实际上,保证禁止指令重排序的措施是插入一个内存屏障;

书中有这么一句话:

	对比一个变量,在volatile修饰前后的汇编代码发现,加了volatile修饰时,会多出一个Lock标记的指令。
  • 这个Lock标记指令,实际上就相当于一个内存屏障,它确保了指令不会被重排序,当执行到lock标记的语句时,它前面的语句必须已经执行完成,而它后面的语句也不能被重排序到它之前执行。
  • 它强制对缓存的修改立即写入主存
  • 如果是写操作,那么他会导致其它cpu中对应的缓存无效(可以任务它保证了当前修改对其它线程的可见性)。

Java内存模型关于volatile变量的特殊规则及其意义

操作指令顺序规则仅限于单个线程操作多个volatile变量;上文讨论可见性是在多个线程之间操作一个volatile变量,请注意区分前提条件。

假定T表示一个线程,X和Y分别表示两个volatile修饰的变量,当在X和Y上进行运算时,在进行read、load、use、assign、store和write操作的时候需要满足如下规则:

  1. 当T对 X 进行操作时,关于X的use操作的前一个操作必须是load操作,load操作的后一个动作必须是use,如此即可保证:每次使用volatile变量前,必须先从主内存刷新最新值,这个规定的目的是保证当前线程能看到别的线程对volatile变量的操作;

  2. 当T对X 的操作是assign时,它的后一个操作必须时store,操作store之前的动作必须是assign操作,由此可以保证,在工作内存中,每次对volatile变量修改后,必须立刻同步回主内存中,用于保证其它线程对当前线程的修改可见。。

  3. volatile变量不会被指令重排序优化,需要保证代码执行顺序与程序的顺序一致。

  • 动作A、B、C表示线程T对volatile变量X的操作,动作D、E、F表示线程T对volatile变量Y的操作;如果A先于D,那么C先于F;同理线程T对X和Y的:assign、store、write也满足此条件。
graph LR
A(A: read X) -->B(B: load X) -->C( C : use X)
graph LR
D(D: read Y) -->E(E: load Y) -->F( F : use Y)

《深入理解Java虚拟机》(七) volatile 变量的更多相关文章

  1. 深入理解Java虚拟机(七)——类文件结构

    Java的无关性 由于计算机领域中有很多操作系统和硬件平台同时在竞争,所以,很多编程语言的程序设计会与其运行的平台和操作系统产生耦合,这样就大大增加了程序员的工作,为了适应不同的平台,需要修改很多代码 ...

  2. 深入理解java虚拟机(6)---内存模型与线程 & Volatile

    其实关于线程的使用,之前已经写过博客讲解过这部分的内容: http://www.cnblogs.com/deman/category/621531.html JVM里面关于多线程的部分,主要是多线程是 ...

  3. jvm--深入理解java虚拟机 精华总结(面试)(转)

    深入理解java虚拟机 精华总结(面试)(转) 原文地址:http://www.cnblogs.com/prayers/p/5515245.html 一.运行时数据区域 3 1.1 程序计数器 3 1 ...

  4. 2018.4.23 《深入理解Java虚拟机:JVM高级特性与最佳实践》笔记

    一.Java内存区域与内存溢出 1.程序计数器是一块较小的内存空间,它可看作是当前线程所执行的字节码的行号指示器.字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令.各条线程 ...

  5. 2018.4.23 深入理解java虚拟机(转)

    深入理解java虚拟机 精华总结(面试) 一.运行时数据区域 Java虚拟机管理的内存包括几个运行时数据内存:方法区.虚拟机栈.本地方法栈.堆.程序计数器,其中方法区和堆是由线程共享的数据区,其他几个 ...

  6. 《深入理解Java虚拟机》学习笔记

    <深入理解Java虚拟机>学习笔记 一.走近Java JDK(Java Development Kit):包含Java程序设计语言,Java虚拟机,JavaAPI,是用于支持 Java 程 ...

  7. 《深入理解 Java 虚拟机》笔记整理

    正文 一.Java 内存区域与内存溢出异常 1.运行时数据区域 程序计数器:当前线程所执行的字节码的行号指示器.线程私有. Java 虚拟机栈:Java 方法执行的内存模型.线程私有. 本地方法栈:N ...

  8. 深入理解Java虚拟机第三版,总结笔记【随时更新】

    最近一直在看<深入理解Java虚拟机>第三版,无意中发现了第三版是最近才发行的,听说讲解的JDK版本升级,新增了近50%的内容. 这种神书,看懂了,看进去了,真的看的很快,并没有想象中的晦 ...

  9. 深入理解Java虚拟机--下

    深入理解Java虚拟机--下 参考:https://www.zybuluo.com/jewes/note/57352 第10章 早期(编译期)优化 10.1 概述 Java语言的"编译期&q ...

  10. 深入理解Java虚拟机--中

    深入理解Java虚拟机--中 第6章 类文件结构 6.2 无关性的基石 无关性的基石:有许多可以运行在各种不同平台上的虚拟机,这些虚拟机都可以载入和执行同一种平台无关的字节码(ByteCode),从而 ...

随机推荐

  1. [转帖]在Mysql中,什么是回表,什么是覆盖索引,索引下推?

    https://zhuanlan.zhihu.com/p/401198674 一.什么是回表查询? 通俗的讲就是,如果索引的列在 select 所需获得的列中(因为在 mysql 中索引是根据索引列的 ...

  2. ChatGPT学习之_shell脚本一例-查找版本冲突的第三方jar包

    ChatGPT学习之_shell脚本一例-查找版本冲突的第三方jar包 背景 自从换了Java后 产品里面用到了非常多的第三方组建,也就是很多jar包. 产品内的研发规范要求, jar包不能带版本号和 ...

  3. 【转帖】Linux性能优化(十三)——CPU性能测试

    一.CPU上下文切换测试场景 使用sysbench模拟多线程调度: sysbench --threads=10 --time=300 threads run 使用vmstat查看CPU上下文切换: c ...

  4. [转帖]总结:Tomcat的IO模型

    一.介绍 对于 linux 操作系统,IO 多路复用使用的是 epoll 方式,对于 windows 操作系统中 IO 多路复用使用的是 iocp 方式,对于 mac 操作系统 IO 多路复用使用的是 ...

  5. js中toFixed 并不是你想的那样进行四舍五入

    toFixed 的简单介绍 toFixed() 方法可把 Number 类型的数字通过四舍五入为指定小数位的字符串.(将数字类型转化为字符串类型) 也就是说toFixed只能够处理数字类型的. 字符串 ...

  6. 关于async函数的错误处理

    1. 关于async函数的错误处理 有些时候,我们请求的接口可能会报错: 从而导致后面的代码无法去执行: 这样就会造成页面上某些状态出错! 那么怎么样才能 既能捕获到错误 还能让代码往后面执行呢 2. ...

  7. vue数据更新后在视图上不响应

    一.vue如何追踪变化 当你把一个普通的JS对象传给vue实例的data选项时, vue将遍历此对象的所有属性, 并使用 Object.defineProperty 把这些属性全部转为 getter/ ...

  8. 【JS 逆向百例】网洛者反爬练习平台第五题:控制台反调试

    关注微信公众号:K哥爬虫,持续分享爬虫进阶.JS/安卓逆向等技术干货! 声明 本文章中所有内容仅供学习交流,抓包内容.敏感网址.数据接口均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后 ...

  9. 微信小程序-页面跳转wxAPI

    官方文档地址:https://developers.weixin.qq.com/miniprogram/dev/api/route/wx.navigateTo.html wx.navigateTo(O ...

  10. GaussDB(for MySQL)剪枝功能,让查询性能提升70倍!

    作者,祝青平,华为云数据库内核高级工程师.擅长数据库优化器内核研发,9年数据库内核研发经验,参与多个TP以及AP数据库的研发工作. 近日,华为云数据库社区下面有这样一条用户提问留言:请问,如何通过My ...