请选择 进入手机版 | 继续访问电脑版
零一零零 门户 IT技术 Java并发 查看内容

Java并发编程|重排序-可见性和有序性问题根源

2020-7-1 11:07| 发布者: x0100| 查看: 741| 评论: 0

摘要: 并发编程的三大问题:原子性、可见性、有序性。 缓存不能及时刷新导致了可见性问题。 编译器为了优化性能而改变程序中语句的先后顺序,导致有序性问题。 而“缓存不能及时刷新“和“编译器为了优化性能而改变程序中 ...


并发编程的三大问题:原子性、可见性、有序性。

缓存不能及时刷新导致了可见性问题。

编译器为了优化性能而改变程序中语句的先后顺序,导致有序性问题。

而“缓存不能及时刷新“和“编译器为了优化性能而改变程序中语句的先后顺序”都是重排序的一种。

1. 重排序概念

在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。

从 java 源代码到最终实际执行的指令序列,会分别经历下面三种重排序:

  1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。

  2. 指令级并行的重排序。处理器将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

  3. 内存系统的重排序。处理器使用缓存和读/写缓冲区,使得加载和存储操作看上去可能是在乱序执行。

举例:如下代码执行过程中,程序不一定按照先A后B的顺序执行,经重排序之后可能按照先B后A的顺序执行。

  1. int a = 1;// A
  2. int b = 2;// B

2. 重排序规则

重排序需要遵守一定规则,以保证程序正确执行。

重排序遵守数据依赖性

数据依赖性:如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。

存在数据依赖性的三种情况:

  1. ① 写后读:a = 1;b = a; 写一个变量之后,再读这个位置。
  2. ② 写后写:a = 1;a = 2; 写一个变量之后,再写这个变量。
  3. ③ 读后写:a = b;b = 1;读一个变量之后,再写这个变量。

存在数据依赖关系的两个操作,不可以重排序。

数据依赖性只针对单个处理器中执行的指令序列和单个线程中执行的操作。

举例:

同一个线程中执行a=1;b=1; 不存在数据依赖性,可能重排序。
同一个线程中执行a=1;b=a; 存在数据依赖性,不可以重排序。

重排序遵守as-if-serial 语义

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

举例,以计算圆的面积为例:

  1. double pi = 3.14; // A
  2. double r = 1.0; // B
  3. double area = pi * r * r; // C

A和B重排序之后,程序的执行结果不会改变,所以允许A、B重排序。A和C重排序之后,程序的执行结果会改变,所以不允许A、C重排序。

笔者看来,遵守数据依赖性和as-if-serial 语义实质上是一回事。为了遵守 as-if-serial 语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。

3. 重排序带来的问题

重排序可以提高程序执行的性能,但是代码的执行顺序改变,可能会导致多线程程序出现可见性问题和有序性问题

举例:

  1. 初始状态:a = b = 0;x = y = 0
  2. Processor A:
  3. a = 1; // A1
  4. x = b; // A2
  5. Processor B:
  6. b = 2; // B1
  7. y = a; // B2

如上代码,Processor A和Processor B同时执行,最终却可能得到x = y = 0的结果。

原因分析:

第一步执行A1/B1将a=1写到缓冲区,此时写缓冲区还在等待其他写操作,不执行A3,所以内存中的a=0;

第二步执行A2/B2,处理器读取内存中的a,得到a=0;

虽然处理器 A 执行内存操作的顺序为:A1->A2,但内存操作实际发生的顺序却是:A2->A1。此时,处理器 A 的内存操作顺序被重排序了

4. JMM如何解决重排序问题

JMM处理重排序问题:

1)对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。

2)对于处理器重排序,JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障指令,来禁止特定类型的处理器重排序。

3)JMM根据代码中的关键字(如:synchronized、volatile)和J.U.C包下的一些具体类来插入内存屏障。

JMM 把内存屏障指令分为下列四类:

Store:数据对其他处理器可见(即:刷新到内存中)

Load:让缓存中的数据失效,重新从主内存加载数据

总结

在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。

从Java源代码到最终实际执行,要经历三种重排序:编译器优化的重排序、指令级并行的重排序、内存系统的重排序。

as-if-serial语义要求:不管怎么重排序,程序的执行结果不能被改变。

存在数据依赖关系的两个操作,不可以重排序。

重排序可能会导致多线程程序出现可见性问题和有序性问题。

JMM编译时在当位置会插入内存屏障指令来禁止特定类型的重排序。


鲜花

握手

雷人

路过

鸡蛋

最新评论


QQ|Archiver|手机版|小黑屋| 零一零零 ( 京ICP备20003964号 ) |网站地图

GMT+8, 2020-10-21 03:42 , Processed in 0.051041 second(s), 17 queries .

Powered by Discuz! X3.4

© 2001-2013 Comsenz Inc.

返回顶部