26讲单例模式:如何创建单一对象优化系统性能 - 图126讲单例模式:如何创建单⼀对象优化系统性能

你好,我是刘超。

从这⼀讲开始,我们将⼀起探讨设计模式的性能调优。在《Design Patterns: Elements of Reusable Object-Oriented
26讲单例模式:如何创建单一对象优化系统性能 - 图2Software》⼀书中,有23种设计模式的描述,其中,单例设计模式是最常⽤的设计模式之⼀。⽆论是在开源框架,还是在我们的⽇常开发中,单例模式⼏乎⽆处不在。

什么是单例模式?

它的核⼼在于,单例模式可以保证⼀个类仅创建⼀个实例,并提供⼀个访问它的全局访问点。

该模式有三个基本要点:⼀是这个类只能有⼀个实例;⼆是它必须⾃⾏创建这个实例;三是它必须⾃⾏向整个系统提供这个实例。

结合这三点,我们来实现⼀个简单的单例:

//饿汉模式
public final class Singleton {
private static Singleton instance=new Singleton();//⾃⾏创建实例private Singleton(){}//构造函数
public static Singleton getInstance(){//通过该函数向整个系统提供实例return instance;
}
}
由于在⼀个系统中,⼀个类经常会被使⽤在不同的地⽅,通过单例模式,我们可以避免多次创建多个实例,从⽽节约系统资源。

饿汉模式

我们可以发现,以上第⼀种实现单例的代码中,使⽤了static修饰了成员变量instance,所以该变量会在类初始化的过程中被收集进类构造器即⽅法中。在多线程场景下,JVM会保证只有⼀个线程能执⾏该类的⽅法,其它线程将会被阻塞 等待。

等到唯⼀的⼀次⽅法执⾏完成,其它线程将不会再执⾏⽅法,转⽽执⾏⾃⼰的代码。也就是说,static修饰了成员变量instance,在多线程的情况下能保证只实例化⼀次。

这种⽅式实现的单例模式,在类加载阶段就已经在堆内存中开辟了⼀块内存,⽤于存放实例化对象,所以也称为饿汉模式。饿汉模式实现的单例的优点是,可以保证多线程情况下实例的唯⼀性,⽽且getInstance直接返回唯⼀实例,性能⾮常⾼。
然⽽,在类成员变量⽐较多,或变量⽐较⼤的情况下,这种模式可能会在没有使⽤类对象的情况下,⼀直占⽤堆内存。试想下,如果⼀个第三⽅开源框架中的类都是基于饿汉模式实现的单例,这将会初始化所有单例类,⽆疑是灾难性的。

懒汉模式

懒汉模式就是为了避免直接加载类对象时提前创建对象的⼀种单例设计模式。该模式使⽤懒加载⽅式,只有当系统使⽤到类对象时,才会将实例加载到堆内存中。通过以下代码,我们可以简单地了解下懒加载的实现⽅式:

//懒汉模式
public final class Singleton {
private static Singleton instance= null;//不实例化private Singleton(){}//构造函数
public static Singleton getInstance(){//通过该函数向整个系统提供实例if(null == instance){//当instance为null时,则实例化对象,否则直接返回对象
instance = new Singleton();//实例化对象
}
return instance;//返回已存在的对象
}
}
以上代码在单线程下运⾏是没有问题的,但要运⾏在多线程下,就会出现实例化多个类对象的情况。这是怎么回事呢?

当线程A进⼊到if判断条件后,开始实例化对象,此时instance依然为null;⼜有线程B进⼊到if判断条件中,之后也会通过条件判断,进⼊到⽅法⾥⾯创建⼀个实例对象。

所以我们需要对该⽅法进⾏加锁,保证多线程情况下仅创建⼀个实例。这⾥我们使⽤Synchronized同步锁来修饰getInstance
⽅法:

//懒汉模式 + synchronized同步锁public final class Singleton {
private static Singleton instance= null;//不实例化
private Singleton(){}//构造函数
public static synchronized Singleton getInstance(){//加同步锁,通过该函数向整个系统提供实例if(null == instance){//当instance为null时,则实例化对象,否则直接返回对象
instance = new Singleton();//实例化对象
}
return instance;//返回已存在的对象
}
}

但我们前⾯讲过,同步锁会增加锁竞争,带来系统性能开销,从⽽导致系统性能下降,因此这种⽅式也会降低单例模式的性
能。

还有,每次请求获取类对象时,都会通过getInstance()⽅法获取,除了第⼀次为null,其它每次请求基本都是不为null的。在没有加同步锁之前,是因为if判断条件为null时,才导致创建了多个实例。基于以上两点,我们可以考虑将同步锁放在if条件⾥
⾯,这样就可以减少同步锁资源竞争。

//懒汉模式 + synchronized同步锁public final class Singleton {
private static Singleton instance= null;//不实例化
private Singleton(){}//构造函数
public static Singleton getInstance(){//加同步锁,通过该函数向整个系统提供实例if(null == instance){//当instance为null时,则实例化对象,否则直接返回对象
synchronized (Singleton.class){
instance = new Singleton();//实例化对象
}
}
return instance;//返回已存在的对象
}
}
看到这⾥,你是不是觉得这样就可以了呢?答案是依然会创建多个实例。这是因为当多个线程进⼊到if判断条件⾥,虽然有同 步锁,但是进⼊到判断条件⾥⾯的线程依然会依次获取到锁创建对象,然后再释放同步锁。所以我们还需要在同步锁⾥⾯再加
⼀个判断条件:

//懒汉模式 + synchronized同步锁 + double-check public final class Singleton {
private static Singleton instance= null;//不实例化
private Singleton(){}//构造函数
public static Singleton getInstance(){//加同步锁,通过该函数向整个系统提供实例
if(null == instance){//第⼀次判断,当instance为null时,则实例化对象,否则直接返回对象synchronized (Singleton.class){//同步锁
if(null == instance){//第⼆次判断
instance = new Singleton();//实例化对象
}
}
}
return instance;//返回已存在的对象
}
}

以上这种⽅式,通常被称为Double-Check,它可以⼤⼤提⾼⽀持多线程的懒汉模式的运⾏性能。那这样做是不是就能保证万
⽆⼀失了呢?还会有什么问题吗?

其实这⾥⼜跟Happens-Before规则和重排序扯上关系了,这⾥我们先来简单了解下Happens-Before规则和重排序。

我们在第⼆期加餐中分享过,编译器为了尽可能地减少寄存器的读取、存储次数,会充分复⽤寄存器的存储值,⽐如以下代码,如果没有进⾏重排序优化,正常的执⾏顺序是步骤1\2\3,⽽在编译期间进⾏了重排序优化之后,执⾏的步骤有可能就变成了步骤1/3/2,这样就能减少⼀次寄存器的存取次数。

int a = 1;//步骤1:加载a变量的内存地址到寄存器中,加载1到寄存器中,CPU通过mov指令把1写⼊到寄存器指定的内存中int b = 2;//步骤2 加载b变量的内存地址到寄存器中,加载2到寄存器中,CPU通过mov指令把2写⼊到寄存器指定的内存中
a = a + 1;//步骤3 重新加载a变量的内存地址到寄存器中,加载1到寄存器中,CPU通过mov指令把1写⼊到寄存器指定的内存中
在 JMM 中,重排序是⼗分重要的⼀环,特别是在并发编程中。如果JVM可以对它们进⾏任意排序以提⾼程序性能,也可能会给并发编程带来⼀系列的问题。例如,我上⾯讲到的Double-Check的单例问题,假设类中有其它的属性也需要实例化,这个时候,除了要实例化单例类本身,还需要对其它属性也进⾏实例化:

//懒汉模式 + synchronized同步锁 + double-check public final class Singleton {
private static Singleton instance= null;//不实例化
public List list = null;//list属性private Singleton(){
list = new ArrayList();
}//构造函数
public static Singleton getInstance(){//加同步锁,通过该函数向整个系统提供实例
if(null == instance){//第⼀次判断,当instance为null时,则实例化对象,否则直接返回对象synchronized (Singleton.class){//同步锁
if(null == instance){//第⼆次判断
instance = new Singleton();//实例化对象
}
}
}
return instance;//返回已存在的对象
}
}

在执⾏instance = new Singleton();代码时,正常情况下,实例过程这样的:

给 Singleton 分配内存;
调⽤ Singleton 的构造函数来初始化成员变量;
将 Singleton 对象指向分配的内存空间(执⾏完这步 singleton 就为⾮ null 了)。

如果虚拟机发⽣了重排序优化,这个时候步骤3可能发⽣在步骤2之前。如果初始化线程刚好完成步骤3,⽽步骤2没有进⾏时,则刚好有另⼀个线程到了第⼀次判断,这个时候判断为⾮null,并返回对象使⽤,这个时候实际没有完成其它属性的构造,因此使⽤这个属性就很可能会导致异常。在这⾥,Synchronized只能保证可⻅性、原⼦性,⽆法保证执⾏的顺序。

这个时候,就体现出Happens-Before规则的重要性了。通过字⾯意思,你可能会误以为是前⼀个操作发⽣在后⼀个操作之前。然⽽真正的意思是,前⼀个操作的结果可以被后续的操作获取。这条规则规范了编译器对程序的重排序优化。

我们知道volatile关键字可以保证线程间变量的可⻅性,简单地说就是当线程A对变量X进⾏修改后,在线程A后⾯执⾏的其它线程就能看到变量X的变动。除此之外,volatile在JDK1.5之后还有⼀个作⽤就是阻⽌局部重排序的发⽣,也就是说,volatile 变量的操作指令都不会被重排序。所以使⽤volatile修饰instance之后,Double-Check懒汉单例模式就万⽆⼀失了。

//懒汉模式 + synchronized同步锁 + double-check public final class Singleton {
private volatile static Singleton instance= null;//不实例化
public List list = null;//list属性private Singleton(){
list = new ArrayList();
}//构造函数
public static Singleton getInstance(){//加同步锁,通过该函数向整个系统提供实例
if(null == instance){//第⼀次判断,当instance为null时,则实例化对象,否则直接返回对象synchronized (Singleton.class){//同步锁
if(null == instance){//第⼆次判断
instance = new Singleton();//实例化对象
}
}
}
return instance;//返回已存在的对象
}
}

通过内部类实现

以上这种同步锁+Double-Check的实现⽅式相对来说,复杂且加了同步锁,那有没有稍微简单⼀点⼉的可以实现线程安全的懒加载⽅式呢?

我们知道,在饿汉模式中,我们使⽤了static修饰了成员变量instance,所以该变量会在类初始化的过程中被收集进类构造器即
⽅法中。在多线程场景下,JVM会保证只有⼀个线程能执⾏该类的⽅法,其它线程将会被阻塞等待。这种⽅式可以保证内存的可⻅性、顺序性以及原⼦性。

如果我们在Singleton类中创建⼀个内部类来实现成员变量的初始化,则可以避免多线程下重复创建对象的情况发⽣。这种⽅ 式,只有在第⼀次调⽤getInstance()⽅法时,才会加载InnerSingleton类,⽽只有在加载InnerSingleton类之后,才会实例化创建对象。具体实现如下:

//懒汉模式 内部类实现
public final class Singleton {
public List list = null;// list属性

private Singleton() {//构造函数list = new ArrayList();
}

// 内部类实现
public static class InnerSingleton {
private static Singleton instance=new Singleton();//⾃⾏创建实例
}

public static Singleton getInstance() {
return InnerSingleton.instance;// 返回内部类中的静态变量
}
}

总结

单例的实现⽅式其实有很多,但总结起来就两种:饿汉模式和懒汉模式,我们可以根据⾃⼰的需求来做选择。

如果我们在程序启动后,⼀定会加载到类,那么⽤饿汉模式实现的单例简单⼜实⽤;如果我们是写⼀些⼯具类,则优先考虑使
⽤懒汉模式,因为很多项⽬可能会引⽤到jar包,但未必会使⽤到这个⼯具类,懒汉模式实现的单例可以避免提前被加载到内存中,占⽤系统资源。

思考题

除了以上那些实现单例的⽅式,你还知道其它实现⽅式吗?

期待在留⾔区看到你的答案。也欢迎你点击“请朋友读”,把今天的内容分享给身边的朋友,邀请他⼀起讨论。

26讲单例模式:如何创建单一对象优化系统性能 - 图3

  1. 精选留⾔ <br />![](https://cdn.nlark.com/yuque/0/2022/png/1852637/1646315782500-8a157a18-5d31-4aa6-92d0-d9a3e5724f8f.png#)⾖泥丸<br />最安全的枚举模式,反射和序列化都是单例。<br />2019-07-23 06:11<br />作者回复<br />对的,我们之前序列化优化这⼀讲中的问答题就是与枚举实现单例相关,《Effective Java》作者也是强烈推荐枚举⽅式实现单例。<br />2019-07-24 09:15

26讲单例模式:如何创建单一对象优化系统性能 - 图4Loubobooo
使⽤枚举来实现单例模式,具体代码如下:public class SinletonExample5 { private static SinletonExample5 instance = null;

// 私有构造函数
private SinletonExample5(){
}

public static SinletonExample5 getInstance(){ return Sinleton.SINLETON.getInstance();
}

private enum Sinleton{ SINLETON;

private SinletonExample5 singleton;

// JVM保证这个⽅法只调⽤⼀次Sinleton(){
singleton = new SinletonExample5();
}

public SinletonExample5 getInstance(){ return singleton;
}
}
}
2019-07-23 11:09
作者回复
很赞!这是⼀种懒加载模式的枚举实现。
2019-07-24 09:30

26讲单例模式:如何创建单一对象优化系统性能 - 图5⾏者
枚举也是⼀种单例模式,同时是饿汉式。
相⽐Double Check,以内部类⽅式实现单例模式,代码简洁,性能相近,在我看来是更优的选择。
2019-07-23 12:44
作者回复
也可以使⽤枚举实现懒汉模式,可以根据本讲中的使⽤内部类⽅式实现懒加载。
2019-07-24 09:33

26讲单例模式:如何创建单一对象优化系统性能 - 图6-W.LI-
枚举底层实现就是静态内部类吧
2019-07-23 08:25
作者回复
对的,枚举是⼀种语法糖,在Java编译后,枚举类中的枚举会被声明为static,接下来就跟我们⽂中讲的⼀样了。
2019-07-24 09:28

26讲单例模式:如何创建单一对象优化系统性能 - 图7我⼜不乱来
枚举天⽣就是单例,但是不清楚这么实现。
注册式单例,spring应该是⽤的这种。这个也不太清楚,超哥有机会讲⼀下spring的实现⽅式和枚举⽅式实现的单例。谢谢

2019-07-23 07:14
作者回复
Spring中的bean的单例虽然是⼀种单例效果,但实现⽅式是通过容器缓存实现,严格来说是⼀种享元模式。
2019-07-24 09:30

26讲单例模式:如何创建单一对象优化系统性能 - 图8我 知 道 了 嗯 枚举实现单例
2019-07-23 06:35
作者回复
对的
2019-07-24 09:23

26讲单例模式:如何创建单一对象优化系统性能 - 图9QQ怪
这⼀节虽然都懂,但是评论区补充的我还是第⼀次⻅到,get到了,有收获,哈哈
2019-07-23 20:32
作者回复
互相学习,共同进步
2019-07-24 09:35

26讲单例模式:如何创建单一对象优化系统性能 - 图10WL
⽼师请问您讲的单例模式三个基本要点, 但是我使⽤spring的框架并⾥⾯默认的类不都是单例的吗,但是并没有满⾜您说的要点,
⽤tomcat也是, tomcat⾥⾯的servlet应该也是单例的吧, 好像也没有满⾜您说的三个要点, 请问spring和tomcat是咋实现的, 是数据懒汉还是饿汉的实现⽅式.
2019-07-24 07:22
作者回复
Spring是通过容器管理来实现单例的,也是基于这三个基本点。
2019-07-24 09:36

26讲单例模式:如何创建单一对象优化系统性能 - 图11 colin
26讲单例模式:如何创建单一对象优化系统性能 - 图12 看评论涨知识
2019-07-24 06:38

26讲单例模式:如何创建单一对象优化系统性能 - 图13Jxin
1.可能⼤部分同学都知道,但为了少部分同学,我在⽼师这个单例上补个点。其它线程空指针异常确实是指令重排导致的,但其原因还有⼀个。加锁并不能阻⽌cpu调度线程执⾏体,所以时间⽚还是会切的(假设单核),所以其他线程依旧会执⾏锁外层的if(),并发情况下就可能拿到仅赋值引⽤,未在内存空间存储数据的实例(null实例),进⽽空指针。
2.给⽼师的代码补段骚的:
// 懒汉模式 + synchronized 同步锁 + double-check public final class Singleton {
private static validate Singleton instance = null;// 不实例化
public List list;//list 属性
private Singleton(){
list = new ArrayList();
}// 构造函数
public static Singleton getInstance(){// 加同步锁,通过该函数向整个系统提供实例
Singleton temp = instance;
if(null == temp){// 第⼀次判断,当 instance 为 null 时,则实例化对象,否则直接返回对象
synchronized (Singleton.class){// 同步锁
temp = instance;
if(null == temp){// 第⼆次判断
temp = new Singleton();// 实例化对象
instance = temp;
}
}
}
return instance;// 返回已存在的对象
}
}
⽤临时变量做⽅法内数据承载(相对于validate修饰的属性,可以减少从内存直接拷⻉数据的次数),最后⽤instance接收临时变量时,因为是validate修饰,所以也不会有指令重排。所以前⾯临时变量的赋值操作已经完成,这样instance就必然是赋值好的实例。(如有错误请⽼师指出,仅个⼈理解的骚操作)

3.极限编程试试就好,业务代码还是尽量优先保证可读性,只有在有性能需求时再采⽤影响可读性的性能优化。我的这种骚写法和⽼师的内部类,这种看起来需要想那么⼀下的东⻄尽量避免,简单才是王道。
2019-07-23 23:48
作者回复
虽然有点绕,还是值得表扬的。我们还是⿎励简单易懂的编程⻛格。
2019-07-24 09:52

26讲单例模式:如何创建单一对象优化系统性能 - 图14码德纽@宝
枚举模式,之外还有CAS⽅式单例模式
2019-07-23 17:24

26讲单例模式:如何创建单一对象优化系统性能 - 图15Zed
容器类管理

class InstanceManager {
private static Map objectMap = new HashMap<>(); private InstanceManager(){}
public static void registerService(String key,Object instance){ if (!objectMap.containsKey(key)){ objectMap.put(key,instance);
}
}
public static Object getService(String key){ return objectMap.get(key);
}
}
2019-07-23 15:33
作者回复
Spring中bean的单例就是使⽤容器来实现的,便于管理。
2019-07-24 09:34

26讲单例模式:如何创建单一对象优化系统性能 - 图16OMT
看过其他⽂章有分析单例反射和序列化问题。枚举单例可以反编译查看。
2019-07-23 11:24
作者回复
对的,在第9讲中,我们的问答题也是关于枚举实现单例来解决序列化问题。
2019-07-24 09:31

26讲单例模式:如何创建单一对象优化系统性能 - 图17计科⼀班枚举单例
2019-07-23 09:06

26讲单例模式:如何创建单一对象优化系统性能 - 图18nightmare 666
2019-07-23 08:44