单例模式,是设计模式当中非常重要的一种,在面试中也常常被考察到。
在小灰的知识星球里,有一位小伙伴分享了单例模式的知识,写得非常清晰详尽。小灰把这篇干货文章分享到公众号上,希望能够帮助到大家。
正文如下:
一、什么时候使用单例模式?
单例模式可谓是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实现单例模式的几种方法各有优缺点,以下是它们的对比小结:
思考题:
相对于单例模式,是否可以有多例模式,多例模式该如何实现?
生活中有哪些单例模式、多例模式的例子?
你熟悉的编程语言、框架中有哪些单例模式、多例模式的例子?
你编写的代码中是否应用了单例模式、多例模式?