程序真的是有序执行的吗?
并不一定
首先来看如下小程序:
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;
}
}
}
}
该小程序会出现如下几种排列组合:
上图可知,只有当x=b,y=a或y=a,x=b先执行时,才会出现x=0,y=0的情况,这也证实了,虽然程序中写的是a=1,b=1先执行,下图中程序结束了循环,侧面验证了两个语句之间有一定概率交换顺序执行
两个语句之间有一定概率交换顺序的执行
为何会存在乱序?
简单来说,是为了提高效率
CPU是为了提高效率而做出的一个优化机制,所以存在乱序操作
指令1会去内存读数据,等待返回,指令2在本地只做一个+1操作,因为寄存器的速度比内存快接近100倍,在指令1去内存读取数据时,因为CPU给每个线程分配的时间片是有限的,所以CPU会尽可能的在有限的时间下做更多的操作,在微观上,那么就会优先执行指令2的操作。
乱序存在的条件?
- as - if- serial 好像-是-序列化 执行的 看起来像序列化执行,但实际未必是序列化执行的
- 不影响单线程的最终一致性
思考如下代码小程序,会出现什么问题?
public class T02_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();
}
}
结论:
- 可能会出现线程可见性问题,因为ready没有被volatile所修饰
- number = 42与ready = true 有可能会存在指令重排序问题导致语句不是顺序执行
第二种情况可能会导致,number的值为0,原因很简单:主线程中可能存在指令重排序的问题导致ready = true先执行,紧接着执行了run方法,然后接着输出了number的值,此时值为0了,接着最后才会执行主线程的 number 赋值42的操作
对象创建过程,this对象溢出
首先查看如下小程序:
class T {
int m = 8;
}
T t = new T();
汇编码如下:
new:内存中开辟一块空间,并为m设定初始值为0
invokespecial:使成员变量m的值由0改为8,调用构造器
astore_1:建立关联关系,让引用 t 指向内存中的 m
其余两个指令与目前无关,暂不做要求
思考:如下小程序是否有问题?
public class T03_ThisEscape {
private int num = 8;
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();
}
}
当然是有问题的,首先我们先执行new指令,在内存开辟空间,然后正常来说是先执行invokespecial指令,后执行astore_1指令,但是由于指令重排序的原因,有可能astore_1指令先执行,那么就会先让this与成员变量m建立关联关系,此时m值为0,然后紧接着new Thread后输出了m的值,invokespecial指令还没执行时,m值已经输出为0,并没有得到我们预想中的8。
正常情况应该是已经构建好我们的this后在使用,但是目前这种情况this还没完全构建好,也就是处于一个中间状态,这也叫this的溢出。
拓展思考,上述小程序还有可能发生其它的问题吗?
**不要在构造器中new好一个线程后,并且启动它。**因为指令重排序是有概率发生的,如果上述小程序,指令重排序没有发生,那么会先执行invokespecial指令,由于构造器中线程 使用了 this.m操作,但是此时astore_1指令还没执行,this引用还没有与对象建立关联,就有可能出现空指针的情况。
问题思考:DCL单例是否一定需要加volatile?
先说结论,必须要,原因如下:
- 由于CPU底层指令是会有重排序的概率的,首先thread1线程会先去new Mgr04()对象,然后正常来说是先执行invokespecial指令调用构造方法,给它赋具体值,最后调用astore_1指令,使得对象间建立关联关系。
- 由于指令重排序的原因,有可能astore_1指令先执行了,此时对象之间建立关联关系,紧接着thread2线程接着去判断INSTANCE是否为空,这时因为对象间已经建立起关联关系了,此时的INSTANCE不为空了,就会直接返回半初始化状态的INSTANCE的值,如果是成员变量的m的话,m的值如果是订单数量100多万数据,因为半初始化状态的原因,导致只拿到了m的初试值0就返回了,那么问题就比较严重了。
哪些指令可以互换顺序?
hanppens-before原则(JVM规定重排序必须遵守的规则)
- 程序次序规则:同一个线程内,按照代码出现的顺序,前面的代码先行于后面的代码,准确的说是控制流顺序,因为要考虑到分支和循环结构。
- 管程锁定规则:一个unlock操作先行发生于后面(时间上)对同一个锁的lock操作。
- volatile变量规则:对一个volatile变量的写操作先行发生于后面(时间上)对这个变量的读操作。
- 线程启动规则:Thread的start( )方法先行发生于这个线程的每一个操作。
- 线程终止规则:线程的所有操作都先行于此线程的终止检测。可以通过Thread.join( )方法结束、Thread.isAlive( )的返回值等手段检测线程的终止。
- 线程中断规则:对线程interrupt( )方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupt( )方法检测线程是否中断 。
- 对象终结规则:一个对象的初始化完成先行于发生它的finalize()方法的开始。
- 传递性:如果操作A先行于操作B,操作B先行于操作C,那么操作A先行于操作C
如何阻止指令重排序?
intel : lfence sfence mfence(CPU特有指令)
-
CPU级别底层使用内存屏障,内存屏障也是一条特殊的指令,将它加在指令1与指令2之间时,指令1 与 指令2则不能互换顺序
JVM是一个规范,hotspot是其中实现之一。JVM虚拟机都应该实现自己JVM级别的内存屏障 JVM虚拟机级别的内存屏障与CPU级别内存屏障没有关系
JVM内存屏障,如下图:
JVM级别阻止乱序执行必须实现四种功能,也就是LoadLoad、LoadStore、StoreStore、StoreLoad四种读写屏障
volatile在JVM级别规定的比较保守,前后加了四个屏障。Hotspot的实现就是使用了lock;addl的前缀指令去实现的
LOCK 用于在多处理器中执行指令时对共享内存的独占使用。
它的作用是能够将当前处理器对应缓存的内容刷新到内存,并使其他处理器对应的缓存失效。
另外还提供了有序的指令无法越过这个内存屏障的作用。