JVM技术专题深入研究JMM的实现原理之Happens-Before原则和As-If-Serial语义「入门篇」

Posted 浩宇の天尚

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM技术专题深入研究JMM的实现原理之Happens-Before原则和As-If-Serial语义「入门篇」相关的知识,希望对你有一定的参考价值。

前提概要

Happens-Before是JMM最核心的概念,所以在了解happens-before原则之前,首先需要了解java的内存模型

JMM内存模型

Java内存模型是共享内存的并发模型线程之间主要通过读-写共享变量来完成隐式通信

Java中的共享变量是存储在内存中的,多个线程由其工作内存,其工作方式是将共享内存中的变量拿出来放在工作内存,操作完成后,再将最新的变量放回共享变量,这时其他的线程就可以获取到最新的共享变量。


从横向去看看,线程A和线程B就好像通过共享变量在进行隐式通信。这其中有很有意思的问题,如果线程A更新后数据并没有及时写回到主存,而此时线程B读到的是过期的数据,这就出现了 “脏读” 现象


为避免脏读,可以通过同步机制(控制不同线程间操作发生的相对顺序)来解决或者通过volatile关键字使得每次volatile变量都能够强制刷新到主存,从而对每个线程都是可见的。

重排序

执行程序时,为了提高性能,编译器和处理器常常会对指令进行重排序。一般重排序可以分为如下三种:如图,1.属于编译器重排序,而2和3统称为处理器重排序


这些重排序会导致线程安全的问题,一个很经典的例子就是DCL问题。JMM的编译器重排序规则会禁止一些特定类型的编译器重排序;针对处理器重排序,编译器在生成指令序列的时候会通过插入内存屏障指令来禁止某些特殊的处理器重排序

  • (1)编译器优化的重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;
  • (2)指令级并行的重排序:现代处理器采用指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序
  • (3)内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行的。

举个例子:

	public double rectangleArea(double length , double width)
		double leng;
		double wid;
		leng=length;//A
		wid=width;//B
		double area=leng*wid;//C
		return area;
	

由于A,B之间没有任何关系,对最终结果也不会存在关系,它们之间执行顺序可以重排序。因此可以执行顺序可以是A->B->C或者B->A->C执行最终结果都是3.14,即A和B之间没有数据依赖性。

因为A  happens-before  B,所以A操作产生的结果leng一定要对B操作可见,但是现在B操作并没有用到length,所以这两个操作可以重排序,那A操作是否可以和C操作重排序呢,如果A操作和C操作进行了重排序,因为leng没有被赋值,所以leng=0,area=0*wid也就是area=0;这个结果显然是错误的,所以A操作是不能和C操作进行重排序的(这就是注2中说的前一个操作的执行结果必须对后羿操作可见,如果不满足这个要求就不允许这两个操作进行重排序)

什么是happens-before(面向JMM的规范原则和编译器层面原则) 多线程场景下

JMM可以通过happens-before关系提供跨线程的内存可见性保证如果A线程的写操作a与B线程的读操作b之间存在happens-before关系,尽管a操作和b操作在不同的线程中执行,但JMM向程序员保证a操作将对b操作可见)。

具体的定义:

  • 1)如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前
  • 2)两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(JMM允许这种重排序)。

具体的规则:

  • (1) 程序顺序规则:线程中每个操作,happens-before该线程任意后续操作
  • (2) 监视器锁规则:对锁的解锁,happens-before于随后对这个锁的加锁
  • (3) volatile变量规则:对volatile域的写happens-before于任意后续对这个volatile域的读/写
// 对一个volatile变量的写操作happen-before对此变量的任意操作:
volatile int a;
a = 1; //1
b = a;  //2
//如果线程1 执行1,“线程2”执行了2,并且“线程1”执行后,“线程2”再执行,那么符合“volatile的
//happen-before原则”所以“线程2”中的a值一定是1。
  • (4) 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C
  • (5) start()规则如果线程A执行操作ThreadB.start()启动线程B),那么A线程的 ThreadB.start() 操作happens-before于线程B中的任意操作。
  • (6) Join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从**ThreadB.join()**操作成功返回。
  • (7) 程序中断规则:对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生
  • (8) 对象finalize规则:一个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()方法的开始。

happens-before具有传递规则

利用程序顺序规则(规则1)存在三个happens-before关系:

  • A happens-before B
  • B happens-before C
  • A happens-before C

这里的第三个关系是利用传递性进行推论的。这里的第三个关系是利用传递性进行推论的

volatile int var;
int b;
int c;
b = 4; //1
var = 3; //2
c = var; //3
c = b; //4

假设“线程1”执行//1 //2这段代码,“线程2”执行//3 //4这段代码。如果某次的执行顺序如下:

//1 //2 //3 //4。那么有如下推导( hd(a,b)表示a happen-before b):

  • 因为有hd(//1,//2) 、hd(//3,//4) (单线程的happen-before原则)
  • 且hd(//2,//3) (volatile的happen-before原则)
  • 所以有 hd(//1,//3),可导出hd(//1,//4) (happen-before原则的传递性)
  • 所以变量c的值最后为4
    如果某次的执行顺序如下:

//1 //3 //2// //4 那么最后4的结果就不能确定喽。其原因是 //3 //2 直接符合上述八大原则中的任何一个,不能通过传递性推测出来什么。


A happens-before B,定义1要求A执行结果对B可见,并且A操作的执行顺序在B操作之前,但与此同时利用定义中的第二条,A,B操作彼此不存在数据依赖性,两个操作的执行顺序对最终结果都不会产生影响,在不改变最终结果的前提下,允许A,B两个操作重排序,即happens-before关系并不代表了最终的执行顺序

As-If-Serial语义的介绍(面向处理器的语义规范)单线程场景下

不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果不能被改变。编译器,runtime 和处理器都必须遵守as-if-serial语义。

为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作可能被编译器和处理器重排序

以上是关于JVM技术专题深入研究JMM的实现原理之Happens-Before原则和As-If-Serial语义「入门篇」的主要内容,如果未能解决你的问题,请参考以下文章

JVM技术专题深入研究JVM内存逃逸原理分析「研究篇」

Java技术专题-JVM研究系列,JVM深入研究挖掘课题

Java技术专题-JVM研究系列,JVM深入研究挖掘课题

JVM技术专题深入研究JVM挖掘知识体系系列「补充篇」

JVM技术专题 深入学习JIT编译器实现机制「 原理篇」

JVM技术专题「原理专题」深入分析Java中finalize方法的作用和底层原理