Java虚拟机(JVM) 规范中定义了一种Java的内存模型,即Java Memoory Model(简称JMM),用来实现让Java程序在各个平台下都能达到一致的内存访问效果。

JVM是整个虚拟机,JMM模型属于JVM的一部分。JDK1.5后Java内存模型才逐渐的成熟和完善起来。

主内存与工作内存

Java内存模型的主要目标是定义程序中各个变量的访问规则,此处的变量与Java编程中所说的变量有所区别,它包括了实例字段、静态字段和构成数组对象的元素,

但不包含局部变量与方法参数,因为后者是线程私有的。

Java内存模型规定了所以的变量都存储在主内存(虚拟机内存的一部分)中,每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的共享变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,不能直接读写主内存中的变量。

线程间的变量值传递等操作均需要通过主内存来完成。

线程、工作内存、主内存三者的交互关系如图:

一个变量如何从主内存拷贝到工作内存、如何从工作内存同步会主内存的实现细节,在Java内存模型中定义了以下8种操作来完成,虚拟机实现时必须保证下面的每一个操作都是原子的,不可再分的。

变量从主内存复制到工作内存:顺序执行read和load操作。
变量从工作内存同步回主内存:顺序的执行store和write操作。

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

  • 不允许read和load、store和write操作之一单独出现。
  • 不允许一个线程丢弃它的最近的assign操作(在线程中改变了变量必须同步回主内存)。
  • 不允许一个线程无原因地把数据从线程的工作内存同步回主内存。
  • 一个新的变量只能从主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化的变量。
  • 一个变量在同一时刻只允许一条线程对其进行lock操作,但可以在一条线程中重复多次lock一个变量,但unlock时也必须重复相应的次数。
  • 如果一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎是用这个变量前,需要观众席执行load或assign操作初始化变量的值。
  • 如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作。
  • 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)

volatile型变量的特殊规则

关键字volatile可以说是Java虚拟机提供的最轻量级的同步机制。

在处理多线程数据竞争问题时,不仅仅是可以使用synchronized关键字来实现,使用volatile也可以实现。

Java内存模型对volatitle专门定义了一些特殊的访问规则,当一个变量被定义为volatile时,它将具备以下两个特性:

  • 一:保证此变量对所有线程的可见性,这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。而普通变量不能做到这一点,普通变量的值,在线程之间的传递都是需要通过主内存的的stroe和write操作以及read和load操作来实现的。volatile只保证了变量的可见性,但并不能保证变量运算的原子性。

  • 二:禁止指令重排序优化,普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。

由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景中,我们仍然要通过枷锁(使用synchronized或java.util.concurrent中的原子类)来保证原子性。

运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。

变量不需要与其他的状态变量共同参与不变约束。

如下代码就很适合使用volatile变量来控制并发:

    volatile boolean shutdownRequested;

    public void shutdown(){
shutdownRequested = true;
} public void doWork(){
while (!shutdownRequested){
//do something;
}
}

原子性、可见性与有序性

Java内存模型是围绕着在并发过程中如何处理原子性、可见性、和有序性这3个特征来建立的。

  • 原子性(Atomicity):

由Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store、和write,基本可以认为基本数据类型的访问读写是具备原子性的。

如果需要一个更大范围的原子性保证,Java内存模型还提供了lock和unlock操作来满足这种需求,尽管虚拟机未把这两个操作直接开发给用户使用,但却提供了更高层次的字节码指令来隐式地使用这两个操作,这两个字节码指令反应到Java代码中就是同步块----synchronized关键字,因此synchronized块之间的操作也具备原子性。

  • 可见性(Visibility):

可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。

Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的,无论是普通变量还是volatile变量和是普通变量都是如此,普通变量与volatile变量的区别是,volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。

除了volatile之外,synchronized和final也可以实现可见性,synchronized的可见性是由“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中”这条规则获得的,final是因为被final修饰的字段在构造器中一旦完成,并且构造器没有吧“this”的引用传递出去,在其他线程中就能看见final字段值。

  • 有序性(Ordering):

Java程序中天然的有序性可以总结为一句话:如果在本线程内观察,所以的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。

前半句指“线程内表现为串行语义”,后半句指“指令重排序”现象和“工作内存与主内存同步延迟”现象。

Java提供了volatile和synchronized两个关键字来保证线程之间的操作时有序的,volatile包含了精致指令重排序的语义,而synchronized是由“一个变量在同一时刻只允许一个条线程对其进行lock操作”这条规则获得的,这条规则决定了持有同一个锁的两个同步块只能串行的进入。

先行发生原则(appens-before)

若Java内存模型中所有的有序性都仅依靠volatile和synchronized来完成,那么有一些操作会变得很繁琐,但是我们在写java代码中并没有感知这一点,因为Java语言中有一个“先行发生”原则。

先行发生是Java内存模型中定义的两项操作之间的偏序关系,若操作A先行发生于操作B,操作A产生的影响能被操作B观察到,“影响”是指修改了内存中共享变量的值、发送了消息、调用了方法等。

例子:

    //以下操作在线程A中执行
i = 7; //以下操作在线程B中执行
j = i; //以下操作在线程C中执行
i = 9;

如果线程A先行发生于线程B那么变量j一定是7,因为线程A的操作会被线程B观察到进而被影响,而此时线程C还没有发生,所以j一定是7。但如果线程C出现在线程A和线程B之间,线程C没有与线程B不存在先行发生,那么此时线程C对变量i的影响可能会被线程B观察到,也可能不会,这时线程B读取到的数据就存在过期风险,不具备多线程安全性。

Java内存模型中存在一些“天然的”先行发生关系,这些先行发生关系无须任何同步器协助就已经存在。如果两个操作之间的关系不在如下规则中,并且无法从下列规则中推导出来,那它们就没有顺序性保障,虚拟机可以对它们随意地进行重排序。

如下:

  • 程序次序规则:在一个线程内按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确的地应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。

  • 管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。这里指的是同一个锁,而“后面”是指时间上的先后顺序。

  • volatile变量规则:对于一个变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”同样指时间上的先后顺序。

  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。

  • 线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。

  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。

  • 对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。

  • 传递性:如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出结论:操作A优先发生于操作C。

如何判断操作是否有顺序性呢?

示例:

    private int value = 9;

    public static int getValue() {
return value;
} public void setValue(int value){
this.value = value;
}

一组普通的getter/setter方法,若线程A先调用了“setValue(10)”,然后线程B调用了同一个对象的“getValue()”,那么线程B得到的返回值是什么?

分析:由于线程A和线程B时两个线程所以程序次序规则不适用,由于没有同步块,也不会发生lock和unlock,所以管程锁定规则也不适用,没有volatile关键字,所以volatile变量规则也不适用,后面的线程启动规则、线程终止规则、线程中断规则、对象终结规则也和这里没关系。因为没有一个适用的先行发生规则,所以传递性也不存在,因此虽然线程A在时间上先与线程B,但是无法确定线程B中“getValue()”的返回值,也就是说,这个操作不具备多线程安全性。

那么怎么修复这个问题,让这个操作编程线程安全的呢?

有两种方式:

  • 把getter/setter方法都定义为synchronized方法。
  • 把value定义为volatile变量。

虽然时间上的先行执行不代表就会先行发生,那如果先行发生是不是就一定会是时间上的先行执行呢?

举例说明:

    //以下操作在同一个线程中执行
int i = 10; int j = 20;

由于两条赋值语句,在同一个线程中执行,根据程序次序规则,第一条语句先行发生于第二条语句,但是第二条语句的代码完全可能先被处理器执行,这并不影响先行发生原则的正确性,因为在这条线程之中没有办法感知到这一点。

小结:

时间先后顺序与先行发生原则之间没什么太大的关系,所以并发问题不要受时间顺序的干扰,要以先行发生原则为准。

悄悄话

理论性的知识很容易忘记。自己提出疑惑然后解开,这样能加深印象。

知识点:Java 内存模型完全解密的更多相关文章

  1. java内存模型知识点汇总

    1.像windows/linux这种操作系统中,自带jvm么?以方便java程序的运行? 答:是的,一般操作系统都自带jvm的.但不带jdk,也就是说java的运行环境有,但编译环境没有. 1.jav ...

  2. Java内存模型知识点小结---《深入理解Java内存模型》(程晓明)读书总结

    一.Java内存模型介绍 内存模型的作用范围: 在Java中,所有实例域.静态域和数组元素存放在堆内存中,线程之间共享,下文称之为“共享变量”.局部变量.方法参数.异常处理器等不会在线程之间共享,不存 ...

  3. JVM学习(3)——总结Java内存模型

    俗话说,自己写的代码,6个月后也是别人的代码……复习!复习!复习!涉及到的知识点总结如下: 为什么学习Java的内存模式 缓存一致性问题 什么是内存模型 JMM(Java Memory Model)简 ...

  4. 【JVM】JVM内存结构 VS Java内存模型 VS Java对象模型

    原文:JVM内存结构 VS Java内存模型 VS Java对象模型 Java作为一种面向对象的,跨平台语言,其对象.内存等一直是比较难的知识点.而且很多概念的名称看起来又那么相似,很多人会傻傻分不清 ...

  5. 【转】JVM内存结构 VS Java内存模型 VS Java对象模型

    JVM内存结构 我们都知道,Java代码是要运行在虚拟机上的,而虚拟机在执行Java程序的过程中会把所管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途. 其中有些区域随着虚拟机进程的启动而 ...

  6. 来,了解一下Java内存模型(JMM)

    网上有很多关于Java内存模型的文章,在<深入理解Java虚拟机>和<Java并发编程的艺术>等书中也都有关于这个知识点的介绍.但是,很多人读完之后还是搞不清楚,甚至有的人说自 ...

  7. JVM学习(3)——总结Java内存模型---转载自http://www.cnblogs.com/kubixuesheng/p/5202556.html

    俗话说,自己写的代码,6个月后也是别人的代码……复习!复习!复习!涉及到的知识点总结如下: 为什么学习Java的内存模式 缓存一致性问题 什么是内存模型 JMM(Java Memory Model)简 ...

  8. 并发编程之 Java 内存模型 + volatile 关键字 + Happen-Before 规则

    前言 楼主这个标题其实有一种作死的味道,为什么呢,这三个东西其实可以分开为三篇文章来写,但是,楼主认为这三个东西又都是高度相关的,应当在一个知识点中.在一次学习中去理解这些东西.才能更好的理解 Jav ...

  9. 再有人问你Java内存模型是什么,就把这篇文章发给他

    前几天,发了一篇文章,介绍了一下JVM内存结构.Java内存模型以及Java对象模型之间的区别.有很多小伙伴反馈希望可以深入的讲解下每个知识点.Java内存模型,是这三个知识点当中最晦涩难懂的一个,而 ...

随机推荐

  1. redis 版的 hello world

    为 redis 添加一个命令,效果如下图: 在 Server.h 中加入命令处理函数的声明: void meCommand(client *c); 在 Server.c 的命令表中加入: struct ...

  2. vscode extensions

    ├─ .obsolete├─ .wlck├─ .wtid├─ abeyuhang.vscode-lesslint-0.0.1├─ abusaidm.html-snippets-0.2.1├─ adam ...

  3. MapServer Tutorial——MapServer7.2.1教程学习——第一节用例实践:Example1.1 A map with single layer

    MapServer Tutorial——MapServer7.2.1教程学习——第一节用例实践:Example1.1 A map with single layer 一.前言 开始MapServer用 ...

  4. spoj Minimax Triangulation

    题解: dp+计算几何 F[i][j]表示第i-j条边的答案 然后转移一下 代码: #include<bits/stdc++.h> using namespace std; ]; ][]; ...

  5. 指导手册05:MapReduce编程入门

    指导手册05:MapReduce编程入门   Part 1:使用Eclipse创建MapReduce工程 操作系统: Centos 6.8, hadoop 2.6.4 情景描述: 因为Hadoop本身 ...

  6. 2018-北航-面向对象-前三次OO作业分析与小结

    基于度量的程序结构分析 由于平时使用了NetBrains出品的IDEA作为IDE,在分析程序的时候我使用了IDEA的插件Metrics Reloaded.然而在使用时发现不懂得很多分析项目的含义,因此 ...

  7. JAVA学习笔记系列2-Java程序的运行机制

    计算机高级语言的类型主要有编译型和解释型两种,而java语言是两种类型的结合. java首先利用文本编辑器编写java源程序,源文件后缀名为.java,再利用编译器(javac)将源程序编译成字节码文 ...

  8. webView 获取内容高度不准确的原因是因为你设置了某个属性

    不管是UIWebView 还是 WKWebView 这里 获取js属性 获取高度的方法 我就不一一细说了 ,本文最主要不说这个 ,网上有太多的方法 我最不摘几个 CGFloat webViewHeig ...

  9. 基于redis的延迟消息队列设计(转)

    需求背景 用户下订单成功之后隔20分钟给用户发送上门服务通知短信 订单完成一个小时之后通知用户对上门服务进行评价 业务执行失败之后隔10分钟重试一次 类似的场景比较多 简单的处理方式就是使用定时任务 ...

  10. javascript继承的6种方法

    1原型式继承 简介:对类式继承的封装,过渡对象相当于子类. function inheritObject(o) { //声明过渡函数对象 function F() {} //过渡对象的原型继承父类 F ...