什么是指令重排序

在计算机执行指令顺序在经过程序编译器编译之后形成的指令序列,一般而言,这个指令序列是会输出确定的结果;以确保每一次的执行都有确定的结果。但是,一般情况下,CPU和编译器为了提升程序执行效率,会按照一定的规则允许进行指令优化,在某些情况下,这种优化会带来一些执行的逻辑问题,主要的原因是代码逻辑之间是存在一定的先后顺序,在并发执行情况下,会发生二义性,即按照不同的执行逻辑,会得到不同的结果信息。
指令重排序 - 图1

什么是as-if-serial语义

不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器,runtime 和处理器都必须遵守as-if-serial语义。
分析: 关键词是单线程情况下,必须遵守;其余的不遵守
那么在多线程下那些情况会发送呢?

名称 代码实例 说明
写后读 int a=1;int b=a; 写入一个变量后,在赋值读取
写后写 int a=1;a=2; 写入一个变量后,更改值
写后读后写 int b=0;int a=b;b=1; 先初始化,然后赋值读取,在更改值

上面三种情况,只要重排序两个操作的执行顺序,程序的执行结果将会被改变。称之为数据依赖性。

指令重排序的类型

在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。

编译器优化的重排序

编译器在不改变单线程程序语义的前提下(代码中不包含synchronized关键字),可以重新安排语句的执行顺序。从而尽可能的减少寄存器读取和存储,并充分复用寄存器。但是编译器对数据的依赖关系判断只能在单执行流内,无法判断其他执行流对竞争数据的依赖管理

指令并行的重排序

现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令执行顺序

内存系统的重排序

由于处理器使用缓存读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行

实例

单线程下:

  1. // 加加减减 5 10 15 - 原子性
  2. public class TestAddSub {
  3. static int balance = 10;
  4. public static void withdraw() {
  5. balance -= 5;
  6. }
  7. public static void deposit() {
  8. balance += 5;
  9. }
  10. public static void main(String[] args) {
  11. List<Thread> threads = Arrays.asList(
  12. new Thread(TestAddSub::deposit),
  13. new Thread(TestAddSub::withdraw)
  14. );
  15. threads.forEach(Thread::start);
  16. for (Thread thread : threads) {
  17. try {
  18. thread.join();
  19. } catch (InterruptedException e) {
  20. e.printStackTrace();
  21. }
  22. }
  23. System.out.println(balance);
  24. }
  25. }

那么无论如何结果都是10
现在我们把代码改为多线程:

  1. // 加加减减 5 10 15 - 原子性
  2. public class TestAddSub {
  3. static int balance = 10;
  4. public static void withdraw() {
  5. balance -= 5;
  6. }
  7. public static void deposit() {
  8. balance += 5;
  9. }
  10. public static void main(String[] args) {
  11. //List<Thread> threads = Arrays.asList(
  12. // new Thread(TestAddSub::deposit),
  13. // new Thread(TestAddSub::withdraw)
  14. //);
  15. List<Thread> threads = Arrays.asList(
  16. new Thread(()->{
  17. for (int i = 0; i < 5000; i++) {
  18. deposit();
  19. }
  20. }),
  21. new Thread(()->{
  22. for (int i = 0; i < 5000; i++) {
  23. withdraw();
  24. }
  25. })
  26. );
  27. threads.forEach(Thread::start);
  28. for (Thread thread : threads) {
  29. try {
  30. thread.join();
  31. } catch (InterruptedException e) {
  32. e.printStackTrace();
  33. }
  34. }
  35. System.out.println(balance);
  36. }
  37. }

可以明显的看到这时的结果,怎么都不会为10,如果我们把for循环减少一些次数,如果你的CPU性能好,那么少量的多线程运行结果还是10。

解决方案

使用synchronized加锁

  1. public class Test17 {
  2. public static void main(String[] args) throws InterruptedException {
  3. Room room = new Room();
  4. Thread t1 = new Thread(() -> {
  5. for (int i = 0; i < 5000; i++) {
  6. room.increment();
  7. }
  8. }, "t1");
  9. Thread t2 = new Thread(() -> {
  10. for (int i = 0; i < 5000; i++) {
  11. room.decrement();
  12. }
  13. }, "t2");
  14. t1.start();
  15. t2.start();
  16. t1.join();
  17. t2.join();
  18. System.out.println(room.getCounter());
  19. }
  20. }
  21. class Room {
  22. private int counter = 0;
  23. public synchronized void increment() {
  24. counter++;
  25. }
  26. public synchronized void decrement() {
  27. counter--;
  28. }
  29. public synchronized int getCounter() {
  30. return counter;
  31. }
  32. }