秋招 - java

27 阅读2分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第28天,点击查看活动详情 >>
懒加载中的问题

当在同一时间多个线程同时执行该单例时就会出现JVM指令重排的问题,从而可能导致某一个线程获取的single对象未初始化对象。

延迟加载代码:

public class Single {

    private static Single single;
    
    private Single() {
    }
    
    public static Single getInstance() {
        if(null == single) {
            synchronized (Single.class) {
                if(null == single) {
                    single = new Single();
                }
            }
        }
        return single;
    }
}

原因: single = new Single()这段代码并不具备原子性,从代码层面上来说确实没问题,但是如果了解JVM指令的就知道其实在执行这句代码的时候在JVM中是需要执行三个指令来完成的,如下:

//1:分配对象的内存空间
memory = allocate(); 
//2:初始化对象
ctorInstance(memory); 
//3:设置instance指向刚分配的内存地址
instance = memory;

现假设有A、B两个线程去调用该单例方法,当A线程执行到single = new Single()时,如果编译器和处理器对指令重新排序,指令重排后:

//1:分配对象的内存空间
memory = allocate(); 
//3:设置instance指向刚分配的内存地址,此时对象还没被初始化
instance = memory; 
//2:初始化对象
ctorInstance(memory);

当A线程执行到第二步(3:设置instance指向刚分配的内存地址,此时对象还没被初始化)变量single指向内存地址之后就不为null了,此时B线程进入第一个if,由于single已经不为null了,那么就不会执行到同步代码块,而是直接返回未初始化对象的变量single,从而导致后续代码报错。

解决方法:

使用volatile关键字修饰变量:

可见性:

  • 写volatile修饰的变量时,JMM会把本地内存中值刷新到主内存
  • 读volatile修饰的变量时,JMM会设置本地内存无效

有序性:

要避免指令重排序,synchronized、lock作用的代码块自然是有序执行的,volatile关键字有效的禁止了指令重排序,实现了程序执行的有序性;

更正后的单例模式:

public class Single {

    private volatile static Single single;
    
    private Single() {
    }
    
    public static Single getInstance() {
        if(null == single) {
            synchronized (Single.class) {
                if(null == single) {
                    single = new Single();
                }
            }
        }
        return single;
    }
}