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

# 懒汉式单例
该模式的特点是类加载时没有生成单例,只有当第一次调用 getInstance 方法时采取创建这个单例,代码如下 :
1 | public class LazySingleTon { |
当在单线程的时候,该方法没有问题,但涉及到多线程的时候就会出问题 :

假设第一个线程进入了①, 还没有生成实例时,切换到第二个线程,而此时第二个线程也进入了①, 最终,就会生成两个不同的实例
我们可以为生成实例的方法上锁,让同一时间只能有一个线程执行生成实例的方法
1 | public class LazySingleTon { |
这种方法可以保证同一时间只有一个线程运行该方法,但是当线程特别多的时候,除了一个线程只在执行,其它线程都处于堵塞状态,且如果对一个静态方法上锁,则锁上的是整个 class, 频繁的上锁与解锁会拖慢效率
所以我们需要找到一个方法,在最小化上锁区域的同时保证线程安全
1 | public class LazySingleTon { |
① : 这一条最难理解。首先需要明确,上锁之后,并不是只会执行一个线程,而是其他进程遇到锁就阻塞了,等到该线程的时间片结束后才切换线程。也就是说,就算是有一个线程已经进入锁内了,并不是意味着这个线程持续执行到锁内代码结束,也是会正常进行线程调度的
了解了这一层之后,我们就可以看代码了
当我们 new 一个类,系统内大体进行了三项操作 :
- 分配内存
- 初始化实例 (运行构造方法)
- 将变量指向内存
在宏观来看,单线程情况下,编译器改变第二步和第三步的位置 (重排序), 对我们是没有影响的,而多线程就不一样了,如果编译器将第二步和第三步进行了互换,
第二步执行完:变量指向不为 null, 但该实例还未进行初始化
如果此时线程切换,其他线程进入锁体,进行②判断,而此时指向已经不为 null 了,所以就直接返回了
所以我们需要加上
volatile关键字,来进入编译器重排序② : 如果不为空的话直接返回,就不用再进锁了,但很多线程几乎同时运行时,还是有很多线程通过了这层判断
③ : 上一层锁,因为被锁上的片段是 static 的,所以上锁的时候是对该类上锁 (所有该类的对象都会受影响,同一时间只允许有一个线程操作这个类或对象)
④ : 这一条要和②结合起来看,很多线程都通过了②的判断,此时就会陷入堵塞,只有一个线程能执行锁内的代码,而当这个线程执行完毕后,阻塞队列中的代码都会一次进入锁中执行,而此时之前的那个线程已经将实例生成了,如果再加一层判断,那么通过②的线程都会各自再生成一个实例
# 饿汉式单例
相比于懒汉式,饿汉式生来线程安全,不过它与懒汉式不同的是,饿汉式的实例在此类被加载到内存的时候就已经生成了
1 | public class HungrySingleton { |
那么饿汉式的实例是什么时候生成的呢


