李林超博客
首页
归档
留言
友链
动态
关于
归档
留言
友链
动态
关于
首页
Java
正文
26.并发编程之有序性介绍
Leefs
2022-10-30 PM
723℃
0条
[TOC] ### 一、基本概念 对于一个线程的执行代码而言,我们总是习惯性认为代码的执行总是从上到下,有序执行。 但为了提升性能,编译器和处理器通常会对指令序列进行**重新排序**。 Java规范规定JVM线程内部维持**顺序化语义**,即只要程序的最终结果与它顺序化执行的结果一致,那么指令的执行顺序可以与代码顺序**不一致,此过程叫指令的重排序**。 **指令重排序类型** (1)**编译器优化的重排序**:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。 (2)**指令级并行的重排序**:现代处理器采用了指令级并行技术(ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。 (3)**内存系统的重排序**:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。 ![26.并发编程之有序性介绍01.jpg](https://lilinchao.com/usr/uploads/2022/10/1716828491.jpg) 上述的1属于编译器重排序,2和3属于处理器重排序。这些**重排序可能会导致多线程程序出现内存可见性问题**。 **指令重排序优缺点** + **优点**:JVM能根据处理器特性(CPU多级缓存系统、多核处理等)适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度发挥机器性能。 + **缺点**:指令重排序**可以保证串行语义一致**,但没有义务保证**多线程之间的语义一致**(即可能产生“脏读”)。 ### 二、代码解读指令重排 `JVM`会在不影响正确性的前提下,可以调整语句的执行顺序,思考下面一段代码 ```java static int i; static int j; // 在某个线程内执行如下赋值操作 i = ...; j = ...; ``` 可以看到,至于是先执行 i 还是 先执行 j ,对最终的结果不会产生影响。所以,上面代码真正执行时,既可以是 ```java i = ...; j = ...; ``` 也可以是 ```java j = ...; i = ...; ``` 这种特性称之为『指令重排』,多线程下『指令重排』会影响正确性。为什么要有重排指令这项优化呢?从 CPU 执行指令的原理来理解一下吧 ### 三、指令级并行原理 **指令重排序优化** 事实上,现代处理器会设计为一个时钟周期完成一条执行时间最长的 CPU 指令。为什么这么做呢?可以想到指令还可以再划分成一个个更小的阶段,例如,每条指令都可以分为: `取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回`这 5 个阶段。 ![26.并发编程之有序性介绍02.jpg](https://lilinchao.com/usr/uploads/2022/10/989160037.jpg) > 术语参考: > > instruction fetch (IF) > > instruction decode (ID) > > execute (EX) > > memory access (MEM) > > register write back (WB) 在不改变程序结果的前提下,这些指令的各个阶段可以通过重排序和组合来实现指令级并行,分阶段、分工正是提升效率的关键! **支持流水线的处理器** 现代 CPU 支持**多级指令流水线**,例如支持同时执行`取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回`的处理器,就可以称之为**五级指令流水线**。这时 CPU 可以在一个时钟周期内,同时运行五条指令的不同阶段(相当于一 条执行时间最长的复杂指令),IPC = 1,本质上,流水线技术并不能缩短单条指令的执行时间,但它变相地提高了 指令地吞吐率。 ![26.并发编程之有序性介绍03.png](https://lilinchao.com/usr/uploads/2022/10/3486907580.png) 大多数处理器包含多个执行单元,并不是所有计算功能都集中在一起,可以再细分为整数运算单元、浮点数运算单元等,这样可以把多条指令也可以做到并行获取、译码等,CPU 可以在一个时钟周期内,执行多于一条指令,IPC>1。 ### 四、指令重排序带来的问题 #### 4.1 诡异的结果 ```java int num = 0; boolean ready = false; // 线程1 执行此方法 public void actor1(I_Result r) { if(ready) { r.r1 = num + num; } else { r.r1 = 1; } } // 线程2 执行此方法 public void actor2(I_Result r) { num = 2; ready = true; } ``` I_Result 是一个对象,有一个属性 r1 用来保存结果,问,可能的结果有几种? + 情况1:线程1 先执行,这时 ready = false,所以进入 else 分支结果为 1 + 情况2:线程2 先执行 num = 2,但没来得及执行 ready = true,线程1 执行,还是进入 else 分支,结果为1 + 情况3:线程2 执行到 ready = true,线程1 执行,这回进入 if 分支,结果为 4(因为 num 已经执行过了) + 情况4:线程2 执行 ready = true,切换到线程1,进入 if 分支,相加结果为 0,再切回线程2 执行 num = 2 这种现象叫做指令重排,是 JIT 编译器在运行时的一些优化 + 这个现象需要通过大量测试才能复现:借助 java 并发压测工具jcstress(Java Concurrency Stress) + jmeter侧重对于接口整体的响应速度等进行测试,而JCStress框架能对某块逻辑代码进行高并发测试,更加侧重JVM,类库等领域的研究 #### 4.2 实操测试 **(1)搭建基本Maven骨架** 在项目终端输入如下命令: ```shell mvn archetype:generate -DinteractiveMode=false -DarchetypeGroupId=org.openjdk.jcstress -DarchetypeArtifactId=jcstress-java-test-archetype -DarchetypeVersion=0.5 -DgroupId=com.lilinchao -DartifactId=ordering -Dversion=1.0 ``` **(2)将生成的ordering文件导入到当前的Maven项目中** ![26.并发编程之有序性介绍04.jpg](https://lilinchao.com/usr/uploads/2022/10/447886861.jpg) **(3)ConcurrencyTest测试类中写入代码** ```java import org.openjdk.jcstress.annotations.*; import org.openjdk.jcstress.infra.results.I_Result; @JCStressTest // 标记此类为一个并发测试类 @Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok") @Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "!!!!") @State // 标记此类是有状态的 public class ConcurrencyTest { int num = 0; // volatile boolean ready = false; boolean ready = false; @Actor public void actor1(I_Result r) { if(ready) { r.r1 = num + num; } else { r.r1 = 1; } } @Actor public void actor2(I_Result r) { num = 2; ready = true; } } ``` **(4)将代码打成Jar包** ![26.并发编程之有序性介绍05.jpg](https://lilinchao.com/usr/uploads/2022/10/3041044956.jpg) **(5)在项目终端运行打好的Jar包** ``` java -jar jcstress.jar ``` 会输出我们感兴趣的结果,摘录其中一次结果: ``` *** INTERESTING tests Some interesting behaviors observed. This is for the plain curiosity. 2 matching test results. [OK] com.lilinchao.ConcurrencyTest (JVM args: [-XX:-TieredCompilation]) Observed state Occurrences Expectation Interpretation 0 698 ACCEPTABLE_INTERESTING !!!! 1 96,151,496 ACCEPTABLE ok 4 44,396,017 ACCEPTABLE ok [OK] com.lilinchao.ConcurrencyTest (JVM args: []) Observed state Occurrences Expectation Interpretation 0 763 ACCEPTABLE_INTERESTING !!!! 1 81,506,960 ACCEPTABLE ok 4 52,252,008 ACCEPTABLE ok *** FAILED tests Strong asserts were violated. Correct implementations should have no assert failures here. ``` 可以看到,出现结果为 0 的情况有 698 次,虽然次数相对很少,但毕竟是出现了 - 指令重排序操作不会对存在数据依赖关系的操作进行重排序。比如:a=1;b=a; 这个指令序列,由于第二个操作依赖于第一个操作,所以在编译时和处理器运行时这两个操作不会被重排序。 - 重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变。 比如:a=1;b=2;c=a+b这三个操作,第一步(a=1)和第二步(b=2)由于不存在数据依赖关系,所以可能会发生重排序,但是c=a+b这个操作是不会被重排序的,因为需要保证最终的结果一定是c=a+b=3。 指令重排序在单线程模式下是一定会保证最终结果的正确性, 但是在多线程环境下,问题就出来了。 ### 五、解决方法 volatile 修饰的变量,可以禁用指令重排 ```java import org.openjdk.jcstress.annotations.*; import org.openjdk.jcstress.infra.results.I_Result; @JCStressTest // 标记此类为一个并发测试类 @Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok") @Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "!!!!") @State // 标记此类是有状态的 public class ConcurrencyTest { int num = 0; volatile boolean ready = false;//加上 volatile 防止 ready 之前的代码指令重排 @Actor public void actor1(I_Result r) { if(ready) { r.r1 = num + num; } else { r.r1 = 1; } } @Actor public void actor2(I_Result r) { num = 2; ready = true; } } ``` **运行结果** ``` *** INTERESTING tests Some interesting behaviors observed. This is for the plain curiosity. 0 matching test results. ``` *思考:是否可以通过synchronized来解决该问题?* > 使用`synchronized并不能解决有序性`问题,**但是如果是该`变量`整个都在synchronized代码块的保护范围内**,那么变量就不会被多个线程同时操作,也不用考虑有序性问题!在这种情况下相当于解决了重排序问题!
标签:
并发编程
非特殊说明,本博所有文章均为博主原创。
如若转载,请注明出处:
https://lilinchao.com/archives/2536.html
上一篇
25.并发编程之Balking模式
下一篇
27.并发编程之volatile原理
取消回复
评论啦~
提交评论
栏目分类
随笔
2
Java
326
大数据
229
工具
31
其它
25
GO
47
标签云
Stream流
DataX
JavaSE
国产数据库改造
BurpSuite
Shiro
Scala
Netty
Java阻塞队列
Spark SQL
ClickHouse
稀疏数组
工具
HDFS
DataWarehouse
SQL练习题
LeetCode刷题
nginx
序列化和反序列化
Azkaban
Linux
MyBatis-Plus
Java
Hbase
Flume
Spark RDD
JVM
Sentinel
数学
Spring
友情链接
申请
范明明
庄严博客
Mx
陶小桃Blog
虫洞