《深入理解Java虚拟机》(七) volatile 变量

学习JVM有一段时间了,现在开始进入并发阶段的学习;之前集中写博客是内存自动管理阶段,这部分有一个相当重要的问题JVM调优,这不仅增长见闻,与工作也有很大关系。之后的部分:虚拟机执行子系统、程序编译与代码调优就只是默默学习了一下概念。现在到了并发了,我知道是时候写博客记录下我的“李姐”了。没写博客的部分不是不重要,而是我还没实际触碰到,必然有一天那些忽视的东西还会重新捡起来。

概述

今天的主角是volatile变量,在讲它之前我会稍微提一些必要的前置概念,肯定不会讲得多详细;可以是为了建立起概念,也可以是为了方便复习时能过快速想起这部分内容,例如:内存模型和相关操作。

一、内存模型

物理机内存模型

当物理机中进行计算任务时,因为处理器与物理内存之间的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变量T值为X,假设线程:A、B、C一起执行,线程A改变T的值为Y,那么线程A对这个变量的修改,可能需要写回主内存,同时也需要立即通知其它也在执行这段代码(这个方法)的所有其它线程,把它们的工作内存中关于volate变量T的副本的值变更为新的值Y;

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

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

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

  1. 读过程
read
load
use
主内存中的变量
放入工作内存中
保存到工作内存的变量副本
传给执行引擎
  1. 写过程
assign
store
write
执行引擎计算结果
赋值变量副本
进入主内存中
把值写主内存变量

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

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

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

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

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

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

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

    例如:

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

禁止指令重排序

普通变量仅会保证在该方法执行过程中,所有依赖赋值结果的地方,都能获取到正确的结果,但是不能保证变量赋值的顺序与程序代码中的执行顺序一致;

  1. 假设线程A执行一个方法:做事情a,做完后通过动作Y标记事情a已做。
  2. 线程B时刻不停的访问A,查看Y标记,如果Y标记已做,B就做事情 b。
  3. 非并发语义下:A退出方法前,需要做事情a,还需要完成动作Y去标记a已做,抛开其它的语义环境,可以把方法拆开,A可以在等待a执行完之前就把Y进行标记,当a执行完,A退出当前方法;
  4. 上一步中,虽然执行顺序变了,但是非并发环境下不会出现任何问题,还节约了执行时间。
  5. 上述现象可以对应于:编译器的指令优化重排序,可以理解成执行a的时候有空余的资源操作Y,所以发生了指令重排序。
  6. 在并发环境下,a未完成,由于指令重排序,Y已标记,B查询到Y完成,执行b,但是如果b操作依赖a,那么就会发生错误。

为了避免上述问题,可以把Y定义为volatile 变量,变量声明为volatile时,禁止指令重排序,此时对于线程A来说,必须先执行a,再标记Y,不能进行指令重排序。

volatile禁止指令重排序是通过内存屏障实现的,它要求编译器进行指令重排序时,不能把屏障后面的指令重排序到屏障之前,这样才能保证并发语义环境下其执行结果的可靠性;

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

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

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

  1. 在工作内存中,每次使用volatile变量前,必须先从主内存刷新最新值,这个规定的目的是保证当前线程能看到别的线程对volatile变量的操作。
  • 当T对X进行操作时,关于X的use操作的前一个操作必须是load操作,load操作之后必须是read操作,而load操作的前一个操作必须是read操作。T对volatile变量的read、load、use操作相关联且必须顺序出现;
  1. 在工作内存中,每次对volatile变量修改后,必须立刻同步回主内存中,用于保证其它线程对当前线程的修改可见。
  • 当T对X 的操作是assign时,它的后一个操作必须时store,store操作之前必须是assign操作,assign与store、write相关联,必须连续且一起出现。
  1. volatile变量不会被指令重排序优化,需要保证代码执行顺序与程序的顺序一致。
  • 动作A、B、C表示线程T对volatile变量X的操作,动作D、E、F表示线程T对volatile变量Y的操作;如果A先于D,那么C先于F;同理线程T对X和Y的:assing、store、write也满足此条件。
A: read X
B: load X
C : use X
D: read Y
E: load Y
F : use Y
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页