为什么要学习内存模型
线程通信是指线程之间以何种机制来交换信息。在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递。
Java 的并发采用的是共享内存模型,Java 线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。
线程同步是指程序用于控制不同线程之间操作发生相对顺序的机制。
Java 内存模型规定了如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。
Java 内存模型, 屏蔽掉各种硬件和操作系统的内存访问差异,实现 Java 程序在各种平台下都能达到一致的内存访问效果。
Java 内存模型规范了 JVM 如何提供按需禁用缓存和编译优化的方法。具体来说,这些方法包括 volatile、synchronized 和 final 三个关键字,以及六项 Happens-Before 规则。
什么是as-if-serial
As-If-Serial 理解
参考URL: https://www.cnblogs.com/jiuya/p/10791903.html
as-if-serial语义的意思指:
不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不会改变。
编译器、runtime和处理器都必须遵守 as-if-serial 语义。
为了遵守 as-if-serial 语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。
但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。
为了具体说明,请看下面计算圆面积的代码示例:
计算圆面积的代码
double pi = 3.14; //A
double r = 1.0; //B
double area = pi * r * r; //C
如上图所示,A和C之间存在数据依赖关系,同时B和C之间也存在数据依赖关系。因此在最终执行的指令序列中,C不能被重排序到A和B的前面(C排到 A和B的前面,程序的结果将会被改变)。
但A和B之间没有数据依赖关系,编译器和处理器可以重排序A和B之间的执行顺序。下图是该程序的两种执行顺序:
as-if-serial语义把单线程程序保护了起来,遵守as-if-serial语义的编译器,runtime 和处理器共同为编写单线程程序的程序员创建了一个幻觉:单线程程序是按程序的顺序来执行的。as-if-serial语义使单线程程序员无需担心重排序会 干扰他们,也无需担心内存可见性问题。
Happens-Before 规则
Happens-Before规则与DCL失效原因分析
参考URL: https://www.jianshu.com/p/8446a398ca68
面试官:谈谈happens-before?
参考URL: https://baijiahao.baidu.com/s?id=1628346233476376109&wfr=spider&for=pc
面试官为什么总是问happens-before规则,看完这篇文章你就懂了
参考URL: https://baijiahao.baidu.com/s?id=1654963077694559106&wfr=spider&for=pc
深入理解happens-before规则、
参考URL: https://www.jianshu.com/p/9464bf340234
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 事件是否发生在同一个线程里。例如 A 事件发生在线程 1 上,B 事件发生在线程 2 上,Happens-Before 规则保证线程 2 上也能看到 A 事件的发生。
public class VolatileExample {
int x = 0 ;
volatile boolean v = false;
public void writer(){
x = 42;
v = true;
}
public void reader(){
if (v == true){
// 这里x会是多少呢
}
}
}
假设有两个线程A和B,A执行了writer方法,B执行reader方法,那么B线程中读到的变量x的值会是多少呢?
jdk1.5之前,线程B读到的变量x的值可能是0,也可能是42,jdk1.5之后,变量x的值就是42了。原因是jdk1.5中,对volatile的语义进行了增强。来看一下happens-before规则在这段代码中的体现。
-
规则一:程序的顺序性规则
在一个线程内一段代码的执行结果是有序的。就是还会指令重排,但是随便它怎么排,结果是按照我们代码的顺序生成的不会变! -
规则二:volatile规则
对一个volatile变量的写操作,happens-before后续对这个变量的读操作。 -
规则三:传递性规则
如果A happens-before B,B happens-before C,那么A happens-before C。 -
管程中锁的规则
就是无论是在单线程环境还是多线程环境,对于同一个锁来说,一个线程对这个锁解锁之后,另一个线程获取了这个锁都能看到前一个线程的操作结果!(管程是一种通用的同步原语,synchronized就是管程的实现)锁操作是具备happens-before关系的,解锁操作happens-before之后对同一把锁的加锁操作。实际上,在解锁的时候,JVM需要强制刷新缓存,使得当前线程所修改的内存对其他线程可见。
-
线程 start() 规则
主线程A启动线程B,线程B中可以看到主线程启动B之前的操作。也就是start() happens before 线程B中的操作。 -
规则六:线程join()规则
主线程A等待子线程B完成,当子线程B执行完毕后,主线程A可以看到线程B的所有操作。也就是说,子线程B中的任意操作,happens-before join()的返回。举例:线程t1写入的所有变量,在任意其它线程t2调用t1.join()成功返回后,都对t2可见。
-
线程中断规则:对线程interrupt()方法的调用 先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。
-
对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
这8条原则摘自《深入理解Java虚拟机》。
总结:这8条规则中,前4条规则是比较重要的,后4条规则都是显而易见的。
Java内存模型底层怎么实现的?
主要是通过内存屏障(memory barrier)禁止重排序的,即时编译器根据具体的底层体系架构,将这些内存屏障替换成具体的 CPU 指令。**对于编译器而言,内存屏障将限制它所能做的重排序优化。而对于处理器而言,内存屏障将会导致缓存的刷新操作。**比如,对于volatile,编译器将在volatile字段的读写操作前后各插入一些内存屏障。
as-if-serial与happens-before的区别
as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。
as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的。
参考
一夜搞懂 | Java 内存模型与线程
参考URL: https://www.cnblogs.com/xcynice/p/java_nei_cun_mo_xing_yu_xian_cheng.html