李林超博客
首页
归档
留言
友链
动态
关于
归档
留言
友链
动态
关于
首页
Java
正文
26.并发编程之有序性介绍
Leefs
2022-10-30 PM
1303℃
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
NLP
4
标签云
机器学习
JavaSE
Java工具类
Hive
Scala
工具
MySQL
前端
Ubuntu
MyBatis-Plus
Java阻塞队列
Stream流
正则表达式
稀疏数组
Map
JavaScript
Linux
Git
Thymeleaf
Golang
Hadoop
Java编程思想
Nacos
gorm
Jenkins
Eclipse
数据结构和算法
数学
数据结构
栈
友情链接
申请
范明明
庄严博客
Mx
陶小桃Blog
虫洞
评论已关闭