并发编程---重排序


什么是重排序?

重排序是指在不改变程序最终执行结果的前提下,编译器、处理器或内存系统对指令的执行顺序进行重新排列,以提高程序的执行效率。

重排序的三种来源

  1. 编译器重排序
    • 编译器在生成字节码或机器码时,可能会对指令进行重新排序。
    • 例如:将不相关的指令提前或延后执行,以减少指令流水线的停顿。
  2. 处理器重排序
    • 现代处理器为了提高执行效率,可能会对指令进行乱序执行(Out-of-Order Execution)。
    • 例如:处理器可能会将没有数据依赖的指令并行执行。
  3. 内存系统重排序
    • 由于 CPU 缓存的存在,不同线程对内存的读写操作可能会被重新排序。
    • 例如:写操作可能会被延迟,而读操作可能会被提前。

重排序的影响

在单线程环境下,重排序不会影响程序的最终结果,因为编译器和处理器会保证程序的 串行语义(As-If-Serial)。但在多线程环境下,重排序可能会导致 可见性问题有序性问题,从而引发并发 Bug。

可见性问题

  • 由于重排序,一个线程对共享变量的修改可能对其他线程不可见。

例如:

int a = 0;
boolean flag = false;

// 线程 1
a = 1;          // 操作 1
flag = true;    // 操作 2

// 线程 2
if (flag) {     // 操作 3
    System.out.println(a);  // 操作 4
}
  • 由于重排序,操作 1 和操作 2 可能会被重新排序,导致线程 2 看到 flagtrue,但 a 仍然是 0

注意:两个线程是并发执行的,但并不是完全同时执行的。对于并发执行的线程,操作的顺序是不可预知的,而这正式引发重排序问题的原因

有序性问题

  • 由于重排序,程序的执行顺序可能与代码的编写顺序不一致。

例如:

int x = 0;
int y = 0;

// 线程 1
x = 1;  // 操作 1
y = 2;  // 操作 2

// 线程 2
if (y == 2) {  // 操作 3
    System.out.println(x);  // 操作 4
}
  • 由于重排序,操作 1 和操作 2 可能会被重新排序,导致线程 2 看到 y2,但 x 仍然是 0

如何避免重排序带来的问题?

为了避免重排序带来的并发问题,Java 提供了 内存屏障(Memory Barrier)同步机制 来保证程序的可见性和有序性。

volatile关键字

  • volatile 保证每次读取和写入共享变量时,都会直接访问主内存,而不是线程的本地缓存,因此它确保了变量在多个线程之间的 可见性

例如:

volatile boolean flag = false;
  • flag 被标记为 volatile 后,线程 A 对 flag 的修改会立即对线程 B 可见,不会存在缓存问题。

synchronized 关键字

  • synchronized 关键字通过加锁和解锁操作来保证代码块的原子性、可见性和有序性。

例如:

synchronized (lock) {
    a = 1;
    flag = true;
}

final关键字

  • final 关键字可以禁止构造函数中的指令重排序,确保对象的正确初始化。

例如:

final int x;
x = 10;  // 构造函数中的赋值操作不会被重排序

happens-before规则

  • Java 内存模型(JMM)定义了一系列 happens-before 规则,用于描述操作之间的可见性和有序性。
  • 例如:
    • 单线程规则:在同一个线程中,前面的操作 happens-before 后面的操作。
    • volatile 规则:对 volatile 变量的写操作 happens-before 后续对它的读操作。
    • 锁规则:解锁操作 happens-before 后续的加锁操作。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值