Java内存模型JMM详解

Java Memory Model简称JMM, 是一系列的Java虚拟机平台对开发者提供的多线程环境下的内存可见性、是否可以重排序等问题的无关具体平台的统一的保证。(可能在术语上与Java运行时内存分布有歧义,后者指堆、方法区、线程栈等内存区域)。
并发编程有多种风格,除了CSP(通信顺序进程)、Actor等模型外,大家最熟悉的应该是基于线程和锁的共享内存模型了。在多线程编程中,需要注意三类并发问题:

·原子性
·可见性
·重排序

原子性涉及到,一个线程执行一个复合操作的时候,其他线程是否能够看到中间的状态、或进行干扰。典型的就是i++的问题了,两个线程同时对共享的堆内存执行++操作,而++操作在JVM、运行时、CPU中的实现都可能是一个复合操作, 例如在JVM指令的角度来看是将i的值从堆内存读到操作数栈、加上一、再写回到堆内存的i,这几个操作的期间,如果没有正确的同步,其他线程也可以同时执行,可能导致数据丢失等问题。常见的原子性问题又叫竞太条件,是基于一个可能失效的结果进行判断,如读取-修改-写入。 可见性和重排序问题都源于系统的优化。

由于CPU的执行速度和内存的存取速度严重不匹配,为了优化性能,基于时间局部性、空间局部性等局部性原理,CPU在和内存间增加了多层高速缓存,当需要取数据时,CPU会先到高速缓存中查找对应的缓存是否存在,存在则直接返回,如果不存在则到内存中取出并保存在高速缓存中。现在多核处理器越基本已经成为标配,这时每个处理器都有自己的缓存,这就涉及到了缓存一致性的问题,CPU有不同强弱的一致性模型,最强的一致性安全性最高,也符合我们的顺序思考的模式,但是在性能上因为需要不同CPU之间的协调通信就会有很多开销。

典型的CPU缓存结构示意图如下

CPU的指令周期通常为取指令、解析指令读取数据、执行指令、数据写回寄存器或内存。串行执行指令时其中的读取存储数据部分占用时间较长,所以CPU普遍采取指令流水线的方式同时执行多个指令, 提高整体吞吐率,就像工厂流水线一样。

读取数据和写回数据到内存相比执行指令的速度不在一个数量级上,所以CPU使用寄存器、高速缓存作为缓存和缓冲,在从内存中读取数据时,会读取一个缓存行(cache line)的数据(类似磁盘读取读取一个block)。数据写回的模块在旧数据没有在缓存中的情况下会将存储请求放入一个store buffer中继续执行指令周期的下一个阶段,如果存在于缓存中则会更新缓存,缓存中的数据会根据一定策略flush到内存。

public class MemoryModel {
  private int count;
  private boolean stop;
  public void initCountAndStop() {
    count = 1;
    stop = false;
  }
  public void doLoop() {
    while(!stop) {
      count++;
    }
  }
  public void printResult() {
    System.out.println(count);
    System.out.println(stop);
  }
}

上面这段代码执行时我们可能认为count = 1会在stop = false前执行完成,这在上面的CPU执行图中显示的理想状态下是正确的,但是要考虑上寄存器、缓存缓冲的时候就不正确了, 例如stop本身在缓存中但是count不在,则可能stop更新后再count的write buffer写回之前刷新到了内存。

另外CPU、编译器(对于Java一般指JIT)都可能会修改指令执行顺序,例如上述代码中count = 1和stop = false两者并没有依赖关系,所以CPU、编译器都有可能修改这两者的顺序,而在单线程执行的程序看来结果是一样的,这也是CPU、编译器要保证的as-if-serial(不管如何修改执行顺序,单线程的执行结果不变)。由于很大部分程序执行都是单线程的,所以这样的优化是可以接受并且带来了较大的性能提升。但是在多线程的情况下,如果没有进行必要的同步操作则可能会出现令人意想不到的结果。例如在线程T1执行完initCountAndStop方法后,线程T2执行printResult,得到的可能是0, false, 可能是1, false, 也可能是0, true。如果线程T1先执行doLoop(),线程T2一秒后执行initCountAndStop, 则T1可能会跳出循环、也可能由于编译器的优化永远无法看到stop的修改。

由于上述这些多线程情况下的各种问题,多线程中的程序顺序已经不是底层机制中的执行顺序和结果,编程语言需要给开发者一种保证,这个保证简单来说就是一个线程的修改何时对其他线程可见,因此Java语言提出了JavaMemoryModel即Java内存模型,对于Java语言、JVM、编译器等实现者需要按照这个模型的约定来进行实现。Java提供了Volatile、synchronized、final等机制来帮助开发者保证多线程程序在所有处理器平台上的正确性。

在JDK1.5之前,Java的内存模型有着严重的问题,例如在旧的内存模型中,一个线程可能在构造器执行完成后看到一个final字段的默认值、volatile字段的写入可能会和非volatile字段的读写重排序。

所以在JDK1.5中,通过JSR133提出了新的内存模型,修复之前出现的问题。

重排序规则

volatile和监视器锁

是否可以重排序 第二个操作 第二个操作 第二个操作
第一个操作 普通读/普通写 volatile读/monitor enter volatile写/monitor exit
普通读/普通写 No
voaltile读/monitor enter No No No
volatile写/monitor exit No No

其中普通读指getfield, getstatic, 非volatile数组的arrayload, 普通写指putfield, putstatic, 非volatile数组的arraystore。

volatile读写分别是volatile字段的getfield, getstatic和putfield, putstatic。

monitorenter是进入同步块或同步方法,monitorexist指退出同步块或同步方法。

上述表格中的No指先后两个操作不允许重排序,如(普通写, volatile写)指非volatile字段的写入不能和之后任意的volatile字段的写入重排序。当没有No时,说明重排序是允许的,但是JVM需要保证最小安全性-读取的值要么是默认值,要么是其他线程写入的(64位的double和long读写操作是个特例,当没有volatile修饰时,并不能保证读写是原子的,底层可能将其拆分为两个单独的操作)。

final字段

final字段有两个额外的特殊规则

final字段的写入(在构造器中进行)以及final字段对象本身的引用的写入都不能和后续的(构造器外的)持有该final字段的对象的写入重排序。例如, 下面的语句是不能重排序的

x.finalField = v; ...; sharedRef = x;

final字段的第一次加载不能和持有这个final字段的对象的写入重排序,例如下面的语句是不允许重排序的

x = sharedRef; ...; i = x.finalField

内存屏障

处理器都支持一定的内存屏障(memory barrier)或栅栏(fence)来控制重排序和数据在不同的处理器间的可见性。例如,CPU将数据写回时,会将store请求放入write buffer中等待flush到内存,可以通过插入barrier的方式防止这个store请求与其他的请求重排序、保证数据的可见性。可以用一个生活中的例子类比屏障,例如坐地铁的斜坡式电梯时,大家按顺序进入电梯,但是会有一些人从左侧绕过去,这样出电梯时顺序就不相同了,如果有一个人携带了一个大的行李堵住了(屏障),则后面的人就不能绕过去了:)。另外这里的barrier和GC中用到的write barrier是不同的概念。

内存屏障的分类

几乎所有的处理器都支持一定粗粒度的barrier指令,通常叫做Fence(栅栏、围墙),能够保证在fence之前发起的load和store指令都能严格的和fence之后的load和store保持有序。通常按照用途会分为下面四种barrier

LoadLoad Barriers

Load1; LoadLoad; Load2;

保证Load1的数据在Load2及之后的load前加载

StoreStore Barriers

Store1; StoreStore; Store2

保证Store1的数据先于Store2及之后的数据 在其他处理器可见

LoadStore Barriers

Load1; LoadStore; Store2

保证Load1的数据的加载在Store2和之后的数据flush前

StoreLoad Barriers

Store1; StoreLoad; Load2

保证Store1的数据在其他处理器前可见(如flush到内存)先于Load2和之后的load的数据的加载。StoreLoad Barrier能够防止load读取到旧数据而不是最近其他处理器写入的数据。

几乎近代的所有的多处理器都需要StoreLoad,StoreLoad的开销通常是最大的,并且StoreLoad具有其他三种屏障的效果,所以StoreLoad可以当做一个通用的(但是更高开销的)屏障。

所以,利用上述的内存屏障,可以实现上面表格中的重排序规则

需要的屏障 第二个操作 第二个操作 第二个操作 第二个操作
第一个操作 普通读 普通写 volatile读/monitor enter volatile写/monitor exit
普通读 LoadStore
普通读 StoreStore
voaltile读/monitor enter LoadLoad LoadStore LoadLoad LoadStore
volatile写/monitor exit StoreLoad StoreStore

为了支持final字段的规则,需要对final的写入增加barrier

x.finalField = v; StoreStore; sharedRef = x;

插入内存屏障

基于上面的规则,可以在volatile字段、synchronized关键字的处理上增加屏障来满足内存模型的规则

volatile store前插入StoreStore屏障
所有final字段写入后但在构造器返回前插入StoreStore
volatile store后插入StoreLoad屏障
在volatile load后插入LoadLoad和LoadStore屏障
monitor enter和volatile load规则一致,monitor exit 和volatile store规则一致。
HappenBefore

前面提到的各种内存屏障对应开发者来说还是比较复杂底层,因此JMM又可以使用一系列HappenBefore的偏序关系的规则方式来说明,要想保证执行操作B的线程看到操作A的结果(无论A和B是否在同一个线程中执行), 那么在A和B之间必须要满足HappenBefore关系,否则JVM可以对它们任意重排序。

HappenBefore规则列表

HappendBefore规则包括

程序顺序规则: 如果程序中操作A在操作B之前,那么同一个线程中操作A将在操作B之前进行
监视器锁规则: 在监视器锁上的锁操作必须在同一个监视器锁上的加锁操作之前执行
volatile变量规则: volatile变量的写入操作必须在该变量的读操作之前执行
线程启动规则: 在线程上对Thread.start的调用必须在该线程中执行任何操作之前执行
线程结束规则: 线程中的任何操作都必须在其他线程检测到该线程已经结束之前执行
中断规则: 当一个线程在另一个线程上调用interrupt时,必须在被中断线程检测到interrupt之前执行
传递性: 如果操作A在操作B之前执行,并且操作B在操作C之前执行,那么操作A在操作C之前执行。
其中显示锁与监视器锁有相同的内存语义,原子变量与volatile有相同的内存语义。锁的获取和释放、volatile变量的读取和写入操作满足全序关系,所以可以使用volatile的写入在后续的volatile的读取之前进行。

可以利用上述HappenBefore的多个规则进行组合。

例如线程A进入监视器锁后,在释放监视器锁之前的操作根据程序顺序规则HappenBefore于监视器释放操作,而监视器释放操作HappenBefore于后续的线程B的对相同监视器锁的获取操作,获取操作HappenBefore与线程B中的操作。

总结

以上就是本文关于Java内存模型JMM详解的全部内容,希望对大家有所帮助。如有不足之处,欢迎留言指出。感谢朋友们对本站的支持!

(0)

相关推荐

  • Java 高并发三:Java内存模型和线程安全详解

    网上很多资料在描述Java内存模型的时候,都会介绍有一个主存,然后每个工作线程有自己的工作内存.数据在主存中会有一份,在工作内存中也有一份.工作内存和主存之间会有各种原子操作去进行同步. 下图来源于这篇Blog 但是由于Java版本的不断演变,内存模型也进行了改变.本文只讲述Java内存模型的一些特性,无论是新的内存模型还是旧的内存模型,在明白了这些特性以后,看起来也会更加清晰. 1. 原子性 原子性是指一个操作是不可中断的.即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其它线程干扰

  • Java内存模型JMM详解

    Java Memory Model简称JMM, 是一系列的Java虚拟机平台对开发者提供的多线程环境下的内存可见性.是否可以重排序等问题的无关具体平台的统一的保证.(可能在术语上与Java运行时内存分布有歧义,后者指堆.方法区.线程栈等内存区域). 并发编程有多种风格,除了CSP(通信顺序进程).Actor等模型外,大家最熟悉的应该是基于线程和锁的共享内存模型了.在多线程编程中,需要注意三类并发问题: ·原子性 ·可见性 ·重排序 原子性涉及到,一个线程执行一个复合操作的时候,其他线程是否能够看

  • JAVA内存模型(JMM)详解

    目录 前言 JAVA并发三大特性 可见性 有序性 原子性 Java内存模型真面目 Happens-Before规则 1.程序的顺序性规则 2. volatile 变量规则 3.传递性 锁的规则 5.线程 start() 规则 6.线程 join() 规则 使用JMM规则 方案一: 使用volatile 方案二:使用锁 小结: volatile 关键字 synchronized 关键字 总结 前言 开篇一个例子,我看看都有谁会?如果不会的,或者不知道原理的,还是老老实实看完这篇文章吧. @Slf4

  • Java内存模型知识详解

    1. 概述 多任务和高并发是衡量一台计算机处理器的能力重要指标之一.一般衡量一个服务器性能的高低好坏,使用每秒事务处理数(Transactions Per Second,TPS)这个指标比较能说明问题,它代表着一秒内服务器平均能响应的请求数,而TPS值与程序的并发能力有着非常密切的关系.在讨论Java内存模型和线程之前,先简单介绍一下硬件的效率与一致性. 2.硬件的效率与一致性 由于计算机的存储设备与处理器的运算能力之间有几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理

  • 学习Java内存模型JMM心得

    有时候编译器.处理器的优化会导致runtime与我们设想的不一样,为此Java对编译器和处理器做了一些限制,JAVA内存模型(JMM)将这些抽象出来,这样编写代码时就无需考虑那么多底层细节,并保证"只要遵循JMM的规则编写程序,其运行结果一定是正确的". JMM的抽象结构 在Java中,所有的实例.静态变量存储在堆内存中,堆内存是可以在线程间共享的,这部分也称为共享变量.而局部变量.方法定义参数.异常处理参数是在栈中的,栈内存不在线程间共享. 而由于编译器.处理器的优化,会导致共享变量

  • Java内存模型JMM与volatile

    目录 1.Java内存模型 2.并发三大特性 2.1.原子性 2.2.可见性 2.3.有序性 3.两个规则 3.1.happens-before规则 3.2.as-if-serial 4.volatile 4.1.volatile 禁止重排优化的实现 4.2.MESI缓存一致性协议 1.Java内存模型 JAVA定义了一套在多线程读写共享数据时时,对数据的可见性.有序性和原子性的规则和保障.屏蔽掉不同操作系统间的微小差异. Java内存模型(Java Memory Model)是一种抽象的概念,

  • Java内存模型(JMM)及happens-before原理

    我们知道java程序是运行在JVM中的,而JVM就是构建在内存上的虚拟机,那么内存模型JMM是做什么用的呢? 我们考虑一个简单的赋值问题: int a=100; JMM考虑的就是什么情况下读取变量a的线程可以看到值为100.看起来这是一个很简单的问题,赋值之后不就可以读到值了吗? 但是上面的只是我们源码的编写顺序,当把源码编译之后,在编译器中生成的指令的顺序跟源码的顺序并不是完全一致的.处理器可能采用乱序或者并行的方式来执行指令(在JVM中只要程序的最终执行结果和在严格串行环境中执行结果一致,这

  • javascript 内存模型实例详解

    本文实例讲述了javascript 内存模型.分享给大家供大家参考,具体如下: 我对于 JavaScript 的内存模型一直都比较困惑,很想了解在操作变量的时候,JS 是如何工作的.如果你和我有同样的困惑,希望这篇文章能给你一些启发. 译文,喜欢原文的可以直接拉到底部 当我们声明变量.初始化变量.更改变量值的时候,到底会发生什么?JavaScript 是如何实现这些基本的功能?最重要的是,我们如何才能理解这些基础知识? 本文将覆盖以下 4 个方面: JavaScript 原始数据类型的变量声明和

  • 详细分析Java内存模型

    目录 一.为什么要学习并发编程 二.为什么需要并发编程 三.从物理机中得到启发 四.Java 内存模型 五.原子性 5.1.什么是原子性 5.2.如何保证原子性 六.可见性 6.1.什么是可见性 6.2.如何保证可见性 七.有序性 7.1.什么是有序性 7.2.如何保证有序性 一.为什么要学习并发编程 对于 "我们为什么要学习并发编程?" 这个问题,就好比 "我们为什么要学习政治?" 一样,我们(至少作为学生党是这样)平常很少接触到,然后背了一堆 "正确且

  • 并发编程之Java内存模型

    目录 一.Java内存模型的基础 1.1 并发编程模型的两个关键问题 1.2 Java内存模型的抽象结构 1.3 从源代码到指令重排序 1.4 写缓冲区和内存屏障 1.4.1 写缓冲区 1.4.2 内存屏障 1.5 happens-before 简介 简介: Java线程之间的通信对程序员完全透明,内存可见性问题很容易困扰Java程序员,这一系列几篇文章将揭开Java内存模型的神秘面纱. 这一系列的文章大致分4个部分,分别是: Java内存模型基础,主要介绍内存模型相关基本概念 Java内存模型

  • Java内存模型之happens-before概念详解

    简介 happens-before是JMM的核心概念.理解happens-before是了解JMM的关键. 1.设计意图 JMM的设计需要考虑两个方面,分别是程序员角度和编译器.处理器角度: 程序员角度,希望内存模型易于理解.易于编程.希望是一个强内存模型. 编译器和处理器角度,希望减少对它们的束缚,以至于编译器和处理器可以做更多的性能优化.希望是一个弱内存模型. ​因此JSR-133专家组设计JMM的核心目标就两个: 为程序员提供足够强的内存模型对编译器和处理器的限制尽可能少 ​下面通过一段代

随机推荐