Zer0e's Blog

设计模式之单例模式

字数统计: 1.4k阅读时长: 5 min
2020/08/07 Share

前言

今天就来谈谈常见的设计模式——单例模式。

正文

概念

单例模式属于创建型模式,这个设计模式涉及到一个单一的类,这个类负责创建自己的对象,又确保只有单个对象被创建,提供了一个全局唯一的实例化对象。它的目的是为了控制实例的数目,又或者是实例是单一的情况下,例如一个设备同时只能有一个对象进行操作。
而实现单例模式的关键代码就是将构造函数设为私有,不允许外部创建实例。

设计

饿汉式

实现的方法很简单,我直接给出代码:

1
2
3
4
5
6
7
8
9
10
public class SingleDemo {
// 饿汉式
private static SingleDemo instance = new SingleDemo();
// 构造函数设为私有
private SingleDemo(){}

public static SingleDemo getInstance(){
return instance;
}
}

这里需要注意的是,我们初始化instance是就直接new SingleDemo(),则此时在getInstance就无需判空处理,这种写法被称为饿汉模式。由于装载类时就已经初始化了instance对象,所以这种方法是线程安全的。

懒汉式

懒汉式则是初始将对象设置为null,等有需要时在进行new操作。饿汉式和懒汉式十分形象,饿汉主动找食物,而懒汉等着别人给。

1
2
3
4
5
6
7
8
9
10
11
12
public class SingleDemo {
private static SingleDemo instance = null;
private SingleDemo(){}

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

}

懒汉式线程安全

以上的懒汉式并不能保证线程安全,当两个线程同时判断instance == null时,同时通过了判断,导致new出了不同的对象。我们可以通过synchronized关键字锁住getInstance方法保证单例。

1
2
3
4
5
6
7
8
9
10
11
public class SingleDemo {
private static SingleDemo instance = null;
private SingleDemo(){}

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

但这种方法会较为影响效率,由于在静态方法上使用synchronized关键字,所以同时只有一个线程能够进入方法内部,导致效率问题。

双重校验锁(DCL)

先上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class SingleDemo {
private static SingleDemo instance = null;
private SingleDemo(){}

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

这个方法就是为了解决上面的效率问题。之所以使用双重检测,是因为多线程情况下,由于方法并不是synchronized修饰的,会导致多个线程同时进入方法进行第一次判空,如果此时instance还未初始化,才会进入临界区域,锁住临界区后,再次进行判空,因为此时前一个线程有可能已经完成了对象创建,而后一个线程才刚刚好进入临界区,所以需要二次判空。
之所以效率比上一个方法高,是因为只有初始化对象时才会进行对象的创建,创建对象的时候才会出现同步的问题,其他情况下直接获取instance就可以了,所以不锁住方法的效率更高一些。

DCL改进

上面的DCL并不是完整的线程安全,原因是JVM在编译时有可能进行指令地重排。举个例子:
例如instance = new SingleDemo(),这一行代码在JVM中可能被分为三个步骤:

  1. 分配对象的内存
  2. 初始化对象
  3. 令instance指向刚才分配的内存

但这个步骤并不是一成不变的,JVM或者CPU会通过优化,改变成以下顺序:

  1. 分配内存
  2. 令instance指向刚才分配的内存
  3. 初始化对象

所以当A线程做完如上1,2步时,instance已经指向了刚才分配的内存了,但并没有初始化,而如果此时B线程才刚开始进行第一次判空时,由于instance已经不是null了,所以B线程直接返回了一个还未初始化的对象。
基于以上原因,我们需要使用volatile修饰instance对象,阻止指令重排,保证instance不会出现中间态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class SingleDemo {
private volatile static SingleDemo instance = null;
private SingleDemo(){}

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

静态内部类

由于从外部无法访问静态内部类,所以只有在getInstance方法才能获得instance对象。
而静态内部类的加载并不是在外部类被加载时被加载,而是当需要使用时才进行加载,来保证懒加载的机制,由于使用的classloader的机制,所以是线程安全的。

1
2
3
4
5
6
7
8
9
10
public class SingleDemo {
private static class SingleHolder{
private static final SingleDemo INSTANCE = new SingleDemo();
}
private SingleDemo(){}

public static final SingleDemo getInstance(){
return SingleHolder.INSTANCE;
}
}

枚举

以上的方法虽然是私有的构造方法,但是依旧可以使用反射的方法来创建多个实例,关于反射,详见这篇文章
而有没有什么方法来限制反射呢?答案就是枚举,代码很简单:

1
2
3
public enum SingletonEnum {
INSTANCE;
}

当我们使用反射时获取对象时,会抛出NoSuchMethodException,阻止了反射。
而这种方法可以防止被多次实例化,是线程安全的,并且支持序列化机制,保证反序列化后返回的都是同一个对象,唯一缺点就是并非懒加载。
这种方法实际中使用较少。

总结

今天整理了几种单例模式,对单例模式重新复习了一下,在项目中,最常用的应该就是饿汉模式,其他模式用的较少,需要反序列化则可以使用枚举,或者实现readResolve()方法。
以上,愿好。

CATALOG
  1. 1. 前言
  2. 2. 正文
    1. 2.1. 概念
    2. 2.2. 设计
      1. 2.2.1. 饿汉式
    3. 2.3. 懒汉式
    4. 2.4. 懒汉式线程安全
    5. 2.5. 双重校验锁(DCL)
    6. 2.6. DCL改进
    7. 2.7. 静态内部类
    8. 2.8. 枚举
  3. 3. 总结