JMM之happens-before整理

Java内存模型(Java Memory Model,简称JMM)是围绕着在并发过程中如何处理原子性,可见性和有序性这三个特征来建立的;

 

其中关于JMM中的有序性这一特性的处理,在《深入理解Java虚拟机》12.3.6先行发生原则有这么一段话,如下:

JMM之happens-before整理

 

从JDK 5开始,Java使用新的JSR-133内存模型,JSR-133使用happens-before的概念来指定两个操作之间的执行顺序,由于这两个操作可以在一个线程之内,也可以是在不同线程之间;因此,JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证;

在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系;

参考:[https://www.cs.umd.edu/~pugh/java/memoryModel/jsr133.pdf]

   [https://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html]

 

Happens-Before原则

  • 如果一个操作Happens-Before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前;即如果A happens-before B,那么JMM可以保证A操作的结果对B可见,且A的执行顺序排在B之前;

  伪代码如下:

// 以下操作在线程A中完成
i = 1;

// 以下操作在线程B中完成
j = i;

  假设线程A中的操作“i = 1”先行发生于线程B的操作”j = 1“,那么我们就可以确定在线程B的操作执行后,变量j的值一定等于1;

  根据先行发生原则,线程A的”i = 1“的结果可以被线程B观察到,还有就是线程A结束后,没有其他线程会修改变量i的值;

  • 两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行;如果重排序之后的执行结果,与按Happens-Before关系来执行的结果一致,那么这种重排序并不非法,JMM允许这种重排序,即JMM对编译器和处理器重排序的约束原则;

  摘自《Java并发编程的艺术》的3.2.3 程序顺序规则

  JMM之happens-before整理

  伪代码如下:

double pi = 3.14; //A
double r = 1.0; //B
double area = pi * r * r; //C 

  这个例子存在3个happens-before关系,如下:

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

  在这三个happens-before关系中,第2个和第3个的对执行结果顺序依赖是必须的,而第1个不是必要的;

注:这里的提到的两个操作可以是在一个线程内,也可以是在不同的线程内;

 

重排序

重排序分为下面两类

  • 会改变程序执行结果的重排序;
  • 不会改变程序执行结果的重排序;

 

JMM对于这两种不同性质的重排序,采取了不同的策略,如下:

  • 对于会改变程序执行结构的重排序,JMM要求编译器和处理器必须禁止这种重排序;
  • 对于不会改变程序执行结构的重排序,JMM对编译器和处理不做要求(JMM允许这种重排序);

 

as-if-serial:

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

例子如下:

double pi = 3.14; //A
double r = 1.0; //B
double area = pi * r * r; //C 

这个例子存在3个happens-before关系,如下:

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

在这三个happens-before关系中,A和C之间存在数据依赖关系,同时B和C之前也存在数据依赖关系,因此在最终执行的指令序列中,C不能被重排序到A和B前面;

 

先行发生规则

  • 程序次序规则
    • 在一个线程内,按照代码顺序,写在前面的操作先行发生于写在后面的操作,即前一个操作的结果可以被后续的操作获取;如:前面一个操作把变量a赋值为1,那后面一个操作肯定能知道a已经变成了1;
  • 管程锁定规则
    • 同一个锁,一个unlock操作先行发行于后面(这里的“后面”指时间上的前后)对同一个锁的lock操作;
  • volatile变量规则
    • 对一个volatile变量的写操作先行发生于后面对这个变量的读操作,前面的写对后面(这里的“后面”是指时间上的先后)的读是可见的;

  • 线程启动规则
    • Thread对象的start方法先行发生于此线程的每一个动作;
  • 线程终止规则
    • 线程中的所有操作都先行发生于对此线程的终止检测,通过Thread::join()方法是否结束、Thread::isAlive()的返回值等手段检测线程是否已经终止执行;
  • 线程中断规则
    • 对线程interrupt方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread#interruptred方法检测是否有中断发生;

      Thread#interrupted静态方法,判断线程是否被中断,并清除当前中断状态,这个方法做了两件事:

    1. 返回当前线程的中断状态;
    2. 将当前线程的中断状态设为false;

    JMM之happens-before整理

 

    Thread#interrupt方法仅仅是设置线程的中断状态为true,不会停止线程;

    JMM之happens-before整理

     其中,当对一个线程调用Thread#interrupt方法时,如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,被设置中断标志的线程将继续正常运行,不受影响,Thread#interrupt并不能真正的中断线程,需要被调用的线程自己进行配合才行;如果线程处于被阻塞状态(例如调用sleep,wait,join等方法),在别的线程中调用当前线程对象的interrupt方法,那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常;

  • 对象终止规则
    • 一个对象的初始化完成(构造方法执行结束)先行发生于它的finalize方法的开始;即对象没有完成初始化之前是不能调用finalized方法的;
  • 传递性
    • 如果操作A先发生于操作B,操作B先发生于操作C,那么就可以得出操作A先行发生于操作C的结论;  

如果代码中两个操作之间的关系不在此列,并无法从以上规则推导出来,则它们就没有顺序性保障,虚拟机可以对它们随意地进行重排序;

 

上一篇:技本功丨解析范式(1NF-4NF),科普得如此直白易懂,别拦着我要学习~


下一篇:227. Basic Calculator II