在Java并发编程中,有序性是指程序执行的顺序与代码书写的顺序相一致。不过在真实运行中,编译器和处理器常为了提升性能而对指令进行重排,从而破坏了原本的代码顺序。理解什么是有序性以及指令重排的原理,是写好线程安全程序的基础。
什么是有序性
Java内存模型定义了线程之间如何通过内存进行交互,其中可见性、原子性和有序性并称为并发三大特性。有序性具体指:在单线程环境下,程序看起来是按照代码的先后顺序执行的;但在多线程环境下,如果没有正确同步,一个线程观察到的另一个线程的操作顺序可能是错乱的。
为什么会发生指令重排
指令重排是指编译器和处理器在不改变单线程语义的前提下,重新安排指令的执行顺序。主要来源有两类:
- 编译器优化:JIT编译器在生成字节码或机器码时,会调整指令顺序以减少寄存器空闲或提高缓存命中率。
- 处理器执行:现代CPU采用流水线、乱序执行技术,只要数据依赖关系满足,就会并行或乱序执行指令。
重排示例
以下代码在单线程中结果固定,但在多线程未同步时可能出问题:
class ReorderDemo {
int a = 0;
boolean flag = false;
// 线程1执行
void writer() {
a = 1; // 步骤1
flag = true; // 步骤2
}
// 线程2执行
void reader() {
if (flag) { // 步骤3
int i = a; // 步骤4,可能读到0
}
}
}
由于步骤1和步骤2之间没有依赖,编译器或CPU可能将flag=true提前到a=1之前,导致线程2看到flag为true时a仍是0。
Java如何保障有序性
Java内存模型使用happens-before规则来约束重排,确保某些操作在另一些操作之前完成。常见手段包括:
| 机制 | 说明 |
|---|---|
| volatile | 对volatile变量的写操作happens-before后续对该变量的读操作,禁止重排 |
| synchronized | 解锁happens-before后续对同一锁的加锁 |
| final | 正确构造的对象,其final字段的写入对其他线程可见且有序 |
使用volatile禁止重排
将上面的flag声明为volatile即可避免重排问题:
class FixedDemo {
int a = 0;
volatile boolean flag = false;
void writer() {
a = 1;
flag = true; // 写volatile,之前的写不会重排到其后
}
void reader() {
if (flag) {
int i = a; // 一定能读到1
}
}
}
总结
有序性关乎多线程程序的正确性。指令重排是性能优化的自然结果,但会破坏跨线程的观察顺序。借助Java内存模型的happens-before规则和volatile等同步工具,我们可以主动限制重排,写出可预测的并发代码。