深入理解单例模式

深入理解单例模式

模式是脱离语言的。

一、单例的模式由来

多线程要操作同一个对象,保证对象的唯一性。

如何解决?

实例化过程只实例化一次。

单例模式的四大原则

1.构造方法私有化
2.以静态方法或者枚举返回实例
3.确保实例只有一个,尤其是多线程环境
4.确保反序列化时,不会重新构建对象

我们常见的单例模式有:

  • 饿汉模式
  • 懒汉模式
  • 双重检索模式
  • 静态内部类模式
  • 枚举模式

二、单例模式的分类

1.饿汉模式

public class HungrySingleton {

    /**
     * 1.加载的时候就产生实例对象
     */
    private static HungrySingleton instance = new HungrySingleton();

    /**
     * 2.构造方法私有化
     */
    private HungrySingleton(){}

    /**
     * 3.提供返回实例对象的静态方法
     */

    public static HungrySingleton getInstance(){
        return instance;
    }

    public static void main(String[] args) {

        for (int i = 0; i < 50; i++) {

            new Thread(() ->{

                System.out.println(HungrySingleton.getInstance());
            }).start();
        }
    }
}

饿汉模式安全性:

  • 在HungrySingleton类被类加载器加载的时候已经被实例化,所有只有这一次,以空间换时间,线程是安全的。
  • 效率问题
    • 没有延迟加载,如果创建后不被使用,占内存,影响性能

2.懒汉式
public class LazySingleton {

    /**
     * 1.不进行初始化
     */
    private static LazySingleton instance = null;


    /**
     * 2.构造方法私有化
     */
    private LazySingleton(){};


    /**
     * 3.使用的时候进行初始化
     */
    public static LazySingleton getInstance(){
        if(Objects.isNull(instance)){
            instance = new LazySingleton();
        }
        return instance;
    }

    public static void main(String[] args) {
        /**
         * 模拟100个线程进行并发操作
         */
        for (int i = 0; i <10000; i++) {
            new Thread(() ->{
                System.out.println(LazySingleton.getInstance());
            }).start();
        }
    }
}

懒汉模式安全性:LazySingleton是在方法被调用后才创建对象,用时间换空间,在多线程环境下存在风险。

3.双重检索懒汉模式(Double -Check -Lock)
public class DCLSingleton {

    /**
     * 1.不进行初始化
     */
    private static DCLSingleton instance = null;

    /**
     * 2.构造方法私有化
     */
    private DCLSingleton(){}


    /**
     * 3.利用双重检索的方式进行初始化操作
     */
    public static DCLSingleton getInstance(){
        if(Objects.isNull(instance)){
            synchronized (DCLSingleton.class){
                if(Objects.isNull(instance)){
                    instance = new DCLSingleton();
                }
            }
        }
        return instance;
    }
}

上面的代码:在获取实例对象 getInstance()的方法中,我们首先判断instance是否为空,如果为空,则锁定DCLSingleton.class,并再次检查instance是否为空,如果还为空则创建DCLSingleton的一个实例。

我们假如有两个线程A,B同时调用getInstance()方法,他们会同时发现instance ==null,于是同时对DCLSingleton.class加锁,此时JVM保证只有一个线程能够加锁成功(假如是线程A),另外一个线程会处于等待状态(假如是线程B);线程A会创建一个DCLSingleton实例,之后释放锁,锁释放后,线程B被唤醒,线程B再次尝试加锁,此时是可以加锁成功的,加锁成功后,线程B检查Objects.isNull(instance)时会发现,已经创建过DCLSingleton实例了,所以线程B不会再创建一个DCLSingleton实例。

这看上去一切都很完美,无懈可击,但实际上这个getInstance()方法并不完美,问题出在哪里呢,出在new 操作上,我们以为的new操作应该是:

  1. 分配一块内存M
  2. 在内存M上初始化DCLSingleton对象;
  3. 然后M的地址赋值给instance变量

但可能经过JVM优化过后的执行顺序是这样的:
1.分配一个内存M;
2.将M的地址赋值instance变量
3.最后再内存上初始化DCLSingleton对象。

优化后会导致什么问题呢? 我们假设线程A先执行getInstance()方法,当执行完指令2时恰好发生了线程切换,切换了线程上B上;如果此时线程B也执行getInstance()方法,那么线程B 在执行第一个判断时会发现instance != null ,所以直接返回instance,而此时的instance是没有初始化过的,如果我们这个时候访问instance的成员变量就可能会触发空指针异常。


image.png

4.双重检索(DCL)+ volatile
public class DCLSingleton {

    /**
     * 1.不进行初始化
     */
    private volatile static DCLSingleton instance = null;

    /**
     * 2.构造方法私有化
     */
    private DCLSingleton(){}


    /**
     * 3.利用双重检索的方式进行初始化操作
     */
    public static DCLSingleton getInstance(){
        if(Objects.isNull(instance)){
            synchronized (DCLSingleton.class){
                if(Objects.isNull(instance)){
                    instance = new DCLSingleton();
                }
            }
        }
        return instance;
    }
}

volatile的作用:

1.保证可见性:

对共享变量的修改,其它线程马上能感知到 ,
这样volatile可以确保instance每次都会在主内存中读取,保证instance的一致性

2.保证有序性:

重排序:(编译阶段,指令优化阶段会进行重排序)
as-if-serial: 重排序后在单线程不影响程序的执行结果,对多线程有影响
volatile原则:
volatile之前的代码不能调整到它的后面
volatile之后的代码不能调整到它的前面
位置不变化

5.静态内部类模式

public class Singleton {
    /**
     * 1.构造方法私有化
     */
    private Singleton(){}

    /**
     * 2.定义静态内部类
     * 
     * 声明类的时候,成员变量中不声明实例变量,而放到内部静态类中
     */
    private static class SingletonHolder{
        private static Singleton instance = new Singleton();
    }

    /**
     * 3.返回实例对象
     */
    public static Singleton getInstance(){
        return SingletonHolder.instance;
    }
}

静态内部类的优点:

外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化instance,故而不占用内存,即当Singleton第一次被加载时,并不需要加载SingleHolder,只有当getInstance()方法第一次被调用时,才会去初始化instance,第一次调用getInstance()方法会导致JVM加载SingletonHolder类,这种方法不仅能保证线程安全,也能保证单例的唯一性,同时也延迟了到单例的实例化。

补充知识
当初始化一个类时,如果其父类还未进行初始化,会先触发其父类的初始化。

  1. 遇到new、getstatic、setstatic或者invokestatic这4个字节码指令时,对应的java代码场景为:new一个关键字或者一个实例化对象时、读取或设置一个静态字段时(final修饰、已在编译期把结果放入常量池的除外)、调用一个类的静态方法时
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没进行初始化,需要先调用其初始化方法进行初始化。
  3. 当初始化一个类时,如果其父类还未进行初始化,会先触发其父类的初始化。
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个类。
  5. 使用JDK 1.7等动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。


这5种情况被称为是类的主动引用,注意,这里《虚拟机规范》中使用的限定词是"有且仅有",那么,除此之外的所有引用类都不会对类进行初始化,称为被动引用。静态内部类就属于被动引用的行列。

那么,是不是可以说静态内部类单例就是最完美的单例模式了呢?其实不然,静态内部类也有着一个致命的缺点,就是传参的问题,由于是静态内部类的形式去创建单例的,故外部无法传递参数进去,例如Context这种参数,所以,我们创建单例时,可以在静态内部类与DCL模式里自己斟酌。

6.枚举模式
public class EnumSingleton {

    /**
     * 1.构造方法私有化
     */
    private EnumSingleton(){}

    private enum SingletonHolder{
        //创建一个枚举对象,该对象天生为单例
        INSTANCE;
        private EnumSingleton instance;

        SingletonHolder(){
            instance = new EnumSingleton();
        }

        public EnumSingleton getInstance(){
            return instance;
        }
    }

    /**
     * 对外暴露一个获取EnumSingleton对象的静态方法
     */
    public static EnumSingleton getInstance(){
        return SingletonHolder.INSTANCE.instance;
    }
    
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值