概述

今天的主角是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. [转帖]kingbase(人大金仓)的一些常用表操作语句

    包括 1)创建表 2)删除表 3)加字段 4)字段换名 5)字段改类型 6)字段添加注释 7)修改字段为自增类型 8)增加主键 9)查看模式下的表 一.创建和删除表 DROP TABLE IF EXI ...

  2. [转帖]Linux之bash反弹shell原理浅析

    环境 攻击机:kali            ip:192.168.25.144 靶    机:centos      ip:192.168.25.142 过程 kali 监听本地8888端口 靶机 ...

  3. [转帖]s3fs

    https://github.com/s3fs-fuse/s3fs-fuse s3fs allows Linux, macOS, and FreeBSD to mount an S3 bucket v ...

  4. [转帖]编译安装goofys挂载Scaleway免费75G对象存储

    日常•2022年5月29日   goofys编译 goofys是一个开源的使用Go编写的s3存储桶挂载工具,主打高性能.由于使用Go编写,没有用到什么特别的依赖,自己编译也很容易.截止2022.5.2 ...

  5. [转帖]【JVM】Java内存区域与OOM

    引入 Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的"高墙",墙外面的人想进去,墙里面的人却想出来. Java虚拟机运行时数据区 如图所示 1.程序计数器(线程私有 ...

  6. ClickHouse(14)ClickHouse合并树MergeTree家族表引擎之VersionedCollapsingMergeTree详细解析

    目录 建表语法 使用场景 合并算法 使用例子. 资料分享 参考文章 VersionedCollapsingMergeTree引擎继承自MergeTree并将折叠行的逻辑添加到合并数据部分的算法中.Ve ...

  7. Gorm 关联关系介绍与基本使用

    目录 一 Belongs To(一对一) 1.1 Belongs To 1.2 重写外键 1.3 重写引用(一般不用) 1.4 Belongs to 的 CRUD 1.5 预加载 1.6 外键约束 二 ...

  8. Asp.Net MVC中点击按钮导出Excel

    一.Excel导出帮助类,要安装包NPOI 1 using NPOI.HSSF.UserModel; 2 using NPOI.SS.UserModel; 3 using System; 4 usin ...

  9. SqlSugar常见问题汇总

    1.已有打开的与此 Command 相关联的 DataReader,必须首先将它关闭. There is already an open DataReader associated with this ...

  10. 你的代码已被埋在北极冰雪之下,保存千年——GitHub北极代码保险库

    GitHub存档计划:北极代码保险库 在2019 GitHub 宇宙大会(GitHub Universe 2019)上,他们提到了一个问题,1000年后的软件会是什么样?人类会是什么样子?对此我们只能 ...