- 程序真的是顺序执行的吗?
- 优化器优化,会重排序
- 线程之间执行的先后顺序及中间过程是不可预知的
乱序程序的分析
package com.mashibing.juc.c_001_03_Ordering;
/**
* 本程序跟可见性无关,曾经有同学用单核也发现了这一点
*/
import java.util.concurrent.CountDownLatch;
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;
}
}
}
}
图解
- 只有再最后一种情况时,即每一个线程中的两句语句都交换了顺序时才会出现都是0的情况
- 验证了两个语句之间有一定概率会交换顺序执行(出现可能性不高)
- 因为一条java语句可能对应好多条汇编语句,要所有的汇编语句都换过来才可能出现
- 两个线程都要换===>概率不高
- 能够验证乱序现象的存在
- 单线程中写了两句话但未必是先执行第一句再执行第二句
乱序的原因
- 简单说,就是为了提高效率
- 寄存器的速度比内存快100倍
- 下面的例子好比吃饭时烧水后再切菜,而一般在烧水的同时会切菜===>流水线技术!!!并行执行,单线程只要没有数据相关性就可以乱序(在编译阶段可以判断?)
- 从微观上讲,第二条指令执行更快,所以可能在第一条指令执行结束之前执行完
- cpu为提高效率进行的优化机制===>所以才有乱序
- 不是所有语句都可以乱序,要前后两条语句没有依赖关系,要不能影响单线程的最终一致性
- 前后两条语句没有依赖关系时,可能会换
乱序存在的条件
- as-if-serial好像是序列化执行的
- 不影响单线程的最终一致性
- 不存在一致性(谁先执行不影响线程的最终一致性)
- 看起来序列化,实际上未必序列化
- 单线程中虽然乱序没有影响,但是多线程中的乱序影响比较严重
存在问题的程序
- 可见性问题:
- ready设为true后并不会马上停止,有可能也会马上停止(MESI的主动性或者yield同步刷新)
- 对ready加volatile修饰
- 有序性问题:
- number和ready赋值语句可能会换顺序
package com.mashibing.juc.c_001_03_Ordering;
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();
}
}
new一个对象所要经过的过程
- 由5条java指令构成
- 申请内存,赋默认值(半初始化状态)
- 调初始化方法,赋初始值
- 与局部变量的引用创建关联(引用变量指向对象)
- 第三步是复制,因为第四步调用的时候需要消耗掉一个指针,先复制一份
- 第四步是构造方法
this对象逸出
- 以下程序会输出什么? ```java package com.mashibing.juc.c_001_03_Ordering;
public class T03_ThisEscape {
private int num = 8;
public T03_ThisEscape() {
// this存在于局部变量表中(jvm),实际上就是一引用对象
new Thread(() -> System.out.println(this.num)
).start();
new Thread(() -> System.out.println(num)
).start();
}
public static void main(String[] args) throws Exception {
new T03_ThisEscape();
// 阻塞,让主线程不结束,确保子线程执行完
System.in.read();
}
} ```
- 这边有可能会出现问题,虽然做实验可能很难做出来
- 理论上是有问题的===>可能会输出中间状态0
- 第三步建立关联是和this建立关联===>而在本程序中调用初始化方法和建立关联可能会互换顺序(有可能换顺序)
- 先建立与this的关联(此时num=0),再调用初始化方法,而在调用构造方法时new了一个线程(启动时可能还没有赋值为8,所以打印的时候有可能是中间状态0)
- 这就叫this的中间状态逸出了,逸出到构造方法了(没穿衣服就出来了,穿了一半就出来了,全穿完才出来)
- ❓要不要加volatile?
- 因为有this逸出的现象,所有不要在构造方法中new线程然后启动,可以new线程,但是不要启动;可以单独写一个方法,启动线程(不要在构造方法中new线程然后启动)
美团的七连问
- 解释对象的创建过程(对象半初始化问题)
- 汇编语言就是助记符
- 加问DCL与volatile问题(指令重排序)
- DCL单例要不要加volatile ===>双重校验锁(Double Check Lock)
- 底层禁止排序内存屏障(jvm级别:memoryBarriar、fence)、java禁止重排序volatile
- 内存屏障使用汇编里的lock做到的
- DCL是什么(开源项目中用的很多)
- 底层只要指令之间互相不影响,没有依赖关系,并且保证了最终一致性,就可以互换(java中不一样)
- jvm是操作系统上的程序
- jvm中规定有8种情况不允许重排序(happened-before),除了这8种外其他都可以(new对象没有在这八种中)===>什么时候用力,什么时候放松
- 内存屏障有很多种===>各种不同的cpu有不同的屏障
- jvm屏蔽了这些区别
- JSR内存屏障(jvm要求===>还有相应的底层实现)
- LoadLoad屏障: 对于这样的语句Load1; LoadLoad; Load2, 在L oad2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
- StoreStore屏障: 对于这样的语句Store1; StoreStore; Store2, 在Stcre2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
- LoadStore屏障: 对于这样的语句oad1; LoadStore; Store2, 在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
- StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2, 在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。
- 和垃圾回收器里的读屏障和写屏障没有什么关系(修改内存时需要做一些操作—->类似拦截器?)
缓存行(缓存一致性)
- volatile
- 不做任何措施:不固定
- 强制刷新操作(比如加锁sout)
JUL工具可以看对象在内存中的存储布局
类型指针原来是8字节,然后经过压缩变成了4bye(UseCompressedOops)===>Ordinary Object Pointer普通对象指针(32GB内存及以上压缩就不起作用了===>硬件厂商的偷工减料:数据总线的宽度、控制总线、地址总线什么48个???)
总结
- 为了提高执行效率,CPU指令可能会乱序执行
乱序执行不得影响单线程的最终一致性
as- if -serial:单线程程序看上去象序列化执行
- 乱序在多线程的情况下可能会产生难于察觉的错误
两个问题
什么时候不能乱序
- happened-before原则(8条),除这8条之外都有可能换顺序
- 程序次序规则:同一个线程内,按照代码出现的顺序,前面的代码先行于后面的代码,准确的说是控制流顺序,因为要考虑到分支和循 环结构。
- 管程锁定规则: -个unlock操作先行发生于后面(时间上)对同-一个锁的lock操作。
- volatile变量规则:对一个volatile变量的写操作先行发生于后面(时间上)对这个变量的读操作。
- 线程启动规则: Thread的start( )方法先行发生于这个线程的每一个操作。
- 线程终止规则:线程的所有操作都先行于此线程的终止检测。可以通过Thread.join( )方法结束、Thread.isAlive( )的返回值等手段检测线 程的终止。
- 线程中断规则:对线程interrupt( )方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Threadinterrupt( )方法检 测线程是否中断
- 对象终结规则: -个对象的初始化完成先行于发生它的finalize0方法的开始。
- 传递性:如果操作A先行于操作B,操作B先行于操作C,那么操作A先行于操作C
不要记,底层一条lock语句就全部解决了;这只是jvm层面的规则
如何解决乱序
底层内存屏障
jvm对于底层的规则,可以选也可以不选!
lock指令必须后面跟一条指令,表明当执行后面这条指令的时候,对总线或者缓存进行锁定;并且这条指令不能是空指令nop,必须有一条;所以就给某个寄存器加了个0(addl),相当于空操作
jvm层级
jvm是一个规范===>hotspot是一个实现
不要与底层内存屏障混起来使用volatile禁止指令重排序
volatile修饰的是位置===>是一个内存位置,与顺序无关(而不是内存屏障)
不要钻牛角尖===>LoadLoadBarrier放在volatile读之后的原因,感觉没必要?(马老师也不知道)
- DCL中加volatile之后将引用与对象关联起来就必然在给内部字段赋值(即初始化)之后了!,不会造成虚幻赋值了
🤏随想
- 使用jclasslib时要先编译然后把光标定位到main方法里面