写点什么

单例模式原来是这么简单?!

发布于: 2021 年 02 月 01 日
单例模式原来是这么简单?!

1、写在开头

创建型模式是用来创建对象的模式,抽象了实例化的过程,帮助一个系统独立于其他关联对象的创建、组合和表示方式。

单例模式目的:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

1.1 相关文章

单例模式也是创建型的设计模式之一,本文是设计模式系列(共 24 节)的第 2 篇文章。

设计模式是基于六大设计原则进行的经验总结:《第一节:设计模式的六大原则

创建型设计模式共 5 种:

2、单例模式是什么

单例模式(Singleton Pattern)可以说是整个设计中最简单的模式之一,且这种模式即使在没有看设计模式相关资料也经常在编码开发中。

因为在编程开发中经常会遇到这样一种场景,那就是需要保证一个类只有一个实例哪怕多线程同时访问,并需要提供一个全局访问此实例的点。

综上以及我们平常的开发中,可以总结一条经验,单例模式主要解决的是,一个全局使用的类频繁的创建和消费,从而提升提升整体的代码的性能。

3、普通模式(非线程安全)

普通模式的特点是:不允许外部直接创建,且对象是全局共享。

下面 例子 1 是普通实现方法的单例模式,也是我们最常用的:


/** * 普通写法 */public class SingletonCase1 { private static SingletonCase1 singleton = null; public SingletonCase1() { } /** * 并发下会产生多个实例 */ public static SingletonCase1 getInstance(){ if (singleton == null){ singleton = new SingletonCase1(); } return singleton; }}
复制代码


代码分析:

因此虽然在默认的构造函数上添加了私有属性 private,也确实满足了懒加载,但是如果有多个访问者同时去获取对象实例,你可以想象很多人在抢厕所,就会造成多个同样的实例并存,所以是非线程安全的。

4、饿汉模式(线程安全)

饿汉式单例模式的特点是:类在加载时就直接初始化了实例。即使没用到,也会实例化,因此,它也是线程安全的单例模式。

下面 例子 2 是饿汉模式:


/** * 饿汉式 * 饿汉式的特点是:类在加载时就直接初始化了实例。即使没用到,也会实例化,因此,也是线程安全的单例模式。 */public class SingletonCase3 { /**类在加载的时候直接进行初始化*/ private static SingletonCase3 singleton = new SingletonCase3(); public SingletonCase3() { } /** * 对外暴露唯一接口 * 提供单例对象 */ public static SingletonCase3 getInstance(){ return singleton; }}
复制代码

5、懒汉模式(加锁 &线程安全)

懒汉式单例模式的特点:对比普通模式,给方法加了排它锁,这是线程安全的写法;对比饿汉模式,全局对象只会在用到时才会进行初始化。

下面 例子 3 是懒汉模式:


/** * 懒汉式,对比SingletonCase1,给方法加了排它锁,这是线程安全的写法。 * 用到这个实例时才去调用方法实例化。但是,我们把整个方法都同步了,效率很低下,我们可以继续优化,只在创建实例的地方加上同步 */public class SingletonCase2 {  private static SingletonCase2 singleton = null;  public SingletonCase2() {  }  /**   * 整个方法锁住了,效率较低   * @return   */  public synchronized static SingletonCase2 getInstance(){    if (singleton == null){      singleton = new SingletonCase2();    }    return singleton;  }}
复制代码

6、双重校验模式

双重校验模式的特点:考虑到多线程下的并发操作,全局对象使用了 volatile关键字 修饰,同时在对象初始化时进行加锁防止对象被其他线程重复创建。

此处可以参考《Java 并发编程实战》的 第3章volatile的使用

下面 例子 4 是双重校验模式:



/** * 双重非空判断,new对象前加一次锁。 * volatile关键字,考虑的是,new关键字在虚拟机中执行时其实分为很多步骤,具体原因可以参考深入理解java虚拟机一书(考虑的是这个new关键字字节码执行时是非原子性的),而volatile关键字可以防止指令重排。 */
public class SingletonCase4 {
/**volatile防止指令重排*/ private static volatile SingletonCase4 singleton;
public SingletonCase4() { }
/** * 只是在实例为空时才进行同步创建 * 为什么做了2次判断? * A线程和B线程同时进入同步方法 getInstance * 然后都在1位置处判断了实例为null * 然后都进入了同步块2中 * 然后A线程优先进入了同步代码块2中(B线程也进入了),然后创建了实例 * 此时,如果没有3处的判断,那么A线程创建实例同时,B线程也会创建一个实例 * 所以,还需要做2次判断 * */ public static SingletonCase4 getInstance(){ if (singleton == null){ synchronized (SingletonCase4.class){ if (singleton == null){ singleton = new SingletonCase4(); } } } return singleton; }}
复制代码

7、内部类模式

内部类模式的特点是:由于静态内部类跟外部类是平级的,所以外部类加载的时候不会影响内部类,因此实现了懒加载。

下面 例子 5 是内部类模式:



/** * 内部类 * 优点:由于静态内部类跟外部类是平级的,所以外部类加载的时候不会影响内部类,因此实现了lazy loading, 同时也是利用静态变量的方式, * 使得INSTANCE只会在SingletonHolder加载的时候初始化一次,从而保证不会有多线程初始化的情况,因此也是线程安全的。 */public class SingletonCase5 { //静态内部类,懒加载:在被用到时才加载(根据内部类不会在其外部类被加载的同时被加载的事实) private static class SingletonCase5Holder{ private static final SingletonCase5 singleton = new SingletonCase5(); } public SingletonCase5() { } public static SingletonCase5 getInstance(){ return SingletonCase5Holder.singleton; }}
复制代码

8、枚举类模式

枚举类模式的特点是:创建一个枚举类,封装一个对象,通过枚举类的私有构造器,强化了单例模式,且实现了懒加载,当唯一全局入口被调用才会初始化对象。

此处可以参考《Effective Java》的 第3点 以及 第4点

下面 例子 6 枚举类模式:



/** * 枚举类 * 本质就是:创建一个枚举类,封装一个对象,枚举类私有构造器中初始化对象 */
public class SingletonCase6 { enum SingletonEnum { //懒加载,创建一个枚举对象,该对象天生为单例 INSTANCE; private SingletonCase6 singleton;
//私有化枚举的构造函数(强调不可外部实例化) private SingletonEnum() { singleton = new SingletonCase6(); }
public SingletonCase6 getSingleton() { return singleton; } }
private SingletonCase6() { }
public static SingletonCase6 getInstance(){
getInstance(); }}
复制代码

通过私有化构造器可以强化单例属性


补充知识点:枚举类如何确保单例呢?

  • 我们通过编译 SingletonCase6.java,得到以下编译输出

  • 是的,我们的枚举类其实就是一个静态类,人家在 JVM 初始化时就创建好了。

  • 看下源码


package design_module.SinglePattern;
import java.io.PrintStream;
enum SingletonCase6$SingletonEnum{ INSTANCE; private SingletonCase6 singleton; private SingletonCase6$SingletonEnum() { System.out.println("-----1 init constructor-----"); this.singleton = new SingletonCase6(null); } public SingletonCase6 getSingleton() { System.out.println("-----2 return singleton-----"); return this.singleton; }}
复制代码


9、总结

单例模式应该是最常见的设计模式之一了,它归类在设计模式创建型模式,用于解决对象创建的对象复用问题。

创建型模式还包括:原型模式、工厂模式、抽象工厂模式和建造者模式,待后续我们再细细解读。

10、延伸阅读

《源码系列》

JDK之Object 类

JDK之BigDecimal 类

JDK之String 类

JDK之Lambda表达式


《经典书籍》

Java并发编程实战:第1章 多线程安全性与风险

Java并发编程实战:第2章 影响线程安全性的原子性和加锁机制

Java并发编程实战:第3章 助于线程安全的三剑客:final & volatile & 线程封闭


《服务端技术栈》

《Docker 核心设计理念

《Kafka史上最强原理总结》

《HTTP的前世今生》


《算法系列》

读懂排序算法(一):冒泡&直接插入&选择比较

《读懂排序算法(二):希尔排序算法》

《读懂排序算法(三):堆排序算法》

《读懂排序算法(四):归并算法》

《读懂排序算法(五):快速排序算法》

《读懂排序算法(六):二分查找算法》


《设计模式》

设计模式之六大设计原则

设计模式之创建型(1):单例模式

设计模式之创建型(2):工厂方法模式

设计模式之创建型(3):原型模式

设计模式之创建型(4):建造者模式



发布于: 2021 年 02 月 01 日阅读数: 21
用户头像

Diligence is the mother of success. 2018.03.28 加入

公众号:后台技术汇 笔者主要从事Java后台开发,喜欢技术交流与分享,保持饥渴,一起进步!

评论

发布
暂无评论
单例模式原来是这么简单?!