[转]JVM学习之路(六)——指令重排序

本文深入探讨了指令重排序现象,解析其在多线程环境下的影响及Java内存模型如何规范指令重排序,确保程序正确执行。

六、指令重排序

根据以往学习多线程的经验,往往就会碰到这样一些例子:明明代码是按照想要的逻辑写的,但是一旦程序执行完后,就出现了一些意想不到的情况,那时只知道是多线程运行的时候出现了问题,具体怎么回事也没搞清楚过。在学习JVM的时候才发现导致这种问题的原因——指令重排序。

一、指令重排序案例重现:

有这样一段程序:

 
  1. int a = 0;
boolean flag = false;
  2.  
  3. //线程1
  4.  
  5. public void writer() {
  6.  
  7. a = 1;
  8.  
  9. flag = true;
  10.  
  11. }
  12.  
  13. //线程2
  14.  
  15. public void reader() {
  16.  
  17. if (flag) {
  18.  
  19. int i= a+1;
  20.  
  21. ...... }
  22.  
  23. }
 

线程1依次执行a=1,flag=true;线程2判断到flag==true后,设置i=a+1。根据代码语义,我们可能会推断此时i的值等于2,因为线程2在判断flag==true时,线程1已经执行了a=1;所以i的值等于a+1=1+1=2;但真实情况却不一定如此,引起这个问题的原因是线程1内部的两条语句a=1;flag=true;可能被重新排序执行,如图:

这就是“指令重排序”问题的案例重现。两个赋值语句尽管他们的代码顺序是一前一后的,但真正执行时却不一定按照代码顺序执行。

那我们就有疑问了,这个指令重排序不会让我们的程序乱套吗?我写的程序都不按我的代码流程走,这还要得?Java、CPU、内存之间的那一套严格的指令重排序规则让我们大可放心,这套规则规定了哪些可以重排,哪些不可以重排。这样,我们的程序就不会乱套了。

二、java程序从编译到执行会经历哪些重排序

1、编译器优化重排序:属于java编译器重排序,会按照JMM(Java内存模型)的规范严格进行,编译器重排序一般不会对程序的正确逻辑造成影响

2、指令级并行重排序:属于CPU重排序,CPU的事情JMM就不好管了,怎么办呢?JMM会要求java编译器在生成指令时加入内存屏障。可以将内存屏障理解为一个密不透风的保护罩,把不能重排序的java指令保护起来,那么处理器在遇到内存屏障保护的指令时就不会对它进行重排序了

3、内存系统重排序:属于CPU重排序,情况与2相似

经过这3个步骤的重排序,一开始编写的.java源代码才有了一个最终可执行的指令序列。

三、单线程中,不会被重排序的逻辑

由于以上3种情况,任意改变任何一个代码的顺序,结果都会大不相同。所以,对于单线程中这样的逻辑代码,是不会被重排序的。


---------------------
作者:zj_daydayup
来源:优快云
原文:https://blog.youkuaiyun.com/u012556994/article/details/81262790?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522158518126519724811817164%2522%252C%2522scm%2522%253A%252220140713.130056874..%2522%257D&request_id=158518126519724811817164&biz_id=0&utm_source=distribute.pc_search_result.none-task
版权声明:本文为作者原创文章,转载请附上博文链接!
 

### 指令重排序可能引发的问题 #### 1. **单线程环境下的潜在问题** 尽管在单线程环境中,`as-if-serial` 规范确保了指令重排不会改变程序的逻辑结果[^1],但如果开发者依赖特定的执行顺序来实现某些功能,则可能出现意外行为。例如,在涉及复杂计算或状态更新时,即使最终结果未受影响,中间变量的状态可能会因重排而变得不可预测。 #### 2. **多线程环境下的一致性破坏** 在多线程环境中,由于多个线程共享同一内存模型,指令重排可能导致数据一致性问题。具体表现为: - **可见性问题**:当一个线程写入某个变量后,另一个线程无法立即看到该修改的结果。这是因为写操作可能被延迟到本地缓存中完成,而不是直接同步到主内存[^4]。 - **原子性破坏**:对于非原子操作(如复合赋值 `i++`),如果这些操作被拆分为读取、修改和写回三个独立步骤,并且发生重排,则其他线程可能观察到不一致的状态[^3]。 #### 3. **条件竞争(Race Condition)** 当两个或更多线程访问同一个资源且至少有一个线程对其进行写操作时,如果没有适当同步机制保护,就可能发生条件竞争。这种现象通常由指令重排加剧,因为看似安全的操作序列实际上可能被打乱[^2]。 --- ### 多线程与并发编程中的影响 #### 1. **违反预期的行为** 开发人员往往假设代码按照书写顺序依次执行,但实际上 JVM 和 CPU 都会基于性能优化目的调整实际运行次序。这使得原本设计良好的算法在高负载或多核处理器条件下失效[^3]。 #### 2. **难以调试的错误** 由于这些问题通常是随机发生的(取决于硬件特性及时机等因素),因此很难重现和定位。即使是经验丰富的程序员也可能花费大量时间排查此类 bug[^4]。 #### 3. **解决方案——volatile关键字的作用** 通过声明字段为 `volatile` ,可以防止其周围的相关代码受到编译期以及运行期间产生的不必要的重新安排干扰,从而保障跨线程间的正确通信[^1] 。 下面是一个简单的例子展示如何利用它解决部分常见隐患: ```java public class VolatileExample { private static volatile boolean ready; private static int number; public static void main(String[] args) throws InterruptedException { Thread writer = new Thread(() -> { number = 42; // Step 1 ready = true; // Step 2 }); Thread reader = new Thread(() -> { while (!ready); // Spin until 'ready' is set to true System.out.println(number); }); writer.start(); reader.start(); writer.join(); // Wait for both threads to finish their tasks. reader.join(); } } ``` 在这个案例里,假如没有标记 `ready` 变量作为 `volatile` 类型的话,那么有可能会出现读者线程永远处于循环等待之中或者提前打印默认初始值零的情况,而这显然违背我们的初衷。 --- ### 总结 综上所述,虽然现代计算机体系结构支持一定程度上的灵活调度以提升效率,但对于那些高度依赖精确流程的应用来说却构成了挑战。尤其是在 Java 并发场景下,理解并妥善处理好指令重排序显得尤为重要。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值