前言

任何一门语言都有其语言规范,从逻辑上我们可划分为语法规范和语义规范,语法规范则是描述了如何通过相关语法编写可执行的程序,而语义规范则是指通过语法编写的程序所构造出的具体含义。语言只要具备存储(比如堆、栈),我们此时必须定义存储行为规则,这种行为规则就是内存模型。Java初始版本内存模型允许行为安全泄漏,此外,它阻止了几乎所有的单线程编译器优化操作,因此,从Java 1.5开始,引入了新的内存模型来修复这些缺陷,接下来我们来详细了解看看其内存模型到底是啥玩意,若有错误之处,还望批评指正。

内存模型(JMM)

我们知道大多数情况下编写的程序按顺序而执行,此时也是按照对应顺序存储在内存中,很显然,读取应遵循该顺序进行的最新写入,这是最原始的单核模型,随着时代的进步、技术也才随之发展,此时出现了多处理器体系结构,线程共享内存已凸显出对于并发编程的优势,但共享内存必然要使用同步机制使得内存中的数据一致,而同步机制却对系统性能产生很大影响,为了避免这种情况,通过对应策略使得存储数据一致性,当然这些策略是放宽的,如此将导致意外情况的出现,因为开发者很难推理执行程序最终结果,所以在多线程情况下我们尤其关心内存模型,但是问题随之变得复杂了起来,内存模型定义了在实现该内存的共享内存体系结构上运行的多线程程序的所有可能结果,从本质上讲,可认为它是对可能值的规范,它允许返回对内存的读取访问,从而指定平台的多线程语义。 Java内存模型(JMM)设计有两个目标:应该允许尽可能多的编译器优化、一般情况下开发者不必了解其所有复杂性可以更容易进行多线程编程,但是这项任务巨大,从某种意义上来讲,这种模型增加了太多的不确定性,为了实现第二个目标,为了开发者能够更好的可编程,于是JMM提供一个较弱的保证,称之为无数据竞争保证(Data Race Free),简称为DRF,它保证:如果程序不包含数据竞争,则允许行为可以通过交错语义来描述,换句话说,如果程序的所有顺序一致的执行没有数据争用,则所有执行似乎都是顺序一致的,所以我们可认为JMM是无数据争用的内存模型,DRF通过顺序一致性来保证。这里我们只是抽象概括了内存模型,接下来将通过大量的篇幅来进一步分析顺序一致性以及通过顺序一致性怎么就保证了内存模型。

顺序一致性(Sequential Consistency简称SC)

我们知道在多线程情况下由于竞争条件的存在会发生数据竞争,那么如何解决数据竞争呢?这就涉及到数据竞争自由度(Data Race Freeness)的概念:程序结果计算的正确性依赖于运行时的相关时序或多线程交替时就会产生竞争条件从而发生数据竞争,那么反过来讲,数据竞争自由度则是正确同步的程序具有顺序一致性,那么到底何为顺序一致性(sequential consistency)呢?执行结果都与所有处理器的操作相同按顺序执行,每个操作处理器按程序指定的顺序依次出现,单个内存引用(加载或存储)完成的顺序称为执行顺序,语句在原始代码中的排序方式称为程序顺序(Program Order以下简称为PO),执行顺序决定总的顺序,执行顺序与程序顺序(每个线程中的指令顺序所决定的顺序)一致,并且每次读取存储位置时都会看到写入的最后一个值,这意味着,执行顺序好像具有按总顺序(Total Order)执行的结果形态,但执行的实际顺序不一定按总顺序进行,因为编译器的优化和指令的并行执行是允许的。讲到这里,感觉很抽象,接下来我们通过简单的例子来介绍执行顺序、总顺序、程序顺序概念。

public class Main {
public static void main(String[] args) {
int x = 1;
int y, z, m;
if (x == 1) {
y = 2;
} else {
z = 1;
}
z = y;
}
}

上述我们初始化分配x==1,而y、z、m都等于0,接下来进入判断语句,总顺序则是由read(x):2、write(y,2)、read(y):2组成,程序顺序就是总顺序,而执行顺序是实际在内存中的操作顺序,由于缓存一致性协议的存在改善了系统处理性能,同时为了解决缓存副本需要使用同步机制使得缓存副本一致,但是同步机制大大降低了性能,于是通过重排序进一步改善性能,接踵而来的重排序能够保持SC(顺序一致性)吗,我们看如下例子:

public class Main {
public static void main(String[] args) {
int a = 1;
int b = 2;
System.out.println(a);
System.out.println(b);
}
}

上述在多线程情况下进行重排序可能先打印出2,然后打印出1,重排序并未影响实际打印结果,重排序优化了性能,使得代码运行的更快,但是要是如下例子呢?

class ReadWriteExample {
int a, b = 0; void write() {
b = 2;
a = 1;
} void read() {
int r1 = a;
int r2 = b;
System.out.println(r1);
System.out.println(r2);
}
}

如果按照SC定义在多线程情况下执行,要么a = 1最后执行,要么r2 = b最后执行,所以r1和r2可能的结果为(0,*)或者(*,2),但是若按照如下形式执行重排序,此时r1和r2的结果为(1,0),所以我们可得出结论:重排序并不能保持SC,可能会打破SC。

因为指令重排序的可能性,所以顺序一致性并不意味着操作按照特定的总顺序执行,尽管顺序一致性具有非常清晰而明确的语义,但编译器很难静态地确定它是否会进行指令重排序或允许并行执行操作以保留安全性,这种语义阻止了许多(但不是全部)针对顺序代码的常见编译器优化,因此,对于未正确同步(即包含数据竞争)的程序,对顺序一致性采取了比较弱的含义。那么问题来了,Java中的内存模型究竟强内存模型还是弱内存模型呢?操作系统的内存模型是强内存模型,因为它对正常内存操作和同步操作做出明确的区分,而Java的内存模型是弱内存模型,它对正常内存操作和同步操作没有做出具体的区分,尤其是针对同步操作,它仅仅只提供了指导方向或基本思想即:同步操作在执行过程中要引起其他操作的可见性和顺序一致性限制。在弱内存模型中,并不对所有动作进行排序,仅对一些有限的原语施加硬性排序,在JMM中,这些原语包装在它们各自的同步动作中。接下来我们进入到进入到利用同步操作构建弱模型的排序情况。在《Java并发编程实战》一书中对内存模型的定义为:通过动作的形式进行描述的、所谓动作,包括变量的读写、监视器的加锁和释放锁、线程的启动和拼接,这里指代的动作即为同步动作(Synchronization Action),通过volatile进行读写、通过lock进行读锁和释放锁、线程的启动(thread.start)、线程的终止(thread.join)。既然讲到同步动作对内存模型的定义,那么逃脱不了对同步顺序(Synchronization Order以下简称为SO)的详细了解,因为同步操作来源于同步顺序,同步顺序(SO)是涵盖所有同步操作的总顺序,JMM提供了两个附加约束:SO-PO一致性和SO一致性。 接下来我们通过简单的例子来解开这些约束。

class ReadWriteExample {
volatile int x, y = 0; void m1() {
x = 1;
int r1 = y;
} void m2() {
y = 1;
int r2 = x;
}
}

上述通过volatile关键字修饰变量x和y,因此满足SO,正常PO是write(x,1)、read(y,?)、write(y,1)、read(x,?),但是SO则可能是:【write(x,1)、read(y,?)、read(x,?)、write(y,1),SO与PO执行不一致】和【write(x,1)、read(y,?)、write(y,1)、read(x,?),SO与PO执行一致】和【write(x,1)、write(y,1)、read(x,?)、read(y,?),SO与PO执行一致】。通过分析我们知道SO-PO一致性就是和正常执行程序操作一样,而SO一致性告诉我们要知道SO所有在此之前的动作,尤其是在不同线程中。所以我们可推出:同步操作(SA)是顺序一致性(SC)的,在声明为volatile的变量程序中,我们可以对结果进行推理,由于SA是SC,通过SO就足以推理结果,即使是所有动作进行了交替执行。然而SO并不能构建实用的弱内存模型,只能说SO构建了弱内存模型的基本骨架,其原因是:要么将所有操作转换为SA,要么让非SA操作不受限制的进行排序,很显然这样还是会破坏程序实际结果,若为了达到SC,需要将整个程序进行锁定,但是这又以牺牲性能为代价。接下来我们继续看看如下例子:

class ReadWriteExample {
int x;
volatile int y; void m1() {
x = 1;
y = 1;
} void m2() {
int r1 = y;
int r2 = x;
}
}

由于我们通过volatile修改了变量y,所以会对y进行SO,我们可以正确读取到y = 1,但是对于非SA操作即x变量的值读取,到底是0还是1呢?不得而知。SO即使以牺牲性能为代价保证了顺序一致性,但对非SA操作还需要一个非常弱的语义保证,那就是事先发生(happens-before)。那么什么是事先发生呢?为了捕获有关内存操作的基本顺序和可见性要求,JMM基于事先发生规则(happens-before)【深入理解Java虚拟机将其翻译为先行发生】, 此规则确定了在任何其他动作之前必须发生的动作,换句话说,此顺序指定任何读取必须看到的内存更新,仅允许执行不违反此顺序的命令。由于此模型提供的保证非常弱,因此可允许单线程编译器优化,但是,发生在模型允许通过执行动作的循环证明而凭空产生值的执行之前发生,为避免此类循环证明并保证DRF,目前的JMM比模型初始版本内存模型处理起来要复杂得多。那么事先发生规则是如何解决非SA操作的问题呢?通过引入SO的子顺序来描述数据的流转或者说以此来连接线程之间的状态,我们称之为Synchronization-with Order简称为SW,构造SW相当容易,SW并不是完整的顺序,不能覆盖所有同步操作对。我们继续来看上述代码,SW仅对看到的彼此进行操作配对,例如如上通过volatile修饰的变量y,对变量y的写入与随后所有y的读取都将同步,所以SW是根据SO来定义的,由于SO的一致性,对于变量y写入1仅与读取1同步,在此示例中,我们看到了读取和写入两个操作之间的SW,该子顺序为我们提供了线程之间的桥梁,但适用于同步操作,同样也可以扩展到非SA操作,上述程序在多线程情况下,将通过PO和SW的并集而得到HB(happens-before),从某种意义上讲,HB同时获得线程间和线程内的语义,PO将与每个线程内的顺序操作有关的信息传输到HB中,而在状态同步时通过SW传输,HB是部分排序,并允许使用指令重排序的动作构造等效执行,所以上述可能的执行顺序是(write(x,1)、write(y,1)、read(y,1)、read(x,1)),再通俗一点讲则是,当执行写入y时,由于SW(基于SO)的存在立马对y的读取完全可见,所以整个HB顺序由写入x到写入y,很自然的过度到读取y和读取x,这是可能性情况之一,如果在执行HB一致性之前就进行了读取,那么x和y值可能就是0和0,这属于HB一致性的边界情况分析,当然,有的时候结果也会出乎意料,比如在进行x的读取和写入时没有做到HB,可能结果为0和1,那说明存在竞争,换句话说没有遵守HB一致性,因此不能用来推断结果。谨记不要将SO一致性和HB一致性概念混淆:SO一致性规则指出同步操作应查看SO中最新的相关内容,而HB一致性规则指出它指示特定读取可以观察到哪些写入。

Java内存模型定义了8种操作(lock、unlock、read、load、use、assign、store、write)来进行主内存和工作内存的交互细节且为原子性,同时针对8种操作之间的规则限定实践起来非常繁琐,所以最终通过引入事先发生(happens-before)规则来保证在并发下的线程安全,关于以上8种操作的细节请参看《深入理解Java虚拟机》一书。本文详细介绍了内存模型就是一种规范,提供了可能允许的结果,它的本质是提供了一个弱的DRF保证即顺序一致性,而顺序一致性则是通过事先发生(happens-before)来保证,而事先发生(happens-before)则是8大规则:程序次序规则、监视器锁定规则、volatile变量规则、线程启动规则、线程终止规则、线程中断规则、对象终结规则、传递性。那么内存模型的具体含义是什么呢?JMM正式定义的基本组成部分是:动作,执行和验证动作的提交过程,而提交过程又会验证完整的执行。而动作则是(例如对变量的赋值),而动作汇总于执行,验证执行通过(po、so、sw、hb)有效执行产生所需的结果,最终提交允许该结果。

总结

本文我们步步分析而引入JMM的概念及其本质,最重要的是我们需要明确知道几个概念:PO、SO、SW、HB,这几个概念就是对DRF的保证, 程序顺序(PO)是对线程内的语义描述,同步顺序(SO)是同步操作的总顺序,同步子顺序(SW)是基于SO连接线程状态的桥梁,事先发生(HB)是操作有序性的保障。

何为内存模型(JMM)?的更多相关文章

  1. Java内存模型JMM与可见性

    Java内存模型JMM与可见性 标签(空格分隔): java 1 何为JMM JMM:通俗地讲,就是描述Java中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这 ...

  2. 多线程并发之java内存模型JMM

    多线程概念的引入是人类又一次有效压寨计算机的体现,而且这也是非常有必要的,因为一般运算过程中涉及到数据的读取,例如从磁盘.其他系统.数据库等,CPU的运算速度与数据读取速度有一个严重的不平衡,期间如果 ...

  3. Java内存模型JMM 高并发原子性可见性有序性简介 多线程中篇(十)

    JVM运行时内存结构回顾 在JVM相关的介绍中,有说到JAVA运行时的内存结构,简单回顾下 整体结构如下图所示,大致分为五大块 而对于方法区中的数据,是属于所有线程共享的数据结构 而对于虚拟机栈中数据 ...

  4. 全面理解Java内存模型(JMM)及volatile关键字(转载)

    关联文章: 深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(enum) 深入理解Java注解类型(@Annotation) 深入理解Java类加载器(ClassLoad ...

  5. 高并发第二弹:并发概念及内存模型(JMM)

    高并发第二弹:并发概念及内存模型(JMM) 感谢 : 深入Java内存模型 http://www.importnew.com/10589.html, cpu缓存一致性 https://www.cnbl ...

  6. 全面理解Java内存模型(JMM)及volatile关键字(转)

    原文地址:全面理解Java内存模型(JMM)及volatile关键字 关联文章: 深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(enum) 深入理解Java注解类型( ...

  7. 什么是Java内存模型(JMM)

    什么是java内存模型 缓存一致性问题 在现代计算机中,因为CPU的运算速度远大于内存的读写速度,因此为了不让CPU在计算的时候因为实时读取内存数据而影响运算速度,CPU会加入一层缓存,在运算之前缓存 ...

  8. 对多线程java内存模型JMM

    多线程概念的引入体现了人类重新有效压力寨计算机.这是非常有必要的,由于所涉及的读数据的过程中的一般操作,如从磁盘.其他系统.数据库等,CPU计算速度和数据读取速度已经严重失衡.假设印刷过程中一个线程将 ...

  9. 深入理解Java内存模型JMM与volatile关键字

    深入理解Java内存模型JMM与volatile关键字 多核并发缓存架构 Java内存模型 Java线程内存模型跟CPU缓存模型类似,是基于CPU缓存模型来建立的,Java线程内存模型是标准化的,屏蔽 ...

  10. Java内存模型(JMM)详解

    在Java JVM系列文章中有朋友问为什么要JVM,Java虚拟机不是已经帮我们处理好了么?同样,学习Java内存模型也有同样的问题,为什么要学习Java内存模型.它们的答案是一致的:能够让我们更好的 ...

随机推荐

  1. redis的管理和监控工具treeNMS

    TreeNMS可以帮助您搭建起一套用于redis的监控管理系统,也支持Memcached,让您可以通过web的方式对数据库进行管理,有了它您就可以展示NOSQL数据库.编辑修改内容,另外还配备了sql ...

  2. 用数组实现栈(C++)

    #include <iostream> //栈的数组实现 using namespace std; #define MAXSIZE 10; template<class T> ...

  3. 实战:CentOS 7.2 / Zabbix3.4安装graphtrees

    众所周知的 Zabbix图形显示问题,决定使用graphtrees 插件. 环境:CentOS7.2 + Zabbix 3.4 1)首先切换到root用户以获得足够的权限将资源下载到 /usr/sha ...

  4. 常见40个常用的js页面效果图

    1. oncontextmenu="window.event.returnValue=false" 将彻底屏蔽鼠标右键<table border oncontextmenu= ...

  5. javaweb 中 error-page

    我们的请求找不到时,会跳到错误页面,tomcat提供了一个错误页面,但是不太好.分析:tomcat自带错误页面不好的原因:有一下两点: 1.不好看: 2.不能为seo做出贡献.思考:如何解决以上问题? ...

  6. JAVA 截图+tess4j识别

    我们先来看看要识别的图片和效果图 效果图: 图片识别需要用到tess4j这个包,下面是下载地址: https://share.weiyun.com/5Hjv13T 我们拿到包以后解压出来,随便你放到哪 ...

  7. Django学习之路03

    django项目生命周期 路由层 路由匹配 #urls中的urlpatterns #url()方法 urlpatterns = [ url(r'^admin/', admin.site.urls), ...

  8. 在python中连接mysql数据库,并进行增删改查

    数据库在开发过程中是最常见的,基本上在服务端的编程过程中都会使用到,mysql是较常见的一种数据库,这里介绍python如果连接到数据库中,并对数据库进行增删改查. 安装mysql的python扩展 ...

  9. 先治再扶,重灾区后的P2P你还敢投吗?

    ​ 互联网强大的包容性和创新性,给予很多新生事物成长的空间.而其全面普及与快速传播的特性,也让任何事物都像被放在放大镜乃至显微镜下,几乎无形遁形.这样一来,新生事物很容易被"神化" ...

  10. OO第四单元总结暨学期总结

    一.第四单元作业架构设计 我们第四单元围绕UML图展开,在第四单元开始之前,本来以为我们的工作是学习如何使用UML工具,开始后才意识到我们要做的是解析UML类图.顺序图和状态图.当然,让我们解析的只是 ...