Java内存模型 JMM

Java Memory Model

为什么需要JMM,它试图解决什么问题?

happen-before保证多线程操作可见性的机制。

没有内存已执行模型的语言,依赖于处理器的内存一致性模型,不同处理器之间又有很大差异,导致同一个程序运行在不同机器上表现不一致。随着JAVA被运行在越来越多的平台,内存模型定义存在很多模棱两可之处,对synchronized,volatile等,类似指令重排(可以是编译器优化,也可以是处理器乱序执行)时的行为,并没有清晰的规范。

换句话说:

  1. 不能保证多线程程序的正确性,例如DCL(Dobule-Checked Lock)失效问题,双检锁可能导致未完全初始化的对象被访问理论上叫并发编程中的安全发布(Safe Publication)失败
  2. 不能保证同一段程序在不同处理器架构上表现一致,例如有的处理器支持缓存一致性,有的不支持,有自己的内存排序模型。

 

一个操作在时间上先于另一个操作发生,并不意味着一个操作happen-before另一个操作。

问题场景

程序执行时,实际要跑在具体的处理器内核上。可以简单理解为,把本地变量的数据从内存加载到缓存、寄存器,然后运算结束写回主内存。

当多线程共享变量时,情况就复杂了。若处理器对某个共享变量进行了修改,可能只是体现在该内核的缓存里,这是个本地状态,而运行在其他内核上的线程,可能还是加载的就状态,这很可能导致一致性问题。从理论上说,多线程共享引入了复杂的数据依赖性,不管编译器、处理器怎么做重排序,都必须尊重数据依赖性的要求,否则就打破了正确性!这就是JMM要解决的问题

JMM内部的实现通常是依赖于所谓的内存屏障,通过禁止某些重排序的方式,提供内存可见性保证,也就是实现了各种happen-before规则。与此同时,更多复杂度在于,需要尽量确保各种编译器、各种体系结构的处理器,都能够提供一致的行为。

volatile为例,看看如何利用内存屏障实现JMM定义的可见性

对于一个volatile变量:

  • 对该变量的写操作之后,编译器会插入一个写屏障。
  • 对该变量的读操作之前,编译器会插入一个读屏障。

内存屏障能够在类似变量读、写操作之后,保证其他线程对volatile变量的修改对当前线程可见,或者本地修改对其他线程提供可见性。换句话说,线程写入,写屏障会通过类似强迫刷出处理器缓存的方式,让其他线程能够拿到最新数值。

 

DCL失效

    getInstance()总是返回非空值,并且每次调用返回相同的引用。如果getInstance()是初次调用,它会执行语句(5)构造一个LazySingleton实例并返回,如果getInstance()不是初次调用,如果不能在语句(2)处检测到非空值,那么必定将在语句(4)处就能检测到instance的非空值,因为语句(4)处于同步块中,对instance的写入--语句(5)也处于同一个同步块中。

    有读者可能要问了,既然根据第3条事实getInstance()总是返回相同的正确的引用,为什么还说DCL有问题呢?

    这里的关键是 尽管得到了LazySingleton的正确引用,但是却有可能访问到其成员变量 的 不正确值 ,具体来说LazySingleton.getInstance().getSomeField()有可能返回someField的默认值0。如果程序行为正确的话,这应当是不可能发生的事,因为在构造函数里设置的someField的值不可能为0。为也说明这种情况理论上有可能发生,我们只需要说明语句(1)和语句(7)并不存在happen-before关系。

posted @ 2019-06-10 16:57  vvf  阅读(193)  评论(0编辑  收藏  举报