文档章节

JMM总结

markeloff
 markeloff
发布于 2016/05/10 22:27
字数 2560
阅读 38
收藏 0

1.     jmm屏蔽各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致性的内存访问效果。jmm解决的是一个线程修改一个变量,何时对其他线程可见的问题。涉及的关键字有volatile、final、锁,通过这些可以实现java的内存可见性。

2.     jmm定义的内存模型如图:

      其实jvm并没有本地内存、主内存的说法,只不过为了让人们更加理解jmm,屏蔽内部实现的复杂性而抽象出来的模型。

3.     happens-before

在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。两个操作既可以是一个线程中的,也可以是两个不同的线程中的。Happends-before的规则如下:

            A 程序顺序规则:一个线程中的每个操作,happens- before 于该线程中的任意后续操作。

            B 监视器锁规则:对一个监视器锁的解锁,happens- before 于随后对这个监视器锁的加锁。

            C volatile变量规则:对一个volatile域的写,happens- before 于任意后续对这个volatile域的读。

            D 传递性:如果A happens- before B,且B happens- before C,那么A happens- before  C。

       A可以这么理解,单线程中如果需要可见性的要求,即写操作对之后读操作的可见性,那必然出现了数据依赖关系,根据as-if-serial语义,不会出现重排序。

       如果遵循这些规则,就能保证变量的可见性了。

       对于java程序员来说,happens-before规则简单易懂,它避免java程序员为了理解JMM提供的内存可见性保证而去学习复制的重排序规则以及这些规则的具体实现。

        重点注意:对两个线程来说,为了正确的设置happens-before关系,访问相同的volatile变量是很重要的。以下的结论是不正确的:当线程A写volatile字段f的时候,线程A可见的所有东西,在线程B读取volatile的字段g之后,变得对线程B可见了。释放操作和获取操作必须匹配(也就是在同一个volatile字段上面完成)。

4.     as-if-serial

      不管怎么重排序(编译器和处理器为了提高并行度),程序的执行结果不能被改变,编译器,runtime和处理器都必须遵守as-if-serial语义。

      为了遵守as­-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。

数据依赖性

       如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。数据依赖分下列三种类型:

名称

代码示例

说明

写后读

a = 1;b = a;

写一个变量之后,再读这个位置。

写后写

a = 1;a = 2;

写一个变量之后,再写这个变量。

读后写

a = b;b = 1;

读一个变量之后,再写这个变量。

5.     volatile的含义

volatile的特性

可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量的最后的写入。

原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这个种符合操作不具有                原子性。

有序性:加入内存屏障,防止重排序。

 

volatile写的内存语义:

        当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。

volatile读的内存语义:

        当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

        volatile的内存语义实现是通过内存屏障来实现的。

        下面是JMM针对编译器制定的volatile重排序规则表:

是否能重排序

第二个操作

第一个操作

普通读/写

volatile读

volatile写

普通读/写

 

 

NO

volatile读

NO

NO

NO

volatile写

 

NO

NO

从上表我们可以看出:

       当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。

       当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。

       当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。

 

下面是基于保守策略的JMM内存屏障插入策略:

 在每个volatile写操作的前面插入一个StoreStore屏障

 在每个volatile写操作的后面插入一个StoreLoad屏障

 在每个volatile读操作的后面插入一个LoadLoad屏障

 在每个volatile读操作的后面插入一个LoadStore屏障

       上述内存屏障插入策略非常保守,但它可以保证在任意处理器平台,任意的程序中都能得到正确的volatile内存语义。

        为了保证内存可见性,java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。JMM把内存屏障指令分为下列四类:

屏障类型

指令示例

说明

LoadLoad Barriers

Load1; LoadLoad; Load2

确保Load1数据的装载,之前于Load2及所有后续装载指令的装载。

StoreStore Barriers

Store1; StoreStore; Store2

确保Store1数据对其他处理器可见(刷新到内存),之前于Store2及所有后续存储指令的存储。

LoadStore Barriers

Load1; LoadStore; Store2

确保Load1数据装载,之前于Store2及所有后续的存储指令刷新到内存。

StoreLoad Barriers

Store1; StoreLoad; Load2

确保Store1数据对其他处理器变得可见(指刷新到内存),之前于Load2及所有后续装载指令的装载。StoreLoad Barriers会使该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令。

内存屏障有两个作用:

        1)确保一些特定操作执行的顺序

        2)影响一些数据的可见性

 

为什么doublecheck需要volitale

public class DoubleCheckedLocking {                 //1

    private static Instance instance;                    //2

    public static Instance getInstance() {               //3

        if (instance == null) {                          //4:第一次检查

            synchronized (DoubleCheckedLocking.class) {  //5:加锁

                if (instance == null)                    //6:第二次检查

                    instance = new Instance();           //7:问题的根源出在这里

            }                                            //8

        }                                                //9

        return instance;                                 //10

    }                                                    //11

}        

前面的双重检查锁定示例代码的第7行(instance = new Singleton();)创建一个对象。这一行代码可以分解为如下的三行伪代码:

memory = allocate();   //1:分配对象的内存空间

ctorInstance(memory);  //2:初始化对象

instance = memory;     //3:设置instance指向刚分配的内存地址

由于步骤2与步骤3没有数据依赖关系 可能发生重排序,synchronized关键字并能保证内部不会发生重排序,所以另外一个线程可能在第四步检查不为空,但实际对象还没有初始化成功,只是分配了内存空间,并install指向了内存空间的地址。

Instance设置成valotile后会禁止instance = new Singleton()内部的重排序。

我认为instance不为volatile的话,还会出现其他的不稳定的因素。在第四步进行检查的时候有可能不是最新的值,因为普通变量不能保证读取到其他线程最后一次写入的值。

6.     final

对于final域,编译器和处理器要遵守两个重排序规则:

1)在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。

2)初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。

写final域的重排序规则

写final域的重排序规则禁止把final域的写重排序到构造函数之外。这个规则的实现包含下面2个方面:

1)JMM禁止编译器把final域的写重排序到构造函数之外。

2)编译器会在final域的写之后,构造函数return之前,插入一个StoreStore屏障。这个屏障禁止处理器把final域的写重排序到构造函数之外。

     读final域的重排序规则:在一个线程中,初次读对象引用与初次读该对象包含的final域,JMM禁止处理器重排序这两个操作(注意,这个规则仅仅针对处理器)。编译器会在读final域操作的前面插入一个LoadLoad屏障。

     对于final域只保证在构造函数中初始化的安全性,不保证后续对final引用的对象的修改的安全性。

      注意:构造对象的引用不能提前在构造器中溢出,对其他线程可见,因为final域可能还没有初始化

7.     锁

锁释放和获取的内存语义:

       当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中

       当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须要从主内存中去读取共享变量

        AQS分公平锁和非公平锁。公平锁是通过获取锁时读取volatile变量,释放锁时写入volatile变量实现可见性的;非公平锁获取锁跟跟公平锁不一样,通过cas实现获取锁,cas具有volatile相同的语义,释放锁跟公平锁一样。

参考 http://www.infoq.com/cn/author/%E7%A8%8B%E6%99%93%E6%98%8E

© 著作权归作者所有

共有 人打赏支持
markeloff
粉丝 5
博文 18
码字总数 23124
作品 0
南京
高级程序员
学习笔记二:Java内存模型以及happens-before规则

JMM的介绍 在上一篇文章中总结了线程的状态转换和一些基本操作,对多线程已经有一点基本的认识了,如果多线程编程只有这么简单,那我们就不必费劲周折的去学习它了。 在多线程中稍微不注意就...

刘祖鹏
07/12
0
0
JVM内存结构 VS Java内存模型 VS Java对象模型

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

Java架构
07/11
0
0
java 内存模型

学习,程晓明的《深入理解Java内存模型》。 摘要 文章内容有并发、内存模型、重排序、内存屏障、happens-before规则、as-if-serial语义、顺序一致性内存模型、volatile、锁、final。 并发 并...

___k先生
2017/11/09
0
0
java高并发之从零到放弃

前言 这是一个长篇博客,希望大家关注我并且一起学习java高并发 废话不多说,直接开始 并行和并发 并行:多个线程同时处理多个任务 并发:多个线程处理同个任务,不一定要同时 下面用图来描述...

野梦M
2017/11/22
0
0
我所理解的JVM(一):内存结构

Java Memory Model 简称JMM,译为Java内存模型,属于Java虚拟机规范的一部分 java虚拟机运行时的内存区域分为:程序计数器、虚机机栈、本地方法栈、堆、方法区5部分。其中前3部分是线程独享的...

康斯但丁
2017/10/28
0
0

没有更多内容

加载失败,请刷新页面

加载更多

大数据框架对比:Hadoop、Storm、Samza、Spark和Flink

简介 大数据是收集、整理、处理大容量数据集,并从中获得见解所需的非传统战略和技术的总称。虽然处理数据所需的计算能力或存储容量早已超过一台计算机的上限,但这种计算类型的普遍性、规模...

hblt-j
22分钟前
2
0
正则介绍及grep/egrep用法

10月16日任务 9.1 正则介绍_grep上 9.2 grep中 9.3 grep下 扩展 把一个目录下,过滤所有*.php文档中含有eval的行 grep -r --include="*.php" 'eval' /data 9.1 正则介绍_grep上 什么是正则 ...

zgxlinux
37分钟前
2
0
想用Unity3D引擎软件赚点钱的看过来

前言: 你可以不拥有很多钱 但你一定要有赚钱的能力 目前手上有项目, 需要熟练Unity3D引擎软件的伙伴 有意向的给我发私信

猿神出窍
40分钟前
1
0
Spring Boot全局异常处理

Spring Boot默认的异常处理机制 默认情况下,Spring Boot为两种情况提供了不同的响应方式。 一种是浏览器客户端请求一个不存在的页面或服务端处理发生异常时,一般情况下浏览器默认发送的请求...

狼王黄师傅
今天
8
0
Thinkphp5 优雅配置两个数据库

工作需要需要配置两个数据库,框架5.0的,步骤如下: 1、在database.php同级创建一个database2.php文件 在里面配置第二个数据库信息, 2、在config中配置这个数据库信息: 3、创建第二个表的...

wqzbxh
今天
5
1

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部