Java内存模型与单例模式

我们直接进入正题

这是双重校验锁的单例模式

    private static Singleton instance;  //1

    public static Singleton getSingleton(){
        if (instance == null){   //2
            synchronized (Singleton.class){   //3
                if (instance == null)   //4
                    instance = new Singleton();  //5   //实例化
            }
        }
        return instance;
    }

 这段代码看起来两全其美

①多个线程试图在同一时间创建对象时,会通过加锁来保证只有一个线程能创建对象

②在对象创建完之后,执行getSingleton方法不需要获取锁,直接返回已创建好的对象

双重校验锁看起来似乎很完美,但这是一个错误的优化,在代码执行到第二行时,代码读取instance不为null,instance引用的对象有可能还没有完成初始化。

一、问题的根源

在创建一个对象时,可分这三个步骤

①分配对象的内存空间

②初始化对象

③将引用变量指向分配的内存地址(在这一步,引用变量就已经不是null了)

但是呢,上面步骤的2,3有可能被重排序,指向顺序如下

①分配对象的内存空间

②将引用变量指向分配的内存地址(在这一步,引用变量就已经不是null了)//此时,对象还没有初始化

②初始化对象

所以当第一个线程执行到第二步时,第二个线程第一次校验发现instance不是空,在使用时就出报错。

抠张图看看

《Java内存模型与单例模式》

 所以我们想办法

①不允许2、3步重排序

②运行2、3步重排序,但不允许其他线程看到这个重排序

解决方法一:基于vloatile的解决方案

我们只需要一点小小的修改,就可以实现线程安全的延迟初始化,那就将instance用volatile修饰,也就是下面的代码

    private static volatile Singleton instance;

    public static Singleton getSingleton(){
        if (instance == null){
            synchronized (Singleton.class){
                if (instance == null)
                    instance = new Singleton();
            }
        }
        return instance;
    }

 当对象声明为volatile时,步骤2和步骤3之前的重排序,在多线程环境中将会被禁止。

解决方法二:基于类初始化的解决方案

JVM在类的初始化阶段,会执行类的初始化,在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。

基于这个特性,可以实现另一种线程安全的延迟初始化方案

private static class SingletonFactory{
        public static Singleton singleton = new Singleton();
    }
    
    private static Singleton getSingleton(){
        return SingletonFactory.singleton;
    }

抠张图看看这是为什么吧

《Java内存模型与单例模式》

 Java语言规范规定,对于每一个类或者接口,都有一个唯一的初始化锁预支对应。JVM在类初始化期间会获取这个锁,并且每隔线程至少获取一次锁来确保这个类已经被初始化过了。

    原文作者:java内存模型
    原文地址: https://blog.csdn.net/yanghan1222/article/details/81259988
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞