目录

单例模式

目录

一、单例模式理论知识

1. 为什么要使用单例?:

  • 保证一个类只会创建一个实例,以保证对某个属性或方法有全局唯一的访问入口,防止资源访问冲突

2. 优点:

  • 减少维护对象的开销:创建、内存占用、回收

  • 避免对资源的多重占用:例如写文件操作

3. 使用场景:

  • 要求生成唯一序列号的场景

  • 创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源

  • 在整个项目中需要一个共享访问点或共享数据:例如一个Web页面上的计数器,可以不用把每次刷新都记录到数据库中,使用单例模式保持计数器的值,并确保是线程安全的

4. 注意问题

  • 线程安全:防止多个线程同时执行调用到了new方法,导致实例化出多个对象

  • 指令重排:new实例的语句底层指令可能发生重排导致返回未实例化好的对象

  • 避免外部用特殊方式创建实例

    • 构造函数需要是 private 访问权限的,初步避免外部通过 new 创建实例
    • 反射构造:私有构造函数抛异常 if (instance != null) throw new RuntimeException(“private constructor”);
    • 反序列化:添加readResolve方法,直接return instance;

二、单例模式具体实践

实现方式:

  1. 饿汉式
  2. 懒汉式(双重检测)
  3. 静态内部类
  4. 枚举

(一)饿汉式(非懒加载)

public class Singleton1 {

    /**
    * 是否 Lazy 初始化:否
    * 是否多线程安全:是
    * 实现难度:易
    * 描述:这种方式比较常用,但容易产生垃圾对象。
    * 优点:没有加锁,执行效率会提高。
    * 缺点:类加载时就初始化,浪费内存(但是避免了后期内存不够导致初始化失败,取舍需按实际情况分析)。
    *
    * 它基于 classloader 机制避免了多线程的同步问题,
    * 不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,
    * 在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,
    * 这时候初始化 instance 显然没有达到 lazy loading 的效果。
    */

    //单例对象 加载时就实例化
    private static Singleton1 instance = new Singleton1();

    //私有构造方法
    private Singleton1(){}

    //静态工厂方法
    public static Singleton1 getInstance() {
        return instance;
    }
}

(二)懒汉式(双重检测 懒加载且绝对的线程安全)

/**
* 优点:延迟加载、支持高并发
*/
public class Singleton2 {

    //私有构造函数
    private Singleton2() {}

    //单例对象 添加volatile关键字
    private volatile static Singleton2 instance = null;

    //静态工厂方法-获取单例对象
    public static Singleton2 getInstance() {
        Singleton2 localRef = instance;
        if (localRef == null) {      //双重检测机制
            synchronized (Singleton2.class){  //同步锁
                if (localRef == null) {     //双重检测机制
                    instance = new Singleton2();
                }
            }
        }
        return localRef;
    }
}
/**
* 	Using localRef, we are reducing the access of volatile variable to just one for positive usecase. 
* If we do not use localRef, then we would have to access volatile variable twice - once for checking null and then at method return time. 
* Accessing volatile memory is quite an expensive affair because it involves reaching out to main memory.
*/

(三)静态内部类方式(和饿汉式类似,但是是懒加载)

/**
* 单例模式例四(静态内部类方式) 懒加载
*
* 这种方式能达到双检锁方式一样的功效,但实现更简单。
* 对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。
* 这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
*/
public class Singleton3 {

    /**
    * 1.从外部无法访问静态内部类LazyHolder,只有当调用Singleton.getInstance方法的时候,才能得到单例对象INSTANCE。
    *
    * 2.INSTANCE对象初始化的时机并不是在单例类Singleton被加载的时候,而是在调用getInstance方法,
    * 使得静态内部类LazyHolder被加载的时候。因此这种实现方式是利用classloader的加载机制来实现懒加载,并保证构建单例的线程安全。
    *
    *
    *存在问题:无法防止利用反射来重复构建对象
    * 复现第一步,获得单例类的构造器。
    * 复现第二步,把构造器设置为可访问。
    * 复现第三步,使用newInstance方法构造对象。
    *
    * <code>
    * //获得构造器
    * Constructor con = Singleton.class.getDeclaredConstructor();
    *
    * //设置为可访问
    * con.setAccessible(true);
    *
    * //构造两个不同的对象
    * Singleton singleton1 = (Singleton)con.newInstance();
    * Singleton singleton2 = (Singleton)con.newInstance();
    *
    * //验证是否是不同对象,结果为false
    * System.out.println(singleton1.equals(singleton2));
    * </>
    *
    *
    */

    //私有构造方法
    private Singleton3(){}

    //静态工厂方法-获取单例对象
    public static Singleton3 getInstance() {
        return LazyHolder.INSTANCE;
    }

    //静态内部类
    private static class LazyHolder {
        private static final Singleton3 INSTANCE = new Singleton3();
    }
}

(四)枚举类方式(非懒加载)

/**
* 单例模式例五(枚举类方式) 非懒加载
* 目前此方式是所有单例模式中最推荐的模式?
*
*/
public class Singleton4 {

    /**
    * JVM会保证enum不能被反射并且构造器方法只执行一次
    *
    * 枚举实现可以防止反射构造对象,而且可以保证线程安全
    * 缺点:并非使用懒加载,单例对象是在枚举类被加载的时候进行初始化的
    */
    //私有构造方法
    private Singleton4(){}

    //静态方法
    public static Singleton4 getInstance(){
        return SingletonEnum.INSTANCE.getInstance();
    }

    private static enum SingletonEnum{
        /**
        * 单例对象
        */
        INSTANCE;

        private Singleton4 singleton4;

        //JVM会保证此方法绝对只调用一次
        SingletonEnum(){
            singleton4 = new Singleton4();
        }

        public Singleton4 getInstance() {
            return singleton4;
        }
    }


}

(五)反例:懒汉式(线程不安全,严格来说不算是单例)

/**
* 单例模式例一(线程不安全)懒汉式
*/
public class Singleton_1 {

    /**
    * 是否 Lazy 初始化:是
    * 是否多线程安全:否
    * 实现难度:易
    * 描述:这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。
    * 这种方式 lazy loading 很明显,不要求线程安全,在多线程下不能正常工作。
    *
    * 为什么说是线程不安全的
    * 假设Singleton类刚刚被初始化,instance对象还是空,这时候两个线程同时访问getInstance方法
    * 因为Instance是空,所以两个线程同时通过了条件判断,开始执行new操作
    * 这样一来,显然instance被构建了两次。
    */

    //私有构造函数
    private Singleton_1() {}

    //单例对象
    private static Singleton_1 instance = null;

    //静态工厂方法-获取单例对象的方法
    public static Singleton_1 getInstance() {
        if (instance == null) {
            instance = new Singleton_1();
        }
        return instance;
    }
}

(六)反例:懒汉式 (双重检测机制,不是绝对的线程安全版本少了volatile关键字,仅发生于低版本JDK中)

/**
* 单例模式例二(双重检测机制) 懒汉式
*/
public class Singleton_2 {


    /**
    * 为什么说不是绝对的线程安全
    * 这里涉及到了JVM编译器的指令重排。
    *
    * 指令重排是什么意思呢?比如java中简单的一句 instance = new Singleton,会被编译器编译成如下JVM指令:
    *
    *
    * memory =allocate(); //1:分配对象的内存空间
    * ctorInstance(memory); //2:初始化对象
    * instance =memory; //3:设置instance指向刚分配的内存地址
    *
    *
    * 但是这些指令顺序并非一成不变,有可能会经过JVM和CPU的优化,指令重排成下面的顺序:
    *
    *
    * memory =allocate(); //1:分配对象的内存空间
    * instance =memory; //3:设置instance指向刚分配的内存地址
    * ctorInstance(memory); //2:初始化对象
    *
    *
    * 当线程A执行完1,3,时,instance对象还未完成初始化,但已经不再指向null。此时如果线程B抢占到CPU资源,
    * 执行 if(instance == null)的结果会是false,从而返回一个没有初始化完成的instance对象。
    */


    //私有构造函数
    private Singleton_2() {}

    //单例对象
    private static Singleton_2 instance = null;

    //静态工厂方法-获取单例对象的方法
    public static Singleton_2 getInstance() {
        //双重检测机制
        if (instance == null) {
            //同步锁 锁住整个类(注意,这里不能使用对象锁)
            synchronized (Singleton_2.class){
                //双重检测机制
                //进入Synchronized 临界区以后,还要再做一次判空。
                //因为当两个线程同时访问的时候,线程A构建完对象,线程B也已经通过了最初的判空验证并马上获取到了锁,
                //不做第二次判空的话,线程B以为还未初始化还是会再次构建instance对象。
                if (instance == null) {
                    instance = new Singleton_2();
                }
            }
        }
        return instance;
    }

}

三、单例模式经典案例

  • Spring中,项目加载时bean自动创建,而后每次调用bean的时候由IOC容器注入。对于IOC容器,实现了Singleton模式。采用双重判断加锁的单例模式。所以对于Spring项目建议不要自己写单例模式,使用Spring DI

  • JDK中,Runtime 类封装了Java运行时的环境。每一个java程序实际上都是启动了一个JVM进程,那么每个JVM进程都是对应这一个Runtime实例,此实例是由JVM为其实例化的。采用饿汉式单例模式。

四、参考链接

← 返回技术