JMM理论二:相关概念——重排序、内存屏障、数据依赖、as-if-serial、final、Happen-Before

参考致谢:
https://blog.csdn.net/zjcjava/article/details/78406330
Java 并发编程 - cuzz’s blog
https://www.jianshu.com/p/8a58d8335270
java内存模型JMM理解整理 - 阿姆斯特朗回旋炮 - 博客园 (cnblogs.com)

1 重排序

1、计算机在执行程序时,为了提高并行度,编译器和处理器常常会对指令做重排序。

2、指令重排序的条件:
1)在单线程环境下不能改变程序的运行结果。
2)存在数据依赖关系的不允许重排序。
3)无法通过Happens-before原则推到出来的,才能进行指令的重排序。

3、重排序分为以下 3 种:

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

注意:

  1. 单线程环境里面确保程序最终执行的结果和代码执行的结果一致。
  2. 处理器在进行重排序时必须考虑指令之间的数据依赖性。

1.1 内存屏障

1、为了保证内存可见性,java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。
2、JMM把内存屏障指令分为下列四类:
在这里插入图片描述

注:

  1. StoreLoad Barriers是一个“全能型”的屏障,它同时具有其他三个屏障的效果。现代的多处理器大都支持该屏障(其他类型的屏障不一定被所有处理器支持)。
  2. 执行该屏障开销会很昂贵,因为当前处理器通常要把写缓冲区中的数据全部刷新到内存中(buffer fully flush)。

3、由于编译器和处理器都能执行指令重排序优化,我们可以通过插入内存屏障禁止在内存屏障前后执行重排序优化。内存屏障另一个作用是强制刷出各种 CPU 缓存数据,因此任何 CPU 上的线程都能读取到这些数据的最新版本。
在这里插入图片描述

1.2 数据依赖

1、如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。
2、数据依赖分下列三种类型:
在这里插入图片描述

注意:

  1. 上面三种情况,只要重排序两个操作的执行顺序,程序的执行结果将会被改变。
  2. 编译器和处理器在重排序时,会遵守数据依赖性,不会改变存在数据依赖关系的两个操作的执行顺序。
  3. 这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作。

1.3 as-if-serial语义

1、as-if-serial语义是指:不管怎么重排序,单线程程序的执行结果不能被改变。
2、编译器,runtime 和处理器都必须遵守as-if-serial语义,给我们创建了一个幻觉:单线程程序是按程序的顺序来执行的。
3、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之前。

在这里插入图片描述

2 final的内存含义

1、被final修饰的变量内存语义如下:

  • JMM禁止把Final域的写,重排序到构造器的外部。
  • 在一个线程中,初次读该对象和读该对象下的Final域,JMM禁止处理器重新排序这两个操作。

2、final语义在处理器中的实现:

  • 会要求编译器在final域的写之后,构造函数return之前插入一个StoreStore障屏。
  • 读final域的重排序规则要求编译器在读final域的操作前面插入一个LoadLoad屏障。

3 Happen-Before原则

1、Happen-Before被翻译成先行发生原则,意思就是当A操作在B操作之前,那么A操作的影响B也会知道。“影响”包括修改了内存中的共享变量的值、发送了消息、调用了方法等。

2、Happen-Before的规则

  • 程序次序规则(Program Order Rule):在一个线程内,程序的执行规则跟程序的书写规则是一致的,从上往下执行。
  • 管程锁定规则(Monitor Lock Rule):一个Unlock的操作肯定先于下一次Lock的操作。这里必须是同一个锁。同理我们可以认为在synchronized同步同一个锁的时候,锁内先行执行的代码,对后续同步该锁的线程来说是完全可见的。
  • volatile变量规则(volatile Variable Rule):对同一个volatile的变量,先行发生的写操作,肯定早于后续发生的读操作。
  • 线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作。
  • 线程中止规则(Thread Termination Rule):Thread对象的中止检测(如:Thread.join(),Thread.isAlive()等)操作,必行晚于线程中所有操作。
  • 线程中断规则(Thread Interruption Rule):对线程的interruption()调用,先于被调用的线程检测中断事件(Thread.interrupted())的发生。
  • 对象中止规则(Finalizer Rule):一个对象的初始化方法先于一个方法执行Finalizer()方法。
  • 传递性(Transitivity):如果操作A先于操作B、操作B先于操作C,则操作A先于操作C。

线程安全性保证

1、工作内存与主内存同步延迟现象导致可见性问题

  • 可以使用 synchronzied 或 volatile 关键字解决,它们可以使用一个线程修改后的变量立即对其他线程可见。

2、对于指令重排导致可见性问题和有序性问题

  • 可以利用 volatile 关键字解决,因为 volatile 的另一个作用就是禁止指令重排序优化。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值