1、指令重排序
何为指令重排序,我们以一个例子来看一下
public class Test1 {
int a = 0;
int b = 0;
void set() {
a = 1;
b = 1;
}
void get() {
while(b == 1) {
assert (a == 1); // 1 可能抛出异常
}
}
}
上述代码位置1,在多线程环境下可能抛出异常,很多同学肯定会有疑问,这种情况怎么会发生?
原因如下:首先代码执行过程如下图
- 编译器可能会做出指令重排序优化:
编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
- 指令级并行的重排序
现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
- 内存系统的重排序
也就是说上述代码set() 内部执行的流程可能是执行b=1在执行a=1,导致多线程下出现错误。
2、可见性问题
public class Test1 {
static boolean stop = false;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
while(true) {
if(stop) {
return;
}
}
});
t1.start();
Thread.sleep(1000);
stop = true;
}
}
上述代码可能永远不会停止,原因如下:
主内存stop变量值为false,t1线程会缓存了stop的值并且为false,主线程修改了stop的值为true并且同步到了主内存,此时主内存stop为true,但是对于t1线程而言是不可见的,t1线程读取到的是当前线程的缓存值false,这就是可见性问题
用一句话来理解就是:a线程更新的共享变量对于b线程不可见
3、volatile关键字
public class Test1 {
volatile int a = 0;
int b = 0;
void set() {
a = 1;
//storeMemoryBarrier()写屏障,写入到内存
b = 1;
}
void get() {
while(b == 1) {
assert (a == 1); // 1 可能抛出异常
}
}
}
volatile关键字可以解决可见性问题和指令重排序问题
变量a加上了volatile
- set方法内指令重排序会被阻止
- a=1之后会加上一层写的屏障,导致a=1会被直接写入到主内存,使用其他线程都是可见的
4、DCL(Double Check Lock)半对象问题
public class DCL {
private static DCL instance = null;
private DCL() { }
public static DCL getInstance() {
if(instance == null) {
synchronized (DCL.class){
if(instance == null) {
instance = new DCL(); // 1
}
}
}
return instance;
}
}
以上代码是一个单例模式懒汉式的实现,当instance没有volatile修饰时可能出现半对象问题,即得到的instance是一个没有正确初始化的对象,原因如下:
instance = new DCL();指令上可以拆解为如下三步
1.memory = allocate(); //分配对象的内存空间
2.ctorInstance(memory); //初始化对象
3.instance =memory; //设置instance指向刚分配的内存地址
但是经过指令重排序可能出现执行顺序为1->3->2,如果线程A执行1->3 此时线程B检查后发现instance不为null,此时得到的instance还没有初始化对象,即为半对象。
所以instance加上volatile修饰后即可阻止指令重排序。