Java内存模型 JMM
Java Memory Model
为什么需要JMM,它试图解决什么问题?
happen-before保证多线程操作可见性的机制。
没有内存已执行模型的语言,依赖于处理器的内存一致性模型,不同处理器之间又有很大差异,导致同一个程序运行在不同机器上表现不一致。随着JAVA被运行在越来越多的平台,内存模型定义存在很多模棱两可之处,对synchronized,volatile等,类似指令重排(可以是编译器优化,也可以是处理器乱序执行)时的行为,并没有清晰的规范。
换句话说:
- 不能保证多线程程序的正确性,例如DCL(Dobule-Checked Lock)失效问题,双检锁可能导致未完全初始化的对象被访问,理论上叫并发编程中的安全发布(Safe Publication)失败
- 不能保证同一段程序在不同处理器架构上表现一致,例如有的处理器支持缓存一致性,有的不支持,有自己的内存排序模型。
一个操作在时间上先于另一个操作发生,并不意味着一个操作happen-before另一个操作。
问题场景
程序执行时,实际要跑在具体的处理器内核上。可以简单理解为,把本地变量的数据从内存加载到缓存、寄存器,然后运算结束写回主内存。
当多线程共享变量时,情况就复杂了。若处理器对某个共享变量进行了修改,可能只是体现在该内核的缓存里,这是个本地状态,而运行在其他内核上的线程,可能还是加载的就状态,这很可能导致一致性问题。从理论上说,多线程共享引入了复杂的数据依赖性,不管编译器、处理器怎么做重排序,都必须尊重数据依赖性的要求,否则就打破了正确性!这就是JMM要解决的问题。
JMM内部的实现通常是依赖于所谓的内存屏障,通过禁止某些重排序的方式,提供内存可见性保证,也就是实现了各种happen-before规则。与此同时,更多复杂度在于,需要尽量确保各种编译器、各种体系结构的处理器,都能够提供一致的行为。
volatile为例,看看如何利用内存屏障实现JMM定义的可见性
对于一个volatile变量:
- 对该变量的写操作之后,编译器会插入一个写屏障。
- 对该变量的读操作之前,编译器会插入一个读屏障。
内存屏障能够在类似变量读、写操作之后,保证其他线程对volatile变量的修改对当前线程可见,或者本地修改对其他线程提供可见性。换句话说,线程写入,写屏障会通过类似强迫刷出处理器缓存的方式,让其他线程能够拿到最新数值。
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关系。