Java并发编程原理与实战四十一:重排序 和 happens-before
一、概念理解
首先我们先来了解一下什么是重排序:重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。
从Java源代码到最终实际执行的指令序列,会分别经历下面3种重排序,如下图所示
上述的1属于编译器重排序,2和3属于处理器重排序。这些重排序可能会导致多线程程序出现内存可见性问题。在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果(这也是as-if-serial语义允许对存在控制依赖的操作做重排序的原因);但在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。
1)数据依赖性(针对单个处理器而已)
关于重排序,这里要先讲一个概念就是数据依赖性问题。如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。数据依赖分为下列3种类型,如下表所示。
上面3种情况,只要重排序两个操作的执行顺序,程序的执行结果就会被改变。前面提到过,编译器和处理器可能会对操作做重排序。编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑。
2)as-if-serial语义
as-if-serial语义的意思是:不管怎么重排序,(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。
为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序。as-if-serial语义把单线程程序保护了起来,as-if-serial语义使单线程程序员无需担心重排序会干扰他们,也无需担心内存可见性问题。
3)happens-before
如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。
对happens-before关系的具体定义如下。
① 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
    ②两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照 happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)。
上面的①是JMM对程序员的承诺。从程序员的角度来说,可以这样理解happens-before关系:如果A happens-before B,那么Java内存模型将向程序员保证——A操作的结果将对B可见,且A的执行顺序排在B之前。注意,这只是Java内存模型向程序员做出的保证!上面的②是JMM对编译器和处理器重排序的约束原则。正如前面所言,其实是在遵循一个基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行。因此,happens-before关系本质上和as-if-serial语义是一回事。
·as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。
     ·as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的。
      as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。
happens-before规则如下:
程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
    监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
    volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
    传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
    start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
    join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
二、例子分析
假设有两个线程分别调用同一个test对象的writer()和reader()。请问,b的值是什么?
(a) 1 
(b) 2 
(c) 1 or 2
 public class test{
    private boolean flag = false;
    private int a = 0;
    public void writer(){
            a = 1;
            flag = True;
    }
    public void reader(){
        if (flag){
            b = a + 1
        }
    }
}
这里主要涉及的是处理器重排序问题。当前处理器为了加速指令执行,会将部分指令重排序之后执行。
数据依赖
数据依赖是一个简单的概念,就是判断前后两行代码在数据上有否有依赖关系。例如:
num1 = 1                // (a)
num2 = 2                // (b)
result = num1 + num2    // (c)
显然,c 语句用到的 num1 和 num2 依赖 a 和 b。
数据依赖分三种:
- 1 store - load
 - 2 load - store
 - 3 store - store
 
如何判断是否有依赖,很简单,只用判断两个语句之间是否用到同一个变量,是否是写操作。
Happen before
JVM定义了一个概念叫做 happen before,意思是前一条执行的结果要对后一条执行可见。简单来说前一条执行完,才能执行后一条。但实际上为了提高处理速度,JVM弱化了这个概念,在有数据依赖的情况下,前一条执行完,才能执行后一条。
看下面的例子:
num1 = 1                // (a)
num2 = 2                // (b)
result = num1 + num2    // (c)
对于上述三条语句 a, b, c执行,单线程顺序执行的情况。
a happen before b
b happen before c。
根据传递性可以得出:
a happen before c
c指令要用到的 num1 和 num2 显然是依赖 a 和 b 的,典型的store-load。所以c指令必须等到 a 和 b 执行完才能执行。然而 a 和 b 并没有数据依赖,于是 JVM 允许处理器对 a 和 b 进行重排序。
a -> b -> c = 3
b -> a -> c = 3
那么happen before到底是什么?我的理解是happen before是JVM对底层内存控制抽象出一层概念。我们可以根据代码顺序来判断happen before的关系,而JVM底层会根据实际情况执行不同的 action (例如添加内存屏障,处理器屏障,阻止重排序又或者是不做任何额外操作,允许处理器冲排序)。通过这一层使得内存控制对程序员透明,程序员也不需要考虑代码实际执行情况,JVM会保证单线程执行成功,as-if-serial。
既然JVM已经透明了内存控制,那为什么要搞清楚这点,那就是JVM只保证单线程执行成功,而多线程环境下,就会出各种各样的问题。
答案
下面就用上述讲的分析一下最初的题目。
A线程执行:
    public void writer(){
            a = 1;              // (1)
            flag = True;        // (2)
    }
B线程执行:
    public void reader(){
        if (flag){              // (3)
            b = a + 1           // (4)
        }
    }
1.先考虑大多数人考虑的情况:
指令顺序:(1)-> (2) -> (3) -> (4),b = 1 +1 = 2
2.意想不到的情况 
对于A线程来说,语句 (1)和(2)并不存在任何数据依赖问题。因此处理器可以对其进行重排序,也就是指令 (2)可能会先于指令(1)执行。 
那么当指令按照(2)-> (3) -> (4) -> (1) 顺序,b = 0 +1 = 1
3.还有一种情况 
对于B线程,处理器可能会提前处理 (4),将结果放到 ROB中,如果控制语句(3)为真,就将结果从ROB取出来直接使用,这是一种优化技术,预测。 
所以指令执行顺序可能是 (4) -> x -> x ->x
看来4条语句都有可能最先被执行。
总结一下,在多处理器环境中,由于每个处理器都有自己的读写缓存区,所以会使部分数据不一致。JMM会有一系列 action 保证数据一致性,但是在多线程环境下,还是会有很多诡异的问题发生,这个时候就要考虑处理器,编译器重排序。
三、知识点总结
1,指令重排序
大多数现代微处理器都会采用将指令乱序执行(out-of-order execution,简称OoOE或OOE)的方法,
在条件允许的情况下,直接运行当前有能力立即执行的后续指令,避开获取下一条指令所需数据时造成的等待。
通过乱序执行的技术,处理器可以大大提高执行效率。
除了处理器,常见的Java运行时环境的JIT编译器也会做指令重排序操作,即生成的机器指令与字节码指令顺序不一致。
2,as-if-serial语义
As-if-serial语义的意思是,所有的动作(Action)都可以为了优化而被重排序,但是必须保证它们重排序后的结果和程序代码本身的应有结果是一致的。
Java编译器、运行时和处理器都会保证单线程下的as-if-serial语义。
ps:即指令好像是连续的,是对这种执行效果特性的一个说法。
为了保证这一语义,重排序不会发生在有数据依赖的操作之中。
3,内存访问重排序与内存可见性

计算机系统中,为了尽可能地避免处理器访问主内存的时间开销,处理器大多会利用缓存(cache)以提高性能。
即缓存中的数据与主内存的数据并不是实时同步的,各CPU(或CPU核心)间缓存的数据也不是实时同步的。
这导致在同一个时间点,各CPU所看到同一内存地址的数据的值可能是不一致的。 从程序的视角来看,就是在同一个时间点,各个线程所看到的共享变量的值可能是不一致的。 有的观点会将这种现象也视为重排序的一种,命名为“内存系统重排序”。
因为这种内存可见性问题造成的结果就好像是内存访问指令发生了重排序一样。
(执行了却不知道执行了和以为执行了却重排序没有执行造成相同效果)

4,内存访问重排序与Java内存模型
Java的目标是成为一门平台无关性的语言,即Write once, run anywhere. 但是不同硬件环境下指令重排序的规则不尽相同。
例如,x86下运行正常的Java程序在IA64下就可能得到非预期的运行结果。 为此,JSR-1337制定了Java内存模型(Java Memory Model, JMM),旨在提供一个统一的可参考的规范,屏蔽平台差异性。 从Java 5开始,Java内存模型成为Java语言规范的一部分。
根据Java内存模型中的规定,可以总结出以下几条happens-before规则。
(ps:内存模型即通过运行环境把一些可见性和重排序问题统一成一个标准描述)
Happens-before的前后两个操作不会被重排序且后者对前者的内存可见。

程序次序法则: 线程中的每个动作A都happens-before于该线程中的每一个动作B,其中,在程序中,所有的动作B都能出现在A之后。
监视器锁法则: 对一个监视器锁的解锁 happens-before于每一个后续对同一监视器锁的加锁。
volatile变量法则:对volatile域的写入操作happens-before于每一个后续对同一个域的读写操作。
线程启动法则: 在一个线程里,对Thread.start的调用会happens-before于每个启动线程的动作。
线程终结法则:线程中的任何动作都happens-before于其他线程检测到这个线程已经终结、或者从Thread.join调用中成功返回,或Thread.isAlive返回false。
中断法则: 一个线程调用另一个线程的interrupt happens-before于被中断的线程发现中断。
终结法则: 一个对象的构造函数的结束happens-before于这个对象finalizer的开始。
传递性: 如果A happens-before于B,且B happens-before于C,则A happens-before于C

Happens-before关系只是对Java内存模型的一种近似性的描述,它并不够严谨,但便于日常程序开发参考使用,
关于更严谨的Java内存模型的定义和描述,请阅读JSR-133原文或Java语言规范章节17.4。
除此之外,Java内存模型对volatile和final的语义做了扩展。
对volatile语义的扩展保证了volatile变量在一些情况下不会重排序,volatile的64位变量double和long的读取和赋值操作都是原子的。
对final语义的扩展保证一个对象的构建方法结束前,所有final成员变量都必须完成初始化(前提是没有this引用溢出)。
(ps:没有理解final的意思)
Java内存模型关于重排序的规定,总结后如下表所示。(ps:下表没看懂)

5,内存屏障
内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题。
Java编译器也会根据内存屏障的规则禁止重排序。
内存屏障可以被分为以下几种类型:
LoadLoad 屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
LoadStore 屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
StoreLoad 屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。
它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。
有的处理器的重排序规则较严,无需内存屏障也能很好的工作,Java编译器会在这种情况下不放置内存屏障。
为了实现上一章中讨论的JSR-133的规定,Java编译器会这样使用内存屏障。(ps:下表没看懂)

四、案例参考
https://blog.csdn.net/qq_32646795/article/details/78221064
Java并发编程原理与实战四十一:重排序 和 happens-before的更多相关文章
- Java并发编程原理与实战三十一:Future&FutureTask 浅析
		
一.Futrue模式有什么用?------>正所谓技术来源与生活,这里举个栗子.在家里,我们都有煮菜的经验.(如果没有的话,你们还怎样来泡女朋友呢?你懂得).现在女票要你煮四菜一汤,这汤是鸡汤, ...
 - Java并发编程原理与实战四十二:锁与volatile的内存语义
		
锁与volatile的内存语义 1.锁的内存语义 2.volatile内存语义 3.synchronized内存语义 4.Lock与synchronized的区别 5.ReentrantLock源码实 ...
 - Java并发编程原理与实战二十一:线程通信wait¬ify&join
		
wait和notify wait和notify可以实现线程之间的通信,当一个线程执行不满足条件时可以调用wait方法将线程置为等待状态,当另一个线程执行到等待线程可以执行的条件时,调用notify可以 ...
 - Java并发编程原理与实战四十四:final域的内存语义
		
一.final域的重排序规则 对于final域,编译器和处理器要遵循两个重拍序规则: 1.在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序 ...
 - Java并发编程原理与实战四十:JDK8新增LongAdder详解
		
传统的原子锁AtomicLong/AtomicInt虽然也可以处理大量并发情况下的计数器,但是由于使用了自旋等待,当存在大量竞争时,会存在大量自旋等待,而导致CPU浪费,而有效计算很少,降低了计算效率 ...
 - Java并发编程原理与实战四十五:问题定位总结
		
背景 “线下没问题的”. “代码不可能有问题 是系统原因”.“能在线上远程debug么” 线上问题不同于开发期间的bug,与运行时环境.压力.并发情况.具体的业务相关.对于线上的问题利用线上 ...
 - Java并发编程原理与实战四十三:CAS ---- ABA问题
		
CAS(Compare And Swap)导致的ABA问题 问题描述 多线程情况下,每个线程使用CAS操作欲将数据A修改成B,当然我们只希望只有一个线程能够正确的修改数据,并且只修改一次.当并发的时候 ...
 - Java并发编程原理与实战四:线程如何中断
		
如果你使用过杀毒软件,可能会发现全盘杀毒太耗时间了,这时你如果点击取消杀毒按钮,那么此时你正在中断一个运行的线程. java为我们提供了一种调用interrupt()方法来请求终止线程的方法,下面我们 ...
 - Java并发编程原理与实战五:创建线程的多种方式
		
一.继承Thread类 public class Demo1 extends Thread { public Demo1(String name) { super(name); } @Override ...
 
随机推荐
- 对it行业的一些看法
			
随着世界产业转移的加速,欧美.日本等发达国家将大量的软件开发业务转移到中国.印度等国家,随之而来的是这些国家对it人才的急切需求! 对比国内的大学生就业形势而言,无疑是it相关专业的毕业生就业压力较少 ...
 - Python入门:字符串的分片与索引、字符串的方法
			
这是关于Python的第3篇文章,主要介绍下字符串的分片与索引.字符串的方法. 字符串的分片与索引: 字符串可以用过string[X]来分片与索引.分片,简言之,就是从字符串总拿出一部分,储存在另一个 ...
 - net user 修改密码的坑
			
不多说 直接上图 自己偷懒修改 admin的密码.. 结果没注意 这个地方 能够输入全角字符. 造成密码 实质上是全角的 标点符号 ... 以后一定注意一些. 里面的坑..说多了 都是浪费时间 另外 ...
 - java 基础 --静态
			
1. 静态变量和静态代码块是在JVM加载类的时候执行的(静态变量被赋值,以后再new时不会重新赋值),执行且只执行一次2. 独立于该类的任何对象,不依赖于特定的实例,被类的所有实例(对象)所共享3. ...
 - 怎样使用ADO中的UpdateBatch方法(200分)
			
诸位: 我在使用ADO组件(ADOQuery.ADODataSet)的BatchUpdate模式时,系统竟不认识UpdateBatch.CancelBatch方法.这是怎么回事?我的运行环境是Win2 ...
 - matlab gradient 和 prctile
			
介绍两个matlab小函数: 1.gradient 借用别人的例子:例:>> x=[6,9,3,4,0;5,4,1,2,5;6,7,7,8,0;7,8,9,10,0]x = 6 ...
 - JVM调优及参数设置
			
(1)参数 -Xms:初始堆大小 -Xmx :最大堆大小 此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存 -Xmn :年轻代大小 整个堆大小=年轻代大小 + 年老代大小 + 持 ...
 - winrar 授权破解过期解决
			
RAR registration data Federal Agency for Education 1000000 PC usage license UID=b621cca9a84bc5deffbf ...
 - 单点登录(十三)-----实战-----cas4.2.X登录启用mongodb验证方式完整流程
			
我们在之前的文章中中已经讲到了正确部署运行cas server 和 在cas client中配置. 在此基础上 我们去掉了https的验证,启用了http访问的模式. 单点登录(七)-----实战-- ...
 - 解题:NOI 2007 社交网络
			
题面 先跑一边Floyd乘法原理统计任意两点间最短路数目,然后再枚举一次按照题意即可求出答案,会写那道JSOI2007就会这个 #include<cstdio> #include<c ...