程序真的是按“顺序”执行的吗?
乱序的验证:
public class T01_Disorder {
private static int x = 0, y = 0;
private static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
for (long i = 0; i < Long.MAX_VALUE; i++) {
x = 0;
y = 0;
a = 0;
b = 0;
CountDownLatch latch = new CountDownLatch(2);
Thread one = new Thread(new Runnable() {
public void run() {
a = 1;
x = b;
latch.countDown();
}
});
Thread other = new Thread(new Runnable() {
public void run() {
b = 1;
y = a;
latch.countDown();
}
});
one.start();
other.start();
latch.await();
String result = "第" + i + "次 (" + x + "," + y + ")";
if (x == 0 && y == 0) {
System.err.println(result);
break;
}
}
}
}
如上代码中的线程one将a的值设置为1,x的值设置为b,线程other将b的值设置为1,将y的值设置为a,然后再用CountDownLatch这个类来控制两个线程的执行,然后当x=0并且y=0的时候结束程序的运行。
输出结果如下:
如下图总结了如上代码运行的所有结果:
如上图中发现如果不打乱两线程run方法里面的语句的执行顺序的话,是不会出现x=0并且y=0的情况,只有当x=a,y=b或者x=b,y=a先执行才会出现x=0并且y=0的情况那么则说明两线程的run方法里面的语句执行顺序被打乱了。
为什么会存在乱序的现象(为了提高效率)
程序的运行就是读取程序编译完成后的各个指令,但是指令的执行可能会交换顺序,如上图指令一首先去从内存中读取一个数据等待内存的返回,然而第二条指令不需要读取数据只需要执行本地的寄存器的增加操作,由于CPU相比于内存的速度是快了很多倍的,如果一定要按照上述的指令顺序去执行的话,那么是非常浪费资源的,那么cpu就会在等待返回的过程中执行指令2,那么第二条指令就跑到了第一条指令的上面先执行完毕。从底层来讲的话是cpu为了提高效率而采用的优化机制,所以才有乱序这件事存在。
乱序发生的原则(语句执行交换顺序的条件)
1、前后两条语句没有依赖关系,不影响单线程的最终一致性(as-if-serial)
如x=1之后执行x++这两条语句互有依赖关系
但是如上代码x=b,a=1这样是没有互相的依赖关系的。
通过小程序认识可见性和有序性问题
public class NoVisibility {
private static boolean ready = false;
private static int number;
private static class ReaderThread extends Thread {
@Override
public void run() {
while (!ready) {
Thread.yield();
}
System.out.println(number);
}
}
public static void main(String[] args) throws Exception {
Thread t = new ReaderThread();
t.start();
number = 42;
ready = true;
t.join();
}
}
如上代码有两个问题:
1、不满足ready线程之间的可见性
2、有序性问题(打印可能是0,由于number=42和ready=true没有前后的依赖关系,所以可能会交换顺序,导致先读取到ready,然后直接输出number=0)
再看如下例子:
1、首先了解对象的创建过程:
第一条指令是 new申请一个内存空间(new出来的t对象所占用的内存,此处m的值为0)
第三条指令invokespecial(特殊调用),此处特殊调用了T的init方法(T的默认的构造方法,此处m的值为8)
第四条指令是里面的内存空间和外面的t变量建立关联
第三条和第四条指令可能互相调换执行顺序,就有可能在运行过程中先输出初始化的0
此处和线程的有序性的关联:
如下代码:
public class T03_ThisEscape {
private int num = 8;
Thread t;
public T03_ThisEscape() {
new Thread(() -> System.out.println(this.num)
).start();
}
public static void main(String[] args) throws Exception {
new T03_ThisEscape();
System.in.read();//让主程序不结束
}
}
如上代码可能输出num=0,所以一般来说尽量不要在构造方法里面去new线程并将它启动,因单独写一个方法让它启动。
如下所示:
public class T03_ThisEscape {
private int num = 8;
Thread t;
public T03_ThisEscape() {
new Thread(() -> System.out.println(this.num)
);
}
public void start(){
t.start();
}
public static void main(String[] args) throws Exception {
new T03_ThisEscape();
System.in.read();//让主程序不结束
}
}
Volatile如何禁止指令重排序?
Hapens Before原则(JVM规定重排序必须遵守的规则)
CPU用内存屏障阻止乱序:
内存屏障是特殊指令:看到这种指令,前面的必须执行完,后面的才能执行。
Intel:ifence(读) sfence(写) mfence(读写)
JVM内存屏障:其中load叫读,store叫写
JVM层级用四个内存屏障:
当volatile修饰的内存需要写入的时候,前面要加一个屏障StoreStoreBarrier,意思是在我写之前前所有的指令需要全部写完,后面有一个StoreLoadBarrier,意思是等我写完才能读,对volatile读操作,意思是等我读完别人才能读,以及后面还有一个等我读完别人才能写。
volatile底层实现:
1:volatile i
2:通过编译成class文件变成 ACC_VOLATILE
3:JVM的内存屏障
屏障两边的指令不可以重排,并保障有序
happens-before
as-if-serial
4:hotspot实现
进入到bytecideinterpreter.cpp
会看到orederAccess的fence方法:
首先判断操作系统是否是多核CPU
由于Lock指令必须后面跟一条指令,意思是当我执行后面的指令的时候对缓存或者总线进行锁定,并且后面的那条指令不能是空指令,如果是AMD64位的对rsp寄存器加0,否则对esp寄存器加0,然后看CPU级别的lock指令。
LOCK用于在多处理器中执行指令时对共享内存的独占使用,它的作用是能够将当前处理器对应缓存的内容刷新到内存,并使其他处理器对应的缓存失效,另外还提供了有序的指令无法越过这个内存屏障的作用。