设计模式-你不知道的单例模式

前言

前几天经历了一次面试,原本的打算是检查自己的水平,好家伙,这一次面试直接给我干自闭了。😭😭😭

内容如题目所述: 单例模式 ,如果你目前对于自己的 Java 有一点自信的话,那我建议你看看!

Joshua Bloch 大神说过的一句话: 实现单例模式的最佳方法是使用枚举


简介

单例模式(Singleton Pattern):确保只有一个类 有且只有 一个实例,并提供一个全局访问点。

在实际开发中,很多对象我们仅需要一个,例如:线程持( threadpool )、缓存( cache )、默认设置、注册表( registry )、日志对象等等,而这个时候将他们设计为单例模式是最好的方式。

Java 中单例模式是一种很广泛使用的设计模式。
单例模式有很多好处:

  • 避免对象的重复创建。
  • 减少每次创建对象时的时间开销。
  • 节约内存空间(例如 Spring 管理的无状态 bean )。
  • 避免多个实例之间操作导致的逻辑错误。
  • 如果一个对象可能会贯穿整个应用,那么还会起到全局统一管理配置的作用。

方法

单例模式的写法非常多,但很多写法存在一些不足,下面以示例的方式加以指出。

懒汉(线程不安全)

1
2
3
4
5
6
7
8
9
10
11
public class Singleton {
private static Singleton instance;
private Singleton (){} //私有构造函数

public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

这种写法 lazy loading(懒加载)很明显,但是一看就知道,存在线程安全问题,所以这种写法是被禁止的。

懒汉(线程安全)

1
2
3
4
5
6
7
8
9
10
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

这种方式加了一个 synchronized 关键字来保证线程安全,但是效率太低了,毕竟 99.99% 的情况下是不需要同步的,有点用力过猛。极力不推荐使用!

饿汉

1
2
3
4
5
6
7
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}

这种基于 classloader 方式b避免了多线程的同步问题,在类进行初始化的时候进行装载。这是目前最简单的实现方式。

饿汉(变种)

1
2
3
4
5
6
7
8
9
10
public class Singleton {  
private static Singleton instance = null;
static {
instance = new Singleton();
}
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}

跟上面的类似,只是在类初始化是进行初始化实例。

静态内部类

1
2
3
4
5
6
7
8
9
public class Singleton {
private static class SingletonHolder { // 静态内部类
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}

刚分析了饿汉模式下的实现方式没有 lazy loading 的效果。
而这种方式类虽然被装载了,但是没有立刻进行初始化,因为静态内部类并没有被主动使用,只有显式调用 getInstance() 方法时,才会装载 SingletonHolder 类,显然他达到了 lazy loading 的效果。

双重校验锁(懒汉)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton {  
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) { // 注意此处还得有次判空~
singleton = new Singleton();
}
}
}
return singleton;
}
}

使用了 volatile 机制,保证了线程之间的可见性,这种方法俗称 双重检查锁定 。既保证了效率也保证了安全,不过代码显得比较复杂但是看起来比较高级。

枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Singleton {
private Singleton (){}
public static Singleton getInstance() {
return EnumSingleton.INSTANCE.getInstance();
}
}
public enum EnumSingleton {
INSTANCE;
private final Singleton insatnce;
EnumSingleton() {
insatnce = new Singleton();
}
private Singleton getInstance() {
return instance;
}
}

这种方式是 Effective Java 的作者 Josh Bloch 提倡的方式,它不仅可以避免线程同步的问题,而且还可以防止序列化重新创建新的对象。所以目前这种写法是十分推荐的而且是最优的。

那么前几种方式实现单例模式都有以下三个特点:

  • 构造方法私有化。
  • 实例化的变量引用私有化。
  • 获取变量的方法共有。
  1. 关于第一点 构造方法私有化 它并不保险,因为它无法抵挡 反射攻击 ,比如以下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public class Singleton implements Serializable {
    private static Singleton instance = new Singleton();
    private Singleton() {}
    public static Singleton getInstance() {
    return instance;
    }
    }

    public class Main() {
    public static void main(String[] args) {
    Singleton s = new Singleton.getInstance();

    Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor(); // 拿到所有的构造函数,包括非 public 的
    constructor.setAccessible(true);
    Singleton sReflection = constructor.newInstance(); // 使用空构造函数 new 一个实例。即使它是 private 的

    System.out.println(s); // cn.vgbhfive.beans.Singleton@1f32e575
    System.out.println(sReflection); // cn.vgbhfive.beans.Singleton@279f2327
    System.out.println(s == sReflection); // false
    }
    }

    通过反射,竟然给所谓的单例创建出了一个新的实例对象。所以这种方式也还是存在不安全因素的。

  2. 再看看前几种方法的序列化和反序列化会不会出问题,如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class Main {
    public static void main(String[] args) {
    Singleton instance = new Singleton();

    byte[] serialize = SerializationUtils.serialize(instance);
    Object deserialize = SerializationUtils.deserialize(serialize);

    System.out.println(instance); // cn.vgbhfive.beans.Singleton@1f32e575
    System.out.println(deserialize); // cn.vgbhfive.beans.Singleton@279f2327
    System.out.println(instance == deserialize); // false
    }
    }

    可以通过结果看出 序列化前后两个对象并不相等 ,所以序列化也是不安全的。

  3. 最后就来测试枚举在序列化和反序列化是否安全,如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class Main {
    public static void main(String[] args) {
    Singleton instance = EnumSingleton.INSTANCE;

    byte[] serialize = SerializationUtils.serialize(instance);
    Object deserialize = SerializationUtils.deserialize(serialize);

    System.out.println(instance);
    System.out.println(deserialize);
    System.out.println(instance == deserialize); // true
    }
    }

    上面的结果已经很明显了,枚举类型对于序列化和反序列化是安全的

  4. 关于枚举在反射获取新实例方面的安全保障,主要在于以下几点方面:

  • 无空的构造函数。
  • 枚举类在创建对象时会检查该类是否有 ENUM 修饰。
    具体内容可以去看看 Enum 相关的源码。
  1. 综上,可以得出的结论:枚举是实现单例模式的最佳实践 。优点如下:
  • 反射安全。
  • 序列化和反序列化安全。
  • 写法简单。
  • 其他方法都不足以说服不去使用枚举。

总结

单例模式作为设计模式中最简单、易理解的一种设计模式,在很多地方都有运用,也有极大的概率在面试中遇到。(比如我

Effective Java 这是一本很好的书,建议阅读,我买的书已经在路上了。


个人备注

此博客内容均为作者学习所做笔记,侵删!
若转作其他用途,请注明来源!