JMM底层之happnes-before原则

Posted 沛沛老爹

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JMM底层之happnes-before原则相关的知识,希望对你有一定的参考价值。

前言

JAVA内存模型中的有序性如果只靠volatile和synchronized,那么并发编程会非常麻烦,但是实际上写代码的时候却并没有这么复杂。原因在于Java语言中有一个happens-before原则。这个原则很重要,是判断数据是否存在竞争、线程是否安全的主要依据。

JVM内存模型

   happen-before是JMM最核心的概念,在了解happen-before原则之前,需要先了解java的内存模型。

*[图片来源于网络]
   java内存模型线程之间主要通过读-写共享变量来完成隐式通信。java中的共享变量是存储在主内存中的,本线程操作的是共享的变量副本,其工作方式是将共享内存中的变量拿出来放在工作内存,操作完成后,再将最新的变量放回共享变量,这时其他的线程就可以获取到最新的共享变量。
    假如线程C和线程B同时操作同一个变量,在理论上来讲,这个时候可能会出现脏读的情况出现。为了避免脏读的这种机制,JMM通过同步机制(控制不同线程间操作发生的先后顺序)来解决,从而对每个线程来讲都是可见的。

happnes-before原则

Java内存模型在JDK1.5版本中引入了Happens-Before原则。
我们编写的程序都要经过优化后(编译器和处理器会对我们的程序进行优化以提高运行效率)才会被运行,优化分为很多种,其中有一种优化叫做重排序,重排序需要遵守happens-before规则

代码重排

执行的代码,在编译和执行的时候,可能不是按照我们写的顺序来执行的。为了提高性能,编译器和处理器都会对代码进行重排序(这就是我们经常口头上说的环境导致的异常问题)。

一般情况下,我们会把重排分为三种:

  • 编译器优化重排
  • 指令级并行优化重排
  • 系统优化重排

什么是happnes-before

其实就是Java内存模型中定义的两项操作之间的排序关系,如果操作A happens-before操作B,就是在说发生在B之前的操作A产生的影响能被B观察到。
下面的例子简单的说明如果存在先行关系,就不用担心指令重排对两个线程的影响,不存在先行关系就要特别小心了:

以下操作在线程A中执行
i=1;
以下操作在线程B中执行
j=i;
以下操作在线程C中执行
i=2;

假设线程A中的操作“i=1” happnes-before 线程B的操作"j=i”", 那我们就可以确定在线程B的操作执行后,变量j的值一定是等于1,得出这个结论的依据有两个:

  • 1 是根据happnes-before原则,“i=1” 的结果可以被观察到;
  • 2 是线程C还没登场,线程A操作结束之后没有其他线程会修改变量i的值。

现在再来考虑线程C,我们依然保持线程A和B之间的先行发生关系,而C出现在线程A和B的操作之间,但是C与B没有先行发生关系,那j的值会是多少呢?

答案是不确定!
1和2都有可能,因为线程C对变量i的影响可能会被线程B观察到,也可能不会,这时候线程B就存在读取到过期数据的风险,不具备多线程安全性。

happnes-before 具体规则

1.程序顺序规则:

在一个线程内,按照控制流顺序,书写在前面的操作先行发生于书写在后面的操作。注意,这里说的是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。

程序顺序规则中所说的每个操作happens-before于该线程中的任意后续操作并不是说前一个操作必须要在后一个操作之前执行,而是指前一个操作的执行结果必须对后一个操作可见,如果不满足这个要求那就不允许这两个操作进行重排序

2.锁定规则:

一个unlock操作happens-before后面对同一个锁的lock操作。
这里必须强调的是“同一个锁",而“后面”是指时间上的先后。
下面的代码,在进入synchronized代码块之前,会自动加锁,在代码块执行完毕后,会自动释放锁。

3.volatile变量规则:

对一个volatile变量的写操作Happens-Before后面对这个变量的读操作。“后面”是指时间上的先后。

4.线程启动规则:

Thread对象的start()方法Happens-Before 此线程的每一个动作

5.线程终止规则:

线程中所有操作都Happens-Before对此线程的终止检测。

6.线程中断规则:

对线程的interrupt方法的调用Happens-Before被中断线程的代码检测到中断事件的发生。

7.对象终结规则:

一个对象的初始化完成,Happens-Before它的finalize方法的开始
线程A等待线程B完成(在线程A中调用线程B的join()方法实现),当线程B完成后(线程A调用线程B的join()方法返回),则线程A能够访问到线程B对共享变量的操作。

8.传递规则:

如果A Happens-Before B,并且B Happens-Before C,则A Happens-Before C。

时间先后和先行的区别,

比如有个共享变量x = 0,A线程在时间上先set其为1,线程B获取这个值,这个时候B获取的是1吗?答案是否定的,应该是不知道。因为这个操作没有任何先行规则匹配,虽然set操作先执行,但是不能确保get操作能获得修改后的值。修改方法很简单,加上synchronized或者定义成volatile。

时间上先发生,不一定是先行发生,那么先行发生,一定在时间上是先发生的吗?不一定,因为指令重排。比如int i = 1; j = 2。指令重排后j可能先被执行,但是根据程序次序规则,在一个线程内i = 1是先行于j=2的。

总结

时间上的先后顺序与Happens-Before原则之间基本没有太大的关系,所以我们衡量并发安全问题的时候不要受到时间顺序的干扰,一切必须以Happens-Before原则为准。

以上是关于JMM底层之happnes-before原则的主要内容,如果未能解决你的问题,请参考以下文章

Juc11_Java内存模型之JMM八大原子操作三大特性读写过程happens-before

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

Java——聊聊JUC中的Java内存模型(JMM)

Java——聊聊JUC中的Java内存模型(JMM)

Java——聊聊JUC中的Java内存模型(JMM)

Java——聊聊JUC中的Java内存模型(JMM)