# 单例模式 (SingleTon)

# 单例模式应用场景

单例模式保证对同一个类只生成一个实例,应用场景主要有以下几个方面

  • 需要频繁创建的类,使用单例可以降低系统的内存压力
  • 某些类创建实例时占用资源较多,或实例化耗时较长,且经常使用
  • 某类只要求生成一个对象的时候,如一个班中的班长,每个人的身份证号
  • 某类需要频繁实例化,而创建的对象又频繁被销毁的时候,如多线程的线程池,网络连接池
  • 频繁访问数据库或文件的对象
  • 对于一些控制硬件级别的操作,或者从系统上来讲应当是单一控制逻辑的操作,如果有多个实例。则系统会完全乱套
  • 当对象需要被共享的场合,由于单例模式只允许创建一个对象,共享该对象可以节省内存,并加快对象访问速度。如 Web 中的配置对象,数据库的连接池等

# 单例模式的结构与实现

单例模式的结构图

# 懒汉式单例

该模式的特点是类加载时没有生成单例,只有当第一次调用 getInstance 方法时采取创建这个单例,代码如下 :

1
2
3
4
5
6
7
8
9
10
public class LazySingleTon {
private static LazySingleTon lazySingleTon;
private LazySingleTon(){};
public static LazySingleTon getInstance() {
if (lazySingleTon == null) { //①
lazySingleTon = new LazySingleTon();
}
return lazySingleTon;
}
}

当在单线程的时候,该方法没有问题,但涉及到多线程的时候就会出问题 :

1

很明显, 多线程返回的实例并不都是同一个

假设第一个线程进入了①, 还没有生成实例时,切换到第二个线程,而此时第二个线程也进入了①, 最终,就会生成两个不同的实例

我们可以为生成实例的方法上锁,让同一时间只能有一个线程执行生成实例的方法

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

这种方法可以保证同一时间只有一个线程运行该方法,但是当线程特别多的时候,除了一个线程只在执行,其它线程都处于堵塞状态,且如果对一个静态方法上锁,则锁上的是整个 class, 频繁的上锁与解锁会拖慢效率

所以我们需要找到一个方法,在最小化上锁区域的同时保证线程安全

1
2
3
4
5
6
7
8
9
10
11
12
13
public class LazySingleTon {
private static volatile LazySingleTon lazySingleTon; //①
private LazySingleTon(){};
public static LazySingleTon getInstance() {
if (lazySingleTon == null) { //②
synchronized (LazySingleTon.class) { //③
if (lazySingleTon == null) //④
lazySingleTon = new LazySingleTon();
}
}
return lazySingleTon;
}
}
  • ① : 这一条最难理解。首先需要明确,上锁之后,并不是只会执行一个线程,而是其他进程遇到锁就阻塞了,等到该线程的时间片结束后才切换线程。也就是说,就算是有一个线程已经进入锁内了,并不是意味着这个线程持续执行到锁内代码结束,也是会正常进行线程调度的

    了解了这一层之后,我们就可以看代码了

    当我们 new 一个类,系统内大体进行了三项操作 :

    • 分配内存
    • 初始化实例 (运行构造方法)
    • 将变量指向内存

    在宏观来看,单线程情况下,编译器改变第二步和第三步的位置 (重排序), 对我们是没有影响的,而多线程就不一样了,如果编译器将第二步和第三步进行了互换,

    第二步执行完:变量指向不为 null, 但该实例还未进行初始化

    如果此时线程切换,其他线程进入锁体,进行②判断,而此时指向已经不为 null 了,所以就直接返回了

    所以我们需要加上 volatile 关键字,来进入编译器重排序

  • ② : 如果不为空的话直接返回,就不用再进锁了,但很多线程几乎同时运行时,还是有很多线程通过了这层判断

  • ③ : 上一层锁,因为被锁上的片段是 static 的,所以上锁的时候是对该类上锁 (所有该类的对象都会受影响,同一时间只允许有一个线程操作这个类或对象)

  • ④ : 这一条要和②结合起来看,很多线程都通过了②的判断,此时就会陷入堵塞,只有一个线程能执行锁内的代码,而当这个线程执行完毕后,阻塞队列中的代码都会一次进入锁中执行,而此时之前的那个线程已经将实例生成了,如果再加一层判断,那么通过②的线程都会各自再生成一个实例

# 饿汉式单例

相比于懒汉式,饿汉式生来线程安全,不过它与懒汉式不同的是,饿汉式的实例在此类被加载到内存的时候就已经生成了

1
2
3
4
5
6
7
8
9
10
public class HungrySingleton {
private static final HungrySingleton instance = new HungrySingleton();

private HungrySingleton() {
}

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

那么饿汉式的实例是什么时候生成的呢

img

可见 在步骤1时就实例化了

img