什么是指令重排序
在计算机执行指令的顺序在经过程序编译器编译之后形成的指令序列,一般而言,这个指令序列是会输出确定的结果;以确保每一次的执行都有确定的结果。但是,一般情况下,CPU和编译器为了提升程序执行的效率,会按照一定的规则允许进行指令优化,在某些情况下,这种优化会带来一些执行的逻辑问题,主要的原因是代码逻辑之间是存在一定的先后顺序,在并发执行情况下,会发生二义性,即按照不同的执行逻辑,会得到不同的结果信息。
什么是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)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
内存系统的重排序
由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
实例
单线程下:
// 加加减减 5 10 15 - 原子性
public class TestAddSub {
static int balance = 10;
public static void withdraw() {
balance -= 5;
}
public static void deposit() {
balance += 5;
}
public static void main(String[] args) {
List<Thread> threads = Arrays.asList(
new Thread(TestAddSub::deposit),
new Thread(TestAddSub::withdraw)
);
threads.forEach(Thread::start);
for (Thread thread : threads) {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(balance);
}
}
那么无论如何结果都是10
现在我们把代码改为多线程:
// 加加减减 5 10 15 - 原子性
public class TestAddSub {
static int balance = 10;
public static void withdraw() {
balance -= 5;
}
public static void deposit() {
balance += 5;
}
public static void main(String[] args) {
//List<Thread> threads = Arrays.asList(
// new Thread(TestAddSub::deposit),
// new Thread(TestAddSub::withdraw)
//);
List<Thread> threads = Arrays.asList(
new Thread(()->{
for (int i = 0; i < 5000; i++) {
deposit();
}
}),
new Thread(()->{
for (int i = 0; i < 5000; i++) {
withdraw();
}
})
);
threads.forEach(Thread::start);
for (Thread thread : threads) {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(balance);
}
}
可以明显的看到这时的结果,怎么都不会为10,如果我们把for循环减少一些次数,如果你的CPU性能好,那么少量的多线程运行结果还是10。
解决方案
使用synchronized加锁
public class Test17 {
public static void main(String[] args) throws InterruptedException {
Room room = new Room();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
room.increment();
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
room.decrement();
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(room.getCounter());
}
}
class Room {
private int counter = 0;
public synchronized void increment() {
counter++;
}
public synchronized void decrement() {
counter--;
}
public synchronized int getCounter() {
return counter;
}
}