JVM的重排序

重排序通常是编译器或运行时环境为了优化程序性能而采取的对指令进行重新排序执行的一种手段。重排序分为两类:编译期重排序运行期重排序,分别对应编译时和运行时环境。

在并发程序中,程序员会特别关注不同进程或线程之间的数据同步,特别是多个线程同时修改同一变量时,必须采取可靠的同步或其它措施保障数据被正确地修改,这里的一条重要原则是:不要假设指令执行的顺序,你无法预知不同线程之间的指令会以何种顺序执行

但是在单线程程序中,通常我们容易假设指令是顺序执行的,否则可以想象程序会发生什么可怕的变化。理想的模型是:各种指令执行的顺序是唯一且有序的,这个顺序就是它们被编写在代码中的顺序,与处理器或其它因素无关,这种模型被称作顺序一致性模型,也是基于冯·诺依曼体系的模型。当然,这种假设本身是合理的,在实践中也鲜有异常发生,但事实上,没有哪个现代多处理器架构会采用这种模型,因为它是在是太低效了。而在编译优化和CPU流水线中,几乎都涉及到指令重排序。

编译期重排序

编译期重排序的典型就是通过调整指令顺序,在不改变程序语义的前提下,尽可能减少寄存器的读取、存储次数,充分复用寄存器的存储值。

假设第一条指令计算一个值赋给变量A并存放在寄存器中,第二条指令与A无关但需要占用寄存器(假设它将占用A所在的那个寄存器),第三条指令使用A的值且与第二条指令无关。那么如果按照顺序一致性模型,A在第一条指令执行过后被放入寄存器,在第二条指令执行时A不再存在,第三条指令执行时A重新被读入寄存器,而这个过程中,A的值没有发生变化。通常编译器都会交换第二和第三条指令的位置,这样第一条指令结束时A存在于寄存器中,接下来可以直接从寄存器中读取A的值,降低了重复读取的开销。

重排序对于流水线的意义

现代CPU几乎都采用流水线机制加快指令的处理速度,一般来说,一条指令需要若干个CPU时钟周期处理,而通过流水线并行执行,可以在同等的时钟周期内执行若干条指令,具体做法简单地说就是把指令分为不同的执行周期,例如读取、寻址、解析、执行等步骤,并放在不同的元件中处理,同时在执行单元EU中,功能单元被分为不同的元件,例如加法元件、乘法元件、加载元件、存储元件等,可以进一步实现不同的计算并行执行。

流水线架构决定了指令应该被并行执行,而不是在顺序化模型中所认为的那样。重排序有利于充分使用流水线,进而达到超标量的效果。

确保顺序性

尽管指令在执行时并不一定按照我们所编写的顺序执行,但毋庸置疑的是,在单线程环境下,指令执行的最终效果应当与其在顺序执行下的效果一致,否则这种优化便会失去意义。

通常无论是在编译期还是运行期进行的指令重排序,都会满足上面的原则。

Java存储模型中的重排序

在Java存储模型(Java Memory Model, JMM)中,重排序是十分重要的一节,特别是在并发编程中。JMM通过happens-before法则保证顺序执行语义,如果想要让执行操作B的线程观察到执行操作A的线程的结果,那么A和B就必须满足happens-before原则,否则,JVM可以对它们进行任意排序以提高程序性能

volatile关键字可以保证变量的可见性,因为对volatile的操作都在Main Memory中,而Main Memory是被所有线程所共享的,这里的代价就是牺牲了性能,无法利用寄存器或Cache,因为它们都不是全局的,无法保证可见性,可能产生脏读。

volatile还有一个作用就是局部阻止重排序的发生,对volatile变量的操作指令都不会被重排序,因为如果重排序,又可能产生可见性问题。

在保证可见性方面,锁(包括显式锁、对象锁)以及对原子变量的读写都可以确保变量的可见性。但是实现方式略有不同,例如同步锁保证得到锁时从内存里重新读入数据刷新缓存,释放锁时将数据写回内存以保数据可见,而volatile变量干脆都是读写内存。

### Java 中 `volatile` 关键字防止重排序的原理与实现 #### 1. **什么是重排序** 在现代计算机体系结构中,为了提高性能,编译器和处理器可能会对指令进行重新排列(即重排序)。这种行为可能导致多线程环境下的程序运行结果不符合预期。例如,在一个线程中先写入某个变量再通知另一个线程去读取它,如果没有适当同步机制,可能由于重排序而导致另一线程看到的是旧值。 #### 2. **`volatile` 防止重排序的作用** `volatile` 是一种轻量级的同步机制,主要用于保证变量的可见性和禁止某些特定类型的重排序。具体而言: - 当一个变量被声明为 `volatile` 后,JVM 将其视为全局共享资源,并强制每次访问都从主内存中读取最新值,而非依赖于线程本地缓存中的副本。 - 此外,`volatile` 还引入了一种特殊的屏障——内存屏障(Memory Barrier),用于阻止编译器或 CPU 对涉及该变量的操作进行不当的重排序[^4]。 #### 3. **`volatile` 的重排序规则** 根据 JMM(Java Memory Model)定义的规则,以下是关于 `volatile` 如何影响重排序的关键点: - 如果第二个操作是对 `volatile` 变量的写入,则无论第一个操作为何,都不允许将其移动至 `volatile` 写之后的位置[^5]。 ```java non-volatile write → volatile write (不允许重排序) ``` - 若第一个操作是从 `volatile` 变量处读取数据,则后续任意其他操作均不得提前发生于此次读取之前[^5]。 ```java volatile read → any operation (不允许重排序) ``` - 特别需要注意的一条是:当存在连续两次分别针对不同 `volatile` 字段先后完成写入与读取动作时,它们之间同样受到严格限制而无法互换顺序。 ```java volatile write A → volatile read B (不允许重排序) ``` 这些规定共同构成了围绕着单个或者多个标记有 `volatile` 属性之字段周围所形成的一种偏序关系链路;从而使得即使是在高度并行化的计算场景下也能维持一定程度上的因果连贯性。 #### 4. **代码实例说明** 考虑如下示例代码片段来展示如何利用 `volatile` 来控制潜在危险的重排序现象: ```java public class VolatileExample { private boolean flag = false; private int data; public void writer() { data = 42; // Step 1: Write to a normal variable. flag = true; // Step 2: Write to the volatile variable. } public void reader() { if (flag) { // Step 3: Read from the volatile variable. System.out.println(data); // Step 4: Read from the normal variable. } } } ``` 在这个例子当中,“Step 1” 和 “Step 2”的次序至关重要。“data=42”这条语句代表初始化了一些重要状态信息给当前对象内部使用;紧随其后的“flag=true”,则充当信号灯告知外部世界本体已经准备就绪可供消费。假如没有采用 `volatile` 宣告的话,那么理论上有可能会出现这样的情况:“flag=true”被执行完毕以后紧接着又跑回前面修改"data"数值的动作尚未结束就被强行打断掉,最终造成消费者端观察到了错误的结果集。然而一旦加上了 `volatile` 注解后,按照前述提到过的那些约束条件可知,上述担忧便不再成立,因为现在有了强有力的保障措施确保所有必要的前期准备工作必定优先达成后再对外发布消息表明自己处于可用状态之中[^4]。 #### 5. **总结** 综上所述,通过运用 `volatile` 关键词不仅可以解决跨进程间通信过程中遇到的数据一致性难题,而且还能有效地规避因硬件架构特性引发的各种意外状况比如乱序执行等问题带来的困扰。不过值得注意的是虽然说相比起重量级锁方案来讲确实更加高效简洁一些,但是毕竟还是存在一定局限性的比如说不具备原子更新能力等等因此实际开发时候还需要结合具体情况灵活选用合适的技术手段才行[^1]。 --- ###
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值