什么是Java内存模型
内存结构指JVM中堆、栈等。
“工作内存”不是物理上存在的。(主内存、工作内存等概念)
导致可见性的原因是缓存,导致有序性的原因是编译优化,解决可见性、有序性最直接的办法就是禁用缓存和编译优化,但是这样问题虽然解决了,程序的性能就堪忧了。合理的方案应该是按需禁用缓存以及编译优化。
Java内存模型是个很复杂的规范(jsr[JavaSpecification Requests]规范),本质上可以理解为,Java内存模型规范了JVM如何按需禁用缓存和编译优化的方法。具体来说这,这些方法包括volatile、synchronized、final三个关键字,以及六项Happens-Before规则。
Java内存模型底层主要是通过内存屏障(memory barrier)禁止重排序的,即时编译器根据具体的底层体系架构,将这些内存屏障替换成具体的CPU指令。
对于编译器而言,内存屏障将限制它所能做的重排序优化。对于处理器而言,内幕才能屏障将会导致缓存的刷新操作。
使用volatile的困惑
volatile关键字并不是Java语言的特产,C语言也有,它最原始的意义就是禁用CPU缓存。
声明一个volatile变量volatile int x = 0,它表达的是:告诉编译器,对这个变量的读写,不能使用CPU缓存,必须从内存中读取或者写入。(volatile修饰的变量刷新回内存的时候,所有CPU缓存里的值都会失效,下次访问到的都是最新新的)
Java内存模型在1.5版本对volatile语义进行了增强-一项Happens-Before规则。
Happens-Before规则
字面意思“先行发生”。Happens-Before并不是说前面一个操作发生在后续操作的前面,真正表达的是:前面一个操作的结果对后续操作是可见的。比较正式的说法是:Happens-Before约束了编译器的优化行为,虽允许编译器优化,但是要求编译器优化后一定遵守Happens-Before规则。
Happens-Before规则和程序员相关的规则一共有八项,都是关于可见性的。
1.程序的顺序性规则
在一个线程中,按照程序顺序,前面的操作Happens-Before于后续的任意操作。比较符合单线程里面的思维:程序前面对某个变量的修改一定是对后续操作可见的。(程序控制流顺序而不是程序代码顺序(分支、循环))
2.volatile变量规则
对一个volatile变量的写操作,Happens-Before于后续对这个volatile变量的读操作。(表面是禁用缓存的意思,须关联规则3)。【不保证原子性,但保证可见性、有序性】
3.传递性
如果A Happens-Before B,且B Happens-Before C,那么A Happens-Before C。
这是1.5版本对volatile语义的增强,1.5并发工具包(java.util.concurrent)就是靠volatile语义来搞定可见性的。
4.管程中锁的规则
对同一个锁的解锁Happens-Before于后续对这个锁的加锁。
管程是一种通用的同步原语,在Java中指的就是synchronized,synchronized是Java里对管程的实现。
管程中的锁在Java里是隐式实现的,在进入同步块之前,会自动加锁monitorentor,而在代码块执行完或异常会自动释放锁monitorexit,加锁以及释放锁都是编译器实现的。
5.线程start()规则[线程启动]
主线程A启动子线程B后,子线程B能够看到主线程在启动子线程前的操作。
如果线程A调用线程B的start()方法(即在线程A中启动线程B),那么该start()操作Happens-Before于线程B中的任意操作。
6.线程join()规则[线程等待]
主线程A等待子线程B完成(主线程A通过调用子线程B的join()方法实现),当子线程B完成后(主线程A中join()方法返回),主线程能够看拿到子线程的操作。所谓的“看到”,指的是对共享变量的操作。
如果在线程A中,调用线程B的join()并成功返回,那么线程B中的任意操作Happens-Before于该join()操作的返回。
线程中的所有操作都先行发生于对此线程的终止检测。通过Thread.join()方法结束、Thread.isAlive()返回值等手段检测线程已经终止执行。
7.线程中断规则
对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。可以通过Thread.interrupted()方法检测到是否有中断发生。
8.对象终结规则
一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
被忽视的final
final修饰变量时,初衷是告诉编译器:这个变量生而不变,可以可劲儿优化。Java编译器在1.5以前的版本的确优化的很努力,以至于都优化错了。(双重检查创建单例,构造函数的错误重排导致线程可能看到final变量的值会变化(指令重排导致访问到未初始化完的对象))
在1.5以后Java内存模型对final类型变量的重排进行了约束。现在只要提供正确构造函数没有“逸出”(对封装性的破坏),就不会出现问题。
1 | final int x; |
总结
Java的内存模型是并发编程领域的一次重要创新,之后C++、C#、Golang等高级语言都开始支持内存模型。Java内存模型里面。最晦涩的部分是Happens-Before规则。Happens-Before规则最初是在Time, Clocks, and the Ordering of Events in a Distributed System论文中提出来的,论文中,Happens-Before的语义是一种因果关系。在现实世界中,如果A事件时导致B事件的起因,那么A事件一定是先于(Happens-Before)B事件发生的,这个就是Happens-Before语义的现实理解。
在Java语言里面,Happens-Before的语义本质上是一种可见性,A Happens-Before B意味着A事件对B事件来说是可见的,无论A事件和B事件是否发生在同一个线程里。
Java内存模型主要分为两部分,一部分面向编写并发程序的应用开发人员(这部分内容的核心就是Happens-Before规则),另一部分是面向JVM的实现人员。