zl程序教程

您现在的位置是:首页 >  其他

当前栏目

设计模式 01 单例模式

2023-03-31 11:01:40 时间

单例模式(Singleton Pattern)属于创建型模式

概述

单例就是只有一个实例对象,即在整个程序中,同一个类始终只有一个对象进行操作。这样可以极大的减少内存开支和系统的性能开销,因此应用十分广泛。比如数据库连接类,实际上只需要创建一个对象或是直接使用静态方法就可以了,没必要去创建多个对象。

这种模式提供了一种创建对象的最佳方式,让类负责创建自己的对象,同时确保只有单个对象被创建。这个类需要提供访问其唯一对象的方式,且可以直接访问,不需要实例化该类的对象。

注意点:

  • 为保证只能由自己创建对象,单例类必须构造方法私有化
  • 单例类必须自己创建自己的唯一实例。
  • 单例类必须给所有其他对象提供这一实例。

经过多年的演进,单例模式有诸多实现方式,下面逐个介绍。

代码实现

饿汉式

饿汉式单例是最普通的单例模式写法,由于在类加载时就创建对象,保证了线程的安全。这种方式比较常用,但容易产生垃圾对象,对空间的消耗较大。

public class Singleton {
    /**
     * 单例模式的核心,构造方法私有化
     */
    private Singleton() {

    }

    /**
     * 用于全局引用的唯一单例对象,在一开始就创建好
     */
    private final static Singleton INSTANCE = new Singleton();

    /**
     * 获取全局唯一的单例对象
     * @return 实例对象
     */
    public static Singleton getInstance() {
        return INSTANCE;
    }
}

这种方式最大的问题就是浪费内存,因为创建的对象程序不一定用得到,如果创建了没用到,就是一种浪费。这种方式由于不存在线程安全问题, 因此不用加锁,效率较高,以空间换时间

想要避免这种浪费,自然就想到在使用的时候才创建对象,这样就诞生了懒汉式

懒汉式

普通

懒汉式单例就是在类加载时不创建对象,用到的时候才创建对象。

public class Singleton {
    /**
     * 单例模式的核心,构造方法私有化
     */
    private Singleton() {

    }
    
    /**
     * 在一开始先不进行对象创建
     */
    private static Singleton INSTANCE;

    /**
     * 获取全局唯一的单例对象
     * @return 单例对象
     */
    public static Singleton getInstance() {
        // 如果实例为空,那么就进行创建,不为空说明已经创建过了,就直接返回,保证单例
        if (INSTANCE == null) {    
            INSTANCE = new Singleton();
        }
        return INSTANCE;
    }
}

这种方式最大的问题就是在多线程情况下不安全,比如这样调用:

for (int i = 0; i < 10; i++) {
    new Thread(()->{
        Singleton.getInstance();
    }).start();
}

多线程环境下,非空判断容易失效,造成创建多个实例,违背了单例的初衷。为了避免这一问题,就不得不加锁,这样效率就会降低,以时间换空间

为了避免线程安全问题,还得进行一些改进:

// 方法添加 synchronized 关键字加锁
public static synchronized Singleton getInstance(){   
    if(INSTANCE == null) {
        INSTANCE = new Singleton();
    }
    return INSTANCE;
}

既然多个线程要调用,那么就直接加一把锁,在方法上添加 synchronized 关键字即可,这样同一时间只能有一个线程进入了。

虽然这样简单粗暴,但是在高并发的情况下,效率肯定是比较低的,可以再进行优化:

public static Singleton getInstance(){
    // 规避多线程情况
    if(INSTANCE == null) {
        // 只对赋值这一步进行加锁,提升效率
        synchronized (Singleton.class) {    
            INSTANCE = new Singleton();   
        }
    }
    return INSTANCE;
}

不过这样还不完美,因为这样还是有可能多个线程同时判断为 null 而进入等锁的状态。

所以,还得加一层内层判断:

public static Singleton getInstance(){
    if(INSTANCE == null) {
        synchronized (Singleton.class) {
            // 内层检测以实现单例
            if(INSTANCE == null) {
                INSTANCE = new Singleton();
            }
        }
    }
    return INSTANCE;
}

这样外层的检测用于规避多线程情况,内层的检测用于单例的实现,比较完善的均衡了安全和性能。

但是这种写法在极端情况还是可能会有一定的问题,因为 INSTANCE = new Singleton(); 不是原子性操作,它会经过三个步骤:

1、分配对象内存空间。

2、执行构造方法初始化对象。

3、设置 instance 指向刚分配的内存地址,此时 instance != null。

由于指令重排,导致 A 线程执行 INSTANCE = new Singleton(); 的时候,可能先执行了第 3 步。此时线程 B 又进来了,发现 INSTANCE 已经不为空了,直接返回,并且后面使用了返回的 INSTANCE。由于线程 A 还没有执行第 2 步,导致此时 INSTANCE 还不完整,可能会有一些意想不到的错误。

所以需要增加 volatile 关键字来避免指令重排:

private volatile static Singleton INSTANCE;

这样就实现了完整的双重检测锁(DCL,即 double-checked locking)懒汉式单例模式。

双重检测锁

public class Singleton {
    /**
     * 单例模式的核心,构造方法私有化
     */
    private Singleton() {

    }

    /**
     * 在一开始先不进行对象创建
     */
    private volatile static Singleton INSTANCE;

    /**
     * 获取全局唯一的单例对象
     * @return 单例对象
     */
    public static Singleton getInstance(){
        // 外层检测规避多线程情况
        if(INSTANCE == null) {
            // 只对赋值这一步进行加锁,提升效率
            synchronized (Singleton.class) {
                // 内层检测以实现单例
                if(INSTANCE == null) {
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}

双重校验锁模式虽然严谨,但终究需要加锁,效率始终会受到影响。那么,有没有一种更好的,不用加锁的方式也能实现延迟加载的写法呢?答案是有的,可以使用静态内部类

静态内部类实现

静态内部类实现依赖于 Java 在类加载时不会加载其内部类,只有实例化内部类时才会加载的特性。其他语言不具备这一特性,无法使用该方式实现,因此这种方式不具备通用性

public class Singleton {
    private Singleton() {

    }

    /**
     * 由静态内部类持有单例对象
     * <p>根据类加载特性,仅使用外部类时,不会对静态内部类进行初始化
     */
    private static class Holder {
        private final static Singleton INSTANCE = new Singleton();
    }

    /**
     * 只有真正使用内部类时,才会进行类初始化
     * @return 单例对象
     */
    public static Singleton getInstance(){
        // 直接获取内部类中的对象
        return Holder.INSTANCE;
    }
}

这种方式显然是最完美的懒汉式解决方案:没有进行任何的加锁操作,也能保证线程安全,还能实现懒加载。

不过要实现这种写法,跟语言本身也有一定的关联,并不是所有的语言都支持这种写法的。而且它也不是绝对安全的,因为 Java 还有一个十分霸道的东西:反射

万恶的反射

前面的写法尽管已经考虑得很完善,但还是忽略了反射

反射是很霸道的,无视 private 修饰的构造方法,可以直接在外面 newInstance,破坏单例。

Singleton singleton1 = Singleton.getInstance();
// 获取无参构造器
Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor(null);
// 取消无参构造器私有属性
declaredConstructor.setAccessible(true);
// 使用反射获取的无参构造器实例化对象
Singleton singleton2 = declaredConstructor.newInstance();
// 比较实例化的两个对象
System.out.println(singleton1 == singleton2);

输出结果为 false,同一个类实例化的两个对象不相等,单例被破坏了。那么,怎么解决这种问题呢?

可以在构造方法中加上对象的非空判断:

private Singleton() {
    synchronized (Singleton.class) {
        if (INSTANCE != null) {
        	throw new RuntimeException("不要试图用反射破坏单例");
        }
    }
}

但是这种写法还是有问题:

前面是先正常的调用了 getInstance 方法,创建了 Singleton 对象,然后第 2 次用反射创建对象,私有构造函数里面的判断起作用了,反射破坏单例模式失败。

但如果先用反射创建对象,判断就不生效了:

// 获取无参构造器
Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor(null);
// 取消无参构造器私有属性
declaredConstructor.setAccessible(true);
// 使用反射获取的无参构造器实例化对象
Singleton singleton1 = declaredConstructor.newInstance();
Singleton singleton2 = declaredConstructor.newInstance();
// 比较实例化的两个对象
System.out.println(singleton1 == singleton2);

输出结果为 false,同一个类实例化的两个对象不相等,单例又被破坏了!还有什么办法防止这种反射破坏呢?

可以使用标志位来避免重复创建:

private static boolean flag = false;

private Singleton() {
    synchronized (Singleton.class) {
        if (flag == false) {
            // 第一次创建后,标志位设为 true
        	flag = true;
        } else {
            // 后续再使用构造方法创建则直接报错
        	throw new RuntimeException("不要试图用反射破坏单例模式");
        }
    }
}

这样看起来很完美了,但还是不能完全防止反射破坏单例,因为可以利用反射修改 flag 的值。

// 获取无参构造器
Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor(null);
// 取消无参构造器私有属性
declaredConstructor.setAccessible(true);
// 使用反射获取的无参构造器实例化对象
Singleton singleton1 = declaredConstructor.newInstance();
// 获取 flag 属性
Field field = Singleton.class.getDeclaredField("flag");
// 取消 flag 属性的私有属性
field.setAccessible(true);
// 通过反射,修改属性的值
field.set(singleton1, false);
// 使用反射获取的无参构造器实例化对象
Singleton singleton2 = declaredConstructor.newInstance();
// 比较实例化的两个对象
System.out.println(singleton1 == singleton2);

输出结果为 false,同一个类实例化的两个对象不相等,单例又又被破坏了!正所谓道高一尺魔高一丈,懒汉式单例不管怎么挣扎,在反射面前都无计可施。

那么,就没有办法完美的实现单例这么一个最简单的设计模式了吗?答案是有的,但不是外界,而是 Java 自己给出的解决方案。正所谓解铃还须系铃人,反射是 Java 的语言特性,Java 赋予了它如此强大的能力,只能由 Java 自身来对其进行限制。在 Java 1.5 中推出的枚举就可以完美解决这个问题。

枚举实现

枚举Java 1.5 中新增特性的一部分,它是一种特殊的数据类型。之所以特殊是因为它既是一种类(class)却又比类多了些特殊的约束。正是这些约束的存在造就了枚举类型的简洁性安全性以及便捷性

先来看下枚举是怎么实现单例的:

public enum Singleton {

    /**
     * 单例对象
     */
    INSTANCE
    
}

你没有看错,就是这么简单,这也正所谓大道至简

尝试用反射进行破坏:

// 获取枚举对象
Singleton instance1 = Singleton.INSTANCE;
// 获取无参构造器
Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor(null);
// 取消无参构造器私有属性
declaredConstructor.setAccessible(true);
// 使用反射获取的无参构造器实例化对象
Singleton instance2 = declaredConstructor.newInstance();
// 比较实例化的两个对象
System.out.println(instance1 == instance2);

意料之外的是,运行报错了:

Exception in thread "main" java.lang.NoSuchMethodException: cn.sail.singleton.enums.Singleton.<init>()
	at java.lang.Class.getConstructor0(Class.java:3082)
	at java.lang.Class.getDeclaredConstructor(Class.java:2178)

从报错可以看出,是因为没有找到无参构造方法。

在 IDEA 中查看编译后的 Singleton.class 文件:

public enum Singleton {
    
    INSTANCE;

    private Singleton() {
    }
}

从编译后的文件可以看到,明明是存在无参构造的,这就奇怪了!

再用 Java 命令 javap -p 编译试试:

D:ProjectMYlearngof-23	argetclassescnsailsingletonenums> javap -p Singleton.class
Compiled from "Singleton.java"
public final class cn.sail.singleton.enums.Singleton extends java.lang.Enum<cn.sail.singleton.enums.Singleton> {
  public static final cn.sail.singleton.enums.Singleton INSTANCE;
  private static final cn.sail.singleton.enums.Singleton[] $VALUES;
  public static cn.sail.singleton.enums.Singleton[] values();
  public static cn.sail.singleton.enums.Singleton valueOf(java.lang.String);
  private cn.sail.singleton.enums.Singleton();
  static {};
}

从输出结果可以看到,枚举本质还是一个类,只是继承了枚举类

不过这里还是输出了无参构造

private cn.sail.singleton.enums.Singleton();

再使用更专业的工具 jad 进行反编译:

D:ProjectMYlearngof-23	argetclassescnsailsingletonenums>jad -sjava Singleton.class
Parsing Singleton.class... Generating Singleton.java

执行该命令后,会在该目录下生成反编译的 Singleton.java 文件:

public final class Singleton extends Enum
{

    public static Singleton[] values()
    {
        return (Singleton[])$VALUES.clone();
    }

    public static Singleton valueOf(String name)
    {
        return (Singleton)Enum.valueOf(cn/sail/singleton/enums/Singleton, name);
    }

    private Singleton(String s, int i)
    {
        super(s, i);
    }

    public static final Singleton INSTANCE;
    private static final Singleton $VALUES[];

    static 
    {
        INSTANCE = new Singleton("INSTANCE", 0);
        $VALUES = (new Singleton[] {
            INSTANCE
        });
    }
}

从这里可以看到,确实是没有无参构造,而是存在一个有参构造

private Singleton(String s, int i) {
    super(s, i);
}

按照有参构造再使用反射创建:

// 获取枚举对象
Singleton instance1 = Singleton.INSTANCE;
// 获取无参构造器
Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor(String.class, int.class);
// 取消无参构造器私有属性
declaredConstructor.setAccessible(true);
// 使用反射获取的无参构造器实例化对象
Singleton instance2 = declaredConstructor.newInstance();
// 比较实例化的两个对象
System.out.println(instance1 == instance2);

此时运行也报错,但报的错不一样了

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects

这就是希望得到的结果:不能用反射破坏枚举

那么这个报错是怎么实现的呢,其实很简单。

来看下反射中 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);
        }
    }
    if ((clazz.getModifiers() & Modifier.ENUM) != 0)
        throw new IllegalArgumentException("Cannot reflectively create enum objects");
    ConstructorAccessor ca = constructorAccessor;
    if (ca == null) {
        ca = acquireConstructorAccessor();
    }
    @SuppressWarnings("unchecked")
    T inst = (T) ca.newInstance(initargs);
    return inst;
}

可以看到,如果类为反射类,就会直接报错:

if ((clazz.getModifiers() & Modifier.ENUM) != 0)
        throw new IllegalArgumentException("Cannot reflectively create enum objects");

这就从底层就避免了反射对枚举实现单例的破坏,是最直接也是最有效的手段。所以,枚举实现是最完美的单例模式

它极致简洁,支持懒加载,自动支持序列化机制,绝对防止多次实例化。它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,避免多次实例化。最让人感动的是,它不会被反射破坏!

这种方式也是 《Effective Java》 作者 Josh Bloch 提倡的方式。

但因为必须 Java 1.5 及之后的版本才支持这种方式,所以还没有被广泛采用,但这确实是实现单例模式的最佳方法最简单方法,也正验证了单例模式是最简单的设计模式

优缺点

优点

1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。

2、避免对资源的多重占用(比如写文件操作)。

缺点

没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。

使用场景

1、要求生产唯一序列号。

2、Web 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。

3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。

注意事项

getInstance() 方法中需要使用同步锁 synchronized(Singleton.class) 防止多线程同时进入造成 instance 被多次实例化。


参考

https://www.bilibili.com/video/BV1K54y197iS?spm_id_from=333.999.0.0&vd_source=299f4bc123b19e7d6f66fefd8f124a03