前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >设计模式系列:经典的单例模式

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

作者头像
小灰
发布2023-09-02 08:54:41
1370
发布2023-09-02 08:54:41
举报
文章被收录于专栏:程序员小灰程序员小灰

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

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

正文如下:

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

单例模式可谓是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实现单例模式的几种方法各有优缺点,以下是它们的对比小结:


思考题:

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

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

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

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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-07-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序员小灰 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档