设计模式-单例模式

462 阅读5分钟

这是我参与8月更文挑战的第9天,活动详情查看:8月更文挑战

在日常开发中,有一些对象实例一般只需要一个,例如线程池对象、打印机等设备驱动的对象等。倘若有多个实例,容易出现资源使用过量等期望外的情况。这时候,为了保证对象的唯一性,我们可以采用单例模式。

单例模式(Singleton Pattern,也称为单件模式),使用最广泛的设计模式之一。其意图是保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。

单例模式按照实例初始化时间划分,可以分为懒汉模式饿汉模式

  • 饿汉模式:在类加载的时候,就生成实例。
  • 懒汉模式:在第一次使用的时候,才生成实例。

按照实现方式划分,又可以分为饿汉模式双重校验锁模式内部类模式枚举类模式。其中双重校验锁模式内部类模式枚举类模式都属于懒汉模式。

单例模式的核心要点

  1. 构造方法私有化。
  2. 在类的内部产生并持有唯一一个实例化对象。
  3. 通过类静态方法向外部提供实例化对象的引用。

饿汉模式

饿汉模式得名于它的实例化对象的初始化时间,早在类加载的过程的准备阶段,实例化对象就被产生。

具体代码如下

public class HungrySingleton {
    private static final HungrySingleton hungrySingleton = new HungrySingleton();
​
    private HungrySingleton(){}
​
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
}

双重校验锁模式

双重校验锁模式属于懒汉模式,为了避免线程安全问题而进行双重校验锁。

具体代码如下:

public class DoubleCheckSingleton {
    private volatile static DoubleCheckSingleton doubleCheckSingleton = null;
​
    private DoubleCheckSingleton(){}
​
    public static DoubleCheckSingleton getInstance(){
        if (doubleCheckSingleton == null){
            synchronized (DoubleCheckSingleton.class){
                if (doubleCheckSingleton == null){
                    doubleCheckSingleton = new DoubleCheckSingleton();
                }
            }
        }
        return doubleCheckSingleton;
    }
}

有几个点需要注意的是:

  1. 在第一次使用的时候,才初始化实例。
  2. 在实例化对象判空采用双重校验锁的形式。
  3. 对于实例化对象,需要volatile修饰。

对于第二点,倘若不进行加锁操作,即

    public static DoubleCheckSingleton getInstance(){
    1.    if (doubleCheckSingleton == null){
    2.          doubleCheckSingleton = new DoubleCheckSingleton();
            }
        }
    3.    return doubleCheckSingleton;
    }

在多线程环境中,可能出现多例的情况出现。

假设有A、B两线程,当线程A运行完语句1,即if (doubleCheckSingleton == null)后,线程A的时间片结束,此时轮到线程B运行。线程B运行语句1、2、3后,时间片结束,此时,doubleCheckSingleton已被成功创建并返回。之后轮到线程A执行,由于之前已执行语句1,此时就无需进行判空操作,直接运行语句2、语句3,生成并返回另一个doubleCheckSingleton。此时出现多例情况。

对于第三点,volatile在这里的主要作用是防止指令重排。

volatile:

  • 保证变量的内存可见性:使用 volatile 修饰共享变量后,每个线程要操作变量时会从主内存中将变量拷贝到本地内存作为副本,当线程操作变量副本并写回主内存后,会通过 CPU 总线嗅探机制告知其他线程该变量副本已经失效,需要重新从主内存中读取。
  • 禁止指令重排序:使用 volatile 修饰变量时,根据 volatile 重排序规则表,Java 编译器在生成字节码时,会在指令序列中插入内存屏障指令来禁止特定类型的处理器重排序。

倘若不使用volatile修饰,容易出现获取到未初始化后的对象,即在线程执行到语句2的时候,代码读取到doubleCheckSingleton不为null时,doubleCheckSingleton引用的对象有可能还没有完成初始化。

主要的原因是重排序。重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。

对于一个语句doubleCheckSingleton = new DoubleCheckSingleton();,可以分为3步操作。

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

对于这3步操作,CPU可能会对操作2和操作3进行指令重排,即

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

在单线程环境中,这种情况并不会产生什么问题。但在多线程环境中,就可能产生上面提到的问题了。

内部类模式

内部类模式同样属于懒汉模式,它借助内部类的延迟加载机制(只会在第一次使用时加载,不使用就不加载),来实现懒汉模式。

内部类特点在于,单例对象并非由自己所在的类持有,而是由内部类持有。 具体代码如下:

public class StaticInnerSingleton {
​
    private StaticInnerSingleton(){}
​
    public StaticInnerSingleton getInstance(){
        return Holder.staticInnerSingleton;
    }
​
    private static class Holder{
        private static final StaticInnerSingleton staticInnerSingleton
            = new StaticInnerSingleton();
    }
}

枚举类模式

枚举类模式借助Java枚举类型的特性而实现的单例模式。

它主要有以下优点:

  1. 实现简单。
  2. 能够防御反射和序列化攻击。

具体代码实现如下:

public enum EnumSingleton {
    INSTANCE;   // 1个命名为INSTANCE的EnumSingleton实例化对象
​
    private int value = 1;
​
    public static EnumSingleton getInstance(){
        return INSTANCE;
    }
​
    public int getValue() {
        return value;
    }
​
    public void setValue() {
        value = value+1;
    }
}

对于其他单例模式,可以通过反射的方式进行破坏,即通过反射获取构造方法,从而获取一个新的实例化对象。

        // 通过类的全限定名以反射的方式获取class      
        Class<HungrySingleton> aClass = 
            (Class<HungrySingleton>)Class.forName("singleton.HungrySingleton");
        // 获取构造方法
        Constructor<HungrySingleton> constructor = aClass.getDeclaredConstructor();
        // 通过设置允许访问,来绕过private方法的限制
        constructor.setAccessible(true);
        // 实例化对象
        HungrySingleton hungrySingleton = constructor.newInstance();

而对于枚举类,反射在通过newInstance()创建对象时,会检查该类是否被enum修饰,如果是则抛出异常,反射失败。

// Constructor类 newInstance源码
  @CallerSensitive
    public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        // 检查是否是Enum对象
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }