设计模式系列:经典的单例模式

2023-09-02 08:54:41 浏览数 (2)

单例模式,是设计模式当中非常重要的一种,在面试中也常常被考察到。

在小灰的知识星球里,有一位小伙伴分享了单例模式的知识,写得非常清晰详尽。小灰把这篇干货文章分享到公众号上,希望能够帮助到大家。

正文如下:

一、什么时候使用单例模式?

单例模式可谓是23种设计模式中最简单、最常见的设计模式了,它可以保证一个类只有一个实例。我们平时网购时用的购物车,就是单例模式的一个例子。想一想,如果购物车不是单例的,会发生什么?

数据不一致:用户在不同页面看到的购物车内容可能不同。用户在一个页面加了商品,可能换到另一个页面就看不到了、或者看到的商品不对。这会让用户感到困惑和不满。

购物车状态丢失:用户在不同服务器上访问的购物车实例可能不同。用户在一个页面加了商品,如果下一个请求被转到另一个服务器,那么之前加的商品就没了。这可能导致用户重新选购,那实在是太麻烦了。

资源浪费:购物车需要加载和处理一些数据,假如用户每次访问页面都创建一个新的购物车实例,这样就会占用更多的资源,并且、频繁地创建和销毁购物车实例,也会增加系统的负担和响应时间。

所以,用单例模式来做购物车可以避免以上问题,并提供更好的用户体验。购物车作为一个共享的对象,把用户选的商品信息保存在一个唯一的实例中,可以在整个用户会话中访问和更新,这样可以保证购物车中的数据是正确、完整和一致的。这其实也和我们生活中,在超市里使用购物小推车或购物篮是一样的。

Spring是Java开发中常用的框架,它里面也有很多单例模式的应用:

ApplicationContext:Spring的核心类之一,负责管理和配置应用程序的Bean。ApplicationContext是单例模式的实例,保证整个应用程序中只有一个ApplicationContext。

Bean对象:在Spring中,通过配置文件或注解方式定义的Bean对象通常也是单例的,默认情况下,Spring会把它们当作单例来管理。这意味着在应用程序中任何地方,通过Spring注入或获取Bean对象时,都是同一个实例。

缓存对象:在Spring中,可以使用缓存注解来实现方法级的缓存策略。这些缓存对象通常也是单例模式的实例,保证在多个方法调用中共享和管理缓存数据。

事务管理器:Spring的事务管理器通常也是单例模式的实例。事务管理器用于处理数据库事务,并保证整个应用程序中保持事务的一致性。

AOP切面:Spring的AOP(面向切面编程)通常也使用单例模式来管理切面。切面用于实现横切关注点的模块化,并可以在多个对象和方法中应用。通过使用单例模式,Spring可以保证在整个应用程序中共享和管理切面对象。

单例模式是关于对象创建的设计模式,当我们需要某个类在整个系统运行期间有且只有一个实例,就可以考虑使用单例模式。


二、Java实现单例模式的几种方式

在Java中,如何实现单例模式呢?经典的单例模式有同样经典的2种实现方式:“饿汉式”“懒汉式”

先来看“饿汉式”:

代码语言:javascript复制
public final class Hungry { // final 不允许被继承

    // 在类初始化过程中收入<clinit>()方法中,该方法能100%保证同步;final 保证不被改变
    private static final Hungry instance = new Hungry();

    private Hungry() {
    }

    public static Hungry getInstance() {
        return instance;
    }

}

“饿汉式”是一种最简单直接的实现方式,它的好处是在多线程环境下应用时是安全的,来验证下:

代码语言:javascript复制
public static void main(String[] args) throws Exception {
    for (int i = 0; i < 20; i  ) {
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName()   " "   Hungry.getInstance());
        }).start();
    }
}

运行结果如下:

Thread-6 gof23.creational.singleton.Hungry@7af2da85

Thread-8 gof23.creational.singleton.Hungry@7af2da85

Thread-10 gof23.creational.singleton.Hungry@7af2da85

Thread-7 gof23.creational.singleton.Hungry@7af2da85

Thread-11 gof23.creational.singleton.Hungry@7af2da85

Thread-12 gof23.creational.singleton.Hungry@7af2da85

Thread-13 gof23.creational.singleton.Hungry@7af2da85

Thread-14 gof23.creational.singleton.Hungry@7af2da85

Thread-15 gof23.creational.singleton.Hungry@7af2da85

Thread-16 gof23.creational.singleton.Hungry@7af2da85

Thread-17 gof23.creational.singleton.Hungry@7af2da85

Thread-19 gof23.creational.singleton.Hungry@7af2da85

Thread-18 gof23.creational.singleton.Hungry@7af2da85

Thread-9 gof23.creational.singleton.Hungry@7af2da85

可见,不同线程得到的对象都是同一个,符合“单例”。但是,这个“单例”是否牢不可破呢?再来运行下面这段代码:

代码语言:javascript复制
public static void main(String[] args) throws Exception {
    Hungry instance1 = Hungry.getInstance();
    Constructor<Hungry> constructor = Hungry.class.getDeclaredConstructor(null);
    Hungry instance2 = constructor.newInstance();
    Hungry instance3 = constructor.newInstance();
    System.out.println("非反射:"   instance1.hashCode());
    System.out.println("反射1:"   instance2.hashCode());
    System.out.println("反射2:"   instance3.hashCode());
}

运行结果如下:

非反射:2062736005

反射1:1072408673

反射2:1531448569

可以看到,“单例”不单、它被反射破坏了。

并且,“饿汉式”还有一个缺点是:当我们还没有使用它时,它就已经被实例化了,这就会造成资源浪费;由此,产生了“懒汉式”实现方式,它在我们第1次使用时才进行实例化:

代码语言:javascript复制
public final class Lazy { // final 不允许被继承
    private static Lazy instance;
    private Lazy() {
    }
    public static Lazy getInstance() {
        if (instance == null) {
            instance = new Lazy();
        }
        return instance;
    }
}

但是,上面这样的“饿汉式”代码在多线程环境下是不安全的、并且同样也会被反射破坏。

要将它改为线程安全的,有以下2种方法:

方法1,为 getInstance 方法加上 synchronized 关键字:

代码语言:javascript复制
public static synchronized Lazy getInstance() {
    if (instance == null) {
        instance = new Lazy();
    }
    return instance;
}

方法2,通过双重检查锁

代码语言:javascript复制
public static Lazy getInstance() {
    if (instance == null) {
        synchronized (Lazy.class) {
        if (instance == null) {
            instance = new Lazy();
        }
        }
    }
    return instance;
}

需要注意的是,双重检查锁方式在多线程环境下可能会产生NPE,因为new Lazy()并非原子操作,它将经历:1-分配内存空间,2-执行构造函数创建对象,3-对象指向空间这几个步骤,而步骤2、3可能会被重排序从而引发NPE。

那么,是否有办法可以避免NPE呢?很简单,为 instance 加上 volatile 关键字即可:

代码语言:javascript复制
private volatile static Lazy instance;

除了“饿汉式”和“懒汉式”,还有别的实现方式吗?答案是肯定的,我们还可以通过静态内部类来实现单例模式:

代码语言:javascript复制
public final class Holder {
    private Holder() {
    }
    /**
    * 调用getInstance实际上是获得InnerHolder的instance静态属性
    */
    public static Holder getInstance() {
        return InnerHolder.instance;
    }
    private static class InnerHolder {
        private static Holder instance = new Holder();
    }
}

静态内部类方式是线程安全的,但它仍然逃不过被反射破坏的命运。

那么,有不会被反射破坏的实现方式吗?来看下列代码:

代码语言:javascript复制
public enum EnumSingleton implements Serializable {
    INSTANCE;
    EnumSingleton() {
    }
    public static EnumSingleton getInstance() {
        return INSTANCE;
    }
}

来测试一下:

代码语言:javascript复制
public static void main(String[] args) throws Exception {
    // 通过反编译工具看到确实没有无参构造函数,而是String,int的2个参数的构造函数
    Constructor<EnumSingleton> constructor = EnumSingleton.class.getDeclaredConstructor(String.class, int.class);
    constructor.setAccessible(true);
    // 将抛出 java.lang.IllegalArgumentException: Cannot reflectively create enum objects
    EnumSingleton instance1 = constructor.newInstance();
    System.out.println("反射1:"   instance1.hashCode());
}

当我们想要通过反射来得到实例时,将得到异常,这次这个破坏王终于被打败啦。遗憾的是,这种实现方式无法延迟加载。

最后,再来看看静态内部类 枚举类这种实现方式:

代码语言:javascript复制
public final class HolderEnum {
    private HolderEnum() {
    }
    public static HolderEnum getInstance() {
        return Holder.INSTANCE.getInstance();
    }
    // 使用枚举类充当holder
    private enum Holder {
        INSTANCE;
        private HolderEnum instance;
        Holder() {
            this.instance = new HolderEnum();
        }
        private HolderEnum getInstance() {
            return instance;
        }
    }
}

经过测试,这种实现方式可以延迟加载、在多线程环境下安全、但却还是逃不过“反射”这个破坏王。

综上,Java实现单例模式的几种方法各有优缺点,以下是它们的对比小结:


思考题:

相对于单例模式,是否可以有多例模式,多例模式该如何实现?

生活中有哪些单例模式、多例模式的例子?

你熟悉的编程语言、框架中有哪些单例模式、多例模式的例子?

你编写的代码中是否应用了单例模式、多例模式?

0 人点赞