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,在多线程环境下可能抛出异常,很多同学肯定会有疑问,这种情况怎么会发生?

原因如下:首先代码执行过程如下图

指令重排 java 指令重排序java_面试

  • 编译器可能会做出指令重排序优化:

编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。

  • 指令级并行的重排序

现代处理器采用了指令级并行技术(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修饰后即可阻止指令重排序。