单例模式

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

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

注意:

  • 1、单例类只能有一个实例。
  • 2、单例类必须自己创建自己的唯一实例。
  • 3、单例类必须给所有其他对象提供这一实例。

优点:

  • 1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
  • 2、避免对资源的多重占用(比如写文件操作)。

缺点:

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


使用场景:

  • 1、要求生产唯一序列号。
  • 2、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
  • 3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。


单例模式的几种实现方式


饿汉模式

描述:基于 classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。

如果后续并没有使用到,可能会造成浪费空间

线程安全问题与登记式是一样的

1
2
3
4
5
6
7
8
9
10
public class Hungry {

private Hungry(){}

private final static Hungry HUNGRY = new Hungry();

public static Hungry getInstance(){
return HUNGRY;
}
}



登记式

这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。

描述:它跟饿汉式不同的是:饿汉式只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Holder类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance。


但没有同步锁是如何保证线程安全的呢?

jvm在加载Holder时不会连SingletonHolder一同加载( lazy loading ),这里涉及到类的初始化时机

  1. 遇到 new 、getstatic 、putstatic 或 invokestatic 这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化 。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及凋用一个类的静态方法的时候。
  2. 使用 java-lang 、 reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化 。
  3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化 。
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类。
  5. 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invokeMethodHandle 实例最后的解析结果 REF-getStatic 、 REF_putStatic 、 REF invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化

外部调用getInstance()方法后,会触发SingletonHolder的加载,任意一个线程获取该【类加载】的锁后,就能保SingletonHolder首次给new出来,只有一个线程进入该内部类。加载完成后,static成员变量是唯一的。


1
2
3
4
5
6
7
8
9
10
11
12
public class Holder {

private Holder (){}

private static class SingletonHolder {
private static final Holder holder = new Holder();
}

public static final Holder getInstance() {
return SingletonHolder.holder;
}
}



懒汉模式

当使用到的时候才去加载这个类,避免了空间的浪费

这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Lazy {

private Lazy(){}

private static Lazy lazy;

public static Lazy getInstance(){
if (lazy == null){
lazy = new Lazy();
}
return lazy;
}
}

上述实现存在一个明显的问题,就是在多线程的情况下不能保证只返回了一个实例,因为getInstance()方法没有加同步锁,可能会造成线程安全的问题

因此我们需要在getInstance()方法上加上同步锁

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Lazy {

private Lazy(){}

private static Lazy lazy;

public static synchronized Lazy getInstance(){
if (lazy == null){
lazy = new Lazy();
}
return lazy;
}
}

在给getInstance()方法加上同步锁之后,这个方法变成线程安全的了,也不会有产生多个实例的情况了。

但是这样做的话我们发现每一次判断lazy实例是否存在的时候就要去拿到同步锁,而判断与线程安全是无关的,因此在方法上加同步锁会大大降低性能。因此我们采取双重锁校验模式,将同步代码块放在方法中,每次先判断实例是否存在,如果存在就直接返回实例;不存在再去拿同步锁进行初始化,这样可以减少线程上下文的切换,提升性能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Lazy {

private Lazy(){}

private static Lazy lazy;

public static Lazy getInstance(){
if (lazy == null) {
synchronized (Lazy.class) {
if (lazy == null) {
lazy = new Lazy();
}
}
}
return lazy;
}
}

上述实现代码,似乎很完美的解决了线程安全问题和性能的问题,但它仍然是线程不安全的。因为初始化代码的new关键字并不是原子性操作

new的操作步骤:

  • 分配内存空间
  • 执行构造方法,初始化对象
  • 把这个对象指向这个空间

由于不是原子性操作,可能会造成指令重排的问题,例如先将这个对象指向这个空间,再去初始化对象

情况:一个线程A执行代码,在new操作时发生了指令重排,导致先将对象指向分配的空间,再去初始化对象;这时线程B进入发现这个对象已经指向了一个空间,它就会认为这个对象!=null,但实际上这个对象可能还未完成初始化对象

所以由于存在这种情况,我们必须禁止指令重排,即加上volatile关键字,这样才算是完整的双重锁校验模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Lazy {

private Lazy(){}

private volatile static Lazy lazy;

public static Lazy getInstance(){
if (lazy == null) {
synchronized (Lazy.class) {
if (lazy == null) {
lazy = new Lazy();
}
}
}
return lazy;
}
}

反射机制破坏单例(以懒汉式为例)

虽然解决了线程安全问题、性能和原子性的问题,但因为存在反射机制,可以人为的去破坏单例模式。所以仍然存在安全问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class TestReflection {
//测试反射
public static void main(String[] args) {
try {
Constructor<Lazy> constructor = Lazy.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
Lazy lazy = null;
lazy = constructor.newInstance();
System.out.println(lazy==Lazy.getInstance()); //false
} catch (Exception e) {
e.printStackTrace();
}
}
}

增加校验

我们可以在造构造器中判断是否存在这个实例来解决问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Lazy {

private Lazy() {
synchronized (Lazy.class){
//当这个实例已经存在了则不能使用反射机制创建对象
if (lazy!=null){
throw new RuntimeException("请勿使用反射机制破坏");
}else {
lazy = this;
}
}
}

private volatile static Lazy lazy;

public static Lazy getInstance() {
if (lazy == null) {
synchronized (Lazy.class) {
if (lazy == null) {
lazy = new Lazy();
}
}
}
return lazy;
}
}

反序列化机制破坏单例(以懒汉式为例)

虽然这样防止了反射破坏单例模式,但是还仍存在安全隐患

反序列化机制能够破坏单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class TestSerializable {
//测试序列化破坏
public static void main(String[] args) throws IOException, ClassNotFoundException {

//Write Obj to file
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
oos.writeObject(Lazy.getInstance());
//Read Obj from file
File file = new File("tempFile");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Lazy newInstance = (Lazy) ois.readObject();

System.out.println(newInstance== Lazy.getInstance()); //false

}

}

防止反序列化破坏

防止反序列化仅需要在代码中添加如下代码

1
2
3
private Object readResolve() {
return lazy;
}

为什么添加这段代码就能够防止反序列化破坏单例呢?

对象的序列化过程通过 ObjectOutputStream 和 ObjectInputputStream 来实现的,而反序列化时会进入到 ObjectInputputStream 的 readOrdinaryObject 方法中

源码:

image-20220328203136918

上述源码中的这一段代码中有两段重要的代码

image-20220328203229857

isInstantiable:如果一个serializable/externalizable的类可以在运行时被实例化,那么该方法就返回true。

desc.newInstance:该方法通过反射的方式查找被序列化的类的第一个没有实现序列化接口的父类,并调用其无参构造方法创建一个对象,所以就算在被序列化的类中的无参构造方法中拦截反射也不能够防止反序列化的破坏!!

image-20220328203920455

hasReadResolveMethod:如果实现了serializable 或者 externalizable接口的类中包含readResolve则返回true

invokeReadResolve:通过反射的方式调用要被反序列化的类的readResolve方法,并把返回值赋值给rep

最后 obj = rep 并返回 obj,而我们写的类中,readResolve方法返回的是唯一的Lazy类的实例,由此得到的对象和通过getInstance()得到的对象是一致的



枚举模式

我们先来看一个枚举的单例模式的实现

1
2
3
4
5
6
public enum  EnumSingleton {
INSTANCE;
public EnumSingleton getInstance(){
return INSTANCE;
}
}

使用无参构造器反射创建

当我们试图使用反射机制去创建这个枚举类时

1
2
3
4
5
6
Constructor<EnumSingleton> constructor= null;
constructor = EnumSingleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
EnumSingleton singleton= null;
singleton = constructor.newInstance();
System.out.println(singleton);

将会报异常:

1
Exception in thread "main" java.lang.NoSuchMethodException: com.lxp.pattern.singleton.EnumSingleton.<init>()

使用debug模式运行,会发现是因为EnumSingleton.class.getDeclaredConstructors()获取所有构造器,会发现并没有我们所设置的无参构造器,但是我们并没有在枚举类中设置其他的有参构造方法,这是怎么回事呢?查看编译后的源码,其继承了Enum类,所以会有(String.class,int.class)的构造器

image-20220321203503408


使用有参构造器反射创建

也许会有疑问,是不是因为自身的类没有无参构造方法才导致的异常?但执行下述代码后仍然会异常

1
2
3
4
5
6
Constructor<EnumSingleton> constructor= null;
constructor = EnumSingleton.class.getDeclaredConstructor(String.class,int.class);
constructor.setAccessible(true);
EnumSingleton singleton= null;
singleton = constructor.newInstance();
System.out.println(singleton);

抛出异常:

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

Cannot reflectively create enum objects 这个异常的意思是不能使用反射机制创建枚举对象


原因

我们通过查看newInstance方法的源码发现,当对枚举类使用newInstance方法时会抛出异常

image-20220321201936962

为了防止反射机制破坏懒汉模式,我们可以使用枚举的方式实现单例模式

枚举是最好的单例模式,同时枚举模式也能保证线程安全