参考文档:https://www.cnblogs.com/_popc/p/6096517.html

volatile关键字虽然从字面上理解起来比较简单,但是要用好不是一件容易的事情。它与Java的内存模型息息相关,因此有必要了解一下与内存模型相关的概念和知识。

一、Java内存模型

究竟什么是内存模型?内存模型是描述了程序中各个变量(实例域、静态域和数组元素)之间的关系,以及在实际计算机系统中将变量存储到内存和从内存中取出变量这样的底层细节,对象最终是存储在内存里面的,但是编译器、运行库、处理器或者系统缓存可以有特权在变量指定内存位置存储或者取出变量的值。JMM(Java Memory Model的缩写)允许编译器和缓存拥有重要的特权,即数据在处理器特定的缓存(或寄存器)和主存之间移动的次序,除非程序员使用了final或synchronized明确要求了某些可见性的保证。在Java中应为不同的目的可以将java划分为两种内存模型:gc内存模型、并发内存模型。

1. gc内存模型

java与c++之间有一堵由内存动态分配与垃圾收集技术所围成的“高墙”。墙外面的人想进去,墙里面的人想出来。java在执行java程序的过程中会把它管理的内存划分若干个不同功能的数据管理区域。如图:

注意:在JDK7以及其前期的JDK版本号中。堆内存通常被分为三块区域:新生代内存(young generation)、老生代(old generation)、永久代(Permanent Generation for VM Matedata)。从JDK8开始,把存放元数据中的永久代内存从堆内存中移到了本地内存(native memory)中,这样永久代内存就不再占用堆内存。也提供了一个新的设置Matespace内存大小的參数。通过这个參数能够设置Matespace内存大小:-XX:MaxMetaspaceSize=128m 设置最大的元内存空间128m。

hotspot中的gc内存模型整体上分为三部分:栈,堆,程序计数器。

它们每一部分有其各自的用途:

(1) 虚拟机栈保存着每一条线程的执行程序调用堆栈;

(2) 堆保存着类对象、数组的具体信息;

(3) 程序计数器保存着每一条线程下一次执行指令位置。

这三块区域中栈和程序计数器是线程私有的。也就是说每一个线程拥有其独立的栈和程序计数器。我们可以看看具体结构:

  • 虚拟机/本地方法栈

在栈中,会为每一个线程创建一个栈,线程越多,栈的内存使用越大。对于每一个线程栈,当一个方法在线程中执行的时候,会在线程栈中创建一个栈帧(stack frame),用于存放该方法的上下文(局部变量表、操作数栈、方法返回地址等等)。每一个方法从调用到执行完毕的过程,就是对应着一个栈帧入栈出栈的过程。

本地方法栈与虚拟机栈发挥的作用是类似的,他们之间的区别不过是虚拟机栈为虚拟机执行java(字节码)服务的,而本地方法栈是为虚拟机执行native方法服务的。

  • 方法区/堆

在hotspot的实现中,方法区就是在堆中称为永久代的堆区域,几乎所有的对象/数组的内存空间都在堆上(有少部分在栈上)。在gc管理中,将虚拟机堆分为永久代、老年代、新生代。通过名字我们可以知道一个对象新建一般在新生代,经过几轮的gc,还存活的对象会被移到老年代。永久代用来保存类信息、代码段等几乎不会变的数据。堆中的所有数据是线程共享的。

    • 新生代:基于gc具体实现的优化的,hotspot又将新生代划分为一个eden区和两个survivor区。每一次新生代gc时候。只用到一个eden区,一个survivor区。新生代一般的gc策略为mark-copy(标记-复制)。
    • 老年代:当新生代中的对象经过若干轮gc后还存活/或survisor在gc内存不够的时候,会把当前对象移动到老年代。老年代一般gc策略为mark-compact(标记-压缩)。
    • 永久代:永久代一般可以不参与gc。因为其中保存的是一些代码/常量数据/类信息。JDK 1.8 中已经不存在永久代。

JVM内存模型中分两大块,一块是 NEW Generation, 另一块是Old Generation。在New Generation中,有一个叫Eden的空间,主要是用来存放新生的对象,还有两个Survivor Spaces(from,to), 它们用来存放每次垃圾回收后存活下来的对象。在Old Generation中,主要存放应用程序中生命周期长的内存对象,还有个Permanent Generation,主要用来放JVM自己的反射对象,比如类对象和方法对象等。

  • 程序计数器

如同其名称一样。程序计数器用于记录某个线程下次执行指令位置。程序计数器也是线程私有的。

2. 并发内存模型

java试图定义一个Java内存模型(Java memory model jmm)来屏蔽掉各种硬件/操作系统的内存访问差异,以实现让java程序在各个平台下都能达到一致的内存访问效果。java内存模型主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。模型图如下:

java并发内存模型以及内存操作规则

java内存模型中规定了所有变量都存贮到主内存(如虚拟机物理内存中的一部分)中,每一个线程都有一个自己的工作内存(如cpu中的高速缓存)。线程中的工作内存保存了该线程使用到的变量的主内存的副本拷贝。线程对变量的所有操作(读取、赋值等)必须在该线程的工作内存中进行。不同线程之间无法直接访问对方工作内存中变量。线程间变量的值传递均需要通过主内存来完成。

关于主内存与工作内存之间的交互协议,即一个变量如何从主内存拷贝到工作内存。如何从工作内存同步到主内存中的实现细节。java内存模型定义了8种操作来完成。这8种操作每一种都是原子操作。8种操作如下:

  • lock(锁定):作用于主内存,它把一个变量标记为一条线程独占状态;
  • unlock(解锁):作用于主内存,它将一个处于锁定状态的变量释放出来,释放后的变量才能够被其他线程锁定;
  • read(读取):作用于主内存,它把变量值从主内存传送到线程的工作内存中,以便随后的load动作使用;
  • load(载入):作用于工作内存,它把read操作的值放入工作内存中的变量副本中;
  • use(使用):作用于工作内存,它把工作内存中的值传递给执行引擎,每当虚拟机遇到一个需要使用这个变量的指令时候,将会执行这个动作;
  • assign(赋值):作用于工作内存,它把从执行引擎获取的值赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的指令时候,执行该操作;
  • store(存储):作用于工作内存,它把工作内存中的一个变量传送给主内存中,以备随后的write操作使用;
  • write(写入):作用于主内存,它把store传送值放到主内存中的变量中。

Java内存模型还规定了执行上述8种基本操作时必须满足如下规则:

  • 不允许read和load、store和write操作之一单独出现,以上两个操作必须按顺序执行,但没有保证必须连续执行,也就是说,read与load之间、store与write之间是可插入其他指令的。
  • 不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
  • 不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。
  • 一个新的变量只能从主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
  • 一个变量在同一个时刻只允许一条线程对其执行lock操作,但lock操作可以被同一个条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
  • 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
  • 如果一个变量实现没有被lock操作锁定,则不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量。
  • 对一个变量执行unlock操作之前,必须先把此变量同步回主内存(执行store和write操作)。

二、volatile型变量的特殊规则

键字volatile可以说是Java虚拟机提供的最轻量级的同步机制,但是它并不容易完全被正确、完整的理解,以至于许多程序员都不习惯去使用它,遇到需要处理多线程的问题的时候一律使用synchronized来进行同步。Java内存模型对volatile专门定义了一些特殊的访问规则,当一个变量被定义成volatile之后,它将具备两种特性:

  • 保证此变量对所有线程的可见性。第一保证此变量对所有线程的可见性,这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。而普通变量是做不到这点,普通变量的值在线程在线程间传递均需要通过主内存来完成,例如,线程A修改一个普通变量的值,然后向主内存进行会写,另外一个线程B在线程A回写完成了之后再从主内存进行读取操作,新变量值才会对线程B可见。另外,java里面的运算并非原子操作,会导致volatile变量的运算在并发下一样是不安全的。
  • 禁止指令重排序优化。普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获得正确的结果,而不能保证变量赋值操作的顺序与程序中的执行顺序一致,在单线程中,我们是无法感知这一点的。

由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景中,我们仍然要通过加锁来保证原子性:

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

三、原子性、可见性与有序性

Java内存模型是围绕着在并发过程中如何处理原子性、可见性和有序性这三个特征来建立的,我们逐个看下哪些操作实现了这三个特性。

  • 原子性(Atomicity):由Java内存模型来直接保证的原子性变量包括read、load、assign、use、store和write,我们大致可以认为基本数据类型的访问读写是具备原子性的。如果应用场景需要一个更大方位的原子性保证,Java内存模型还提供了lock和unlock操作来满足这种需求,尽管虚拟机未把lock和unlock操作直接开放给用户使用,但是却提供了更高层次的字节码指令monitorenter和monitorexit来隐式的使用这两个操作,这两个字节码指令反应到Java代码中就是同步块--synchronized关键字,因此在synchronized块之间的操作也具备原子性。
  • 可见性(Visibility):可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。上文在讲解volatile变量的时候我们已详细讨论过这一点。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的,无论是普通变量还是volatile变量都是如此,普通变量与volatile变量的区别是,volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。因此,可以说volatile保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。除了volatile之外,Java还有两个关键字能实现可见性,即synchronized和final.同步快的可见性是由“对一个变量执行unlock操作前,必须先把此变量同步回主内存”这条规则获得的,而final关键字的可见性是指:被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把"this"的引用传递出去,那么在其他线程中就能看见final字段的值。
  • 有序性(Ordering):Java内存模型的有序性在前面讲解volatile时也详细的讨论过了,Java程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的:如果在一个线程中观察另外一个线程,所有的线程操作都是无序的。前半句是指“线程内表现为串行的语义”,后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象。Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性,volatile关键字本身就包含了禁止指令重排序的语义,而synchronized则是由“一个变量在同一个时刻只允许一条线程对其进行lock操作”这条规则获得的,这条规则决定了持有同一个锁的两个同步块只能串行的进入。

volatile关键字及Java内存模式的更多相关文章

  1. volatile变量,java内存模型

    volatile变量提供了最轻量级的同步机制,当一个变量加上volatile修饰时,会具有一下两个特性 https://blog.csdn.net/u011277123/article/details ...

  2. Java内存模式

    Java内存模型即Java Memory Model,简称JMM.JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式. JVM是Java Virtual Machine(Java ...

  3. Java之先行发生原则与volatile关键字详解

    volatile关键字可以说是Java虚拟机提供的最轻量级的同步机制,但是它并不容易完全被正确.完整地理解,以至于许多程序员都习惯不去使用它,遇到需要处理多线程数据竞争问题的时候一律使用synchro ...

  4. 万字长文:从计算机本源深入探寻volatile和Java内存模型

    万字长文:从计算机本源深入探寻volatile和Java内存模型 前言 在本篇文章当中,主要给大家深入介绍Volatile关键字和Java内存模型.在文章当中首先先介绍volatile的作用和Java ...

  5. Java 内存模型,或许应该这么理解

    大家好,我是树哥. 在前面一段时间,我连续写了几篇关于并发编程的文章: 从 CPU 讲起,深入理解 Java 内存模型! - 陈树义的博客 深入理解 happens-before 原则 - 陈树义的博 ...

  6. java IO操作和计算操作:工作内存和主内存 volatile关键字作用;原子操作对象AtomicInteger ....

    应该停止但无法停止的计算线程 如下线程示例,线程实例中while循环中的条件,在主线程中通过调用实例方法更新后,while循环并没有更新判断变量是否还成立.而是陷入了while(true)死循环. i ...

  7. Java并发机制(3)--volatile关键字与内存模型

    Java并发编程:volatile关键字解析及内存模型 个人整理自:博客园-海子-http://www.cnblogs.com/dolphin0520/p/3920373.html 1.线程内存模型: ...

  8. 07 volatile & java 内存模型

    一 从单例模式说起 在singleton 单例模式一文中我们详细了解Java中单例模式的实现,不了解的可以先阅读之. 在该文最后我们给出了双重校验锁来保证既实现线程安全,又能够使性能不受很大的影响的单 ...

  9. Java并发编程:volatile关键字解析(转载)

    转自https://www.cnblogs.com/dolphin0520/p/3920373.html Java并发编程:volatile关键字解析   Java并发编程:volatile关键字解析 ...

随机推荐

  1. sqlite 的去重

    1) 找到重复的记录,归类到一个新表里面 max(id) 是想要删除的record 2) 删除 delete from gallery where id in ( select theid from ...

  2. Java常识2

    JDK 的下载 安装 下载 官网 github安装 傻瓜式安装 JDK .JRE 注意问题 安装软件的 路径不能包含中文 空格 path环境变量 windows操作系统执行命令是所要搜寻的路径为什么要 ...

  3. 在线web软件

    编程 Compiler Explorer SharpLab 着色器 shadertoy 腾讯文档 word  excel  ppt ... 思维导图 MindMaster ProcessOn 流程图. ...

  4. 使用nexus搭建maven仓库(maven 本地私服)

    我们在使用maven的时候,对于项目所依赖的jar包,maven默认会在中央仓库下载jar包,到本地的磁盘目录(如果没有配置则是用户目录下/.m2/repository文件夹下).如果公司内部搭了一个 ...

  5. webpack-dev-server 不是内部或外部命令,也不是可运行的程序 解决方案

    我看了网上的 一些解决方案,说是webpack版本不对,但我按照提示操作后依然不行: 要先确认是否安装了webpack-dev-server,如果没有安装,安装便可以解决: 粗暴的解决方案是删除nod ...

  6. 【MySQL】Mariadb主从复制

    Mariadb主从复制 环境配置: Master : 172.30.200.200 Slave : 172.30.200.204 架构图,如下: Master的配置: 1.binlog配置 [root ...

  7. python操作时间

    一.问题背景 在对数据进行操作的时候我们总是会遇到数据类型是date类型的数据,这种数据会让我们在使用和操作的过程中遇到一些问题,比如int类型和date类型不对等,string类型和date类型不对 ...

  8. OpenGL入门1.3:着色器 GLSL

    前言 经过之前一段时间的学习(渲染管线简介)我们已经知道了着色器(Shader)是运行在GPU上的程序,这些小程序为图形渲染管线的某个特定部分而运行,着色器只是一种把输入转化为输出的程序,着色器也是一 ...

  9. Oracle 查询练习

    非常经典的一些日常醒脑练习内容!! 如有更高效的写法欢迎赐教! .已知Oracle的Scott用户中提供了三个测试数据库表,名称分别为dept,emp和salgrade.使用SQL语言完成以下操作 ) ...

  10. mssql附加的数据库查询的时候没有搜索权限

    1.选中数据安全性-登录名-选择某个账户-右键-属性 2.服务器角色-选择public和systemadmin 3.用户映射-选中库-下面选中public 和owner