来源:mikechen的互联网架构
一位小伙伴前几天参加美团一面,被问到了这道题目:会单例模式吗,用静态内部类写个单例。
在 Java 中,单例模式是开发时应用最多的设计模式。例如,常见的 Spring 默认创建的 bean ,就是单例模式。
在大厂面试中,单例模式也是高频常考。通过这个题目,可以考察很多知识点。例如:单例模式的线程安全问题,枚举的实现、类加载机制、synchronized 的原理.....。
由于单例模式没回答好,小伙子最终败倒美团一面,错失了大厂机会。
详细面试过程就不细说了,更重要的是,咱不能在同一个地方跌倒两次。
我们今天就来介绍单例模式的 8 种实现方式,看完本文,面试再被问到单例模式的实现,就能给到面试官满意的答案了。
本文目录:
单例模式的定义
单例模式的使用原因
单例模式的 8 种实现方式
单例模式的 4 大应用场景
单例模式的优点、缺点
单例模式的选型参考思路
总结

大家好,我是mikechen,本文是设计模式系列篇之一。为了方便大家学习,我已将本文内容归纳到《深入浅出设计模式》PDF,一共约 2+万字,81页,图文并茂非常详细。
夯实基础、复习备面都用得上,需要的同学文末自取。

单例模式(Singleton Pattern),又称为单子模式,属于创建型模式。
单例模式是一种常见的设计模式,确保一个类只有一个实例,并提供一个全局访问点以获取该实例。

单例模式的特点:
单例类只能有一个实例。
单例类必须自己创建自己的唯一实例。
单例类必须给所有其他对象提供这一实例。
单例模式:
保证了全局对象的唯一性。在整个应用中有、且只有一个实例,并提供一个全局访问点,以获取该实例。
避免了因为创建了多个实例造成资源的浪费,且多个实例由于多次调用、容易导致结果出现错误。
示例:
public class Printer {private static Printer printer =null;//创建一个私有的全局变量/** 如果有多线程并发访问时,上锁,让其排队等候,一次只能一人用。*/public static synchronized Printer getPrinter(){if(printer==null){//如果为空,创建本实例printer = new Printer();}return printer;}/** 构造私有化,保证在系统的使用中,只有一个实例*/private Printer(){}}
使用单例模式:
单例模式向外提供了一个可被访问的实例化的对象。如果没有该对象,printer 类创建一个。
如果遇到多线程并发访问,加上关键字 Synchronized 上锁,让没有持有该对象的类处于等待状态。
当前持有该 printer 的线程任务结束之后,处于等待中的线程才能逐个去持有该实例,去操作其方法。
通过单例模式,让多线程处于等待的状态,一个一个去解决,节约了内存,也提高了运行的效率。
不使用单例模式:
如果多线程访问,printer 就会给要请求的类,分别在内存中 new 出一个 printer 对象,让这些请求的类去做 print 方法。
这样会占用大量内存,导致系统运行变慢。
常见的单例模式实现方式有 8 种。
我们先一览全貌,然后再逐一了解。

1. 饿汉模式

1.1 饿汉式:静态常量
实现最简单、线程安全。
实例在类加载时就被创建,避免了线程同步问题。
但是,实例在类加载时就被创建,没有达到 Lazy Loading 的效果。
如果不需要、且从未使用过该实例,就会浪费内存。
public class Singleton {private static final Singleton instance = new Singleton();private Singleton() { }public static Singleton getInstance() {return instance;}}
1.2 饿汉模式:静态代码块
和第 1 种方式的主要区别,是在类装载时,就执行静态代码块中的代码,初始化类的实例。
public class Singleton {private static Singleton instance;static {instance = new Singleton( );}private Singleton() { }public static Singleton getInstance() {return inatance;}}
2. 懒汉模式

2.1 懒汉模式
线程不安全,在多线程环境中不能使用该方式,只能在单线程环境中使用。
如果在多线程环境中,一个线程进入了 if (singleton == null) 判断语句块,还没往下执行,另一个线程也通过了这个判断语句,这时就会产生多个实例。
public class Singleton {private static Singleton singleton;private singleton () { }public static Singleton getInstance () {if (singleton == null ) {singleton = new Singleton ( ) ;}return singleton;}}
2.2 懒汉模式:同步方法
线程安全,但方法进行同步的效率极低。
虽说做个线程同步,能解决第 3 种方法中的线程不安全问题。
但是,每个线程需要获得类的实例时,执行 getInstance() 方法都需要进行同步,效率极低。
public class Singleton {private static Singleton instance;private Singleton() { }public static synchronized Singleton getInstance() {if (instance == null) {instance = new Singleton();}return instance;}}
2.3 懒汉模式:同步代码块
线程安全,但没有起到线程同步的作用,可能产生多个实例。
例如:
一个线程进入了 if (singleton == null) 判断语句块,还没往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。
public class Singleton {private static Singleton singleton;private Singleton ( ) { }public static Singleton getInstance( ) {if (singleton == null ) {synchronized (Singleton. class) {singleton = new Singleton( ) ;}return singleton;}}
3. 双重检查锁(Double-Checked Locking)

线程安全、延迟加载、效率较高。
使用 volatile 关键字,来保证底层指令执行顺序。
入口处判断 null,可以省去每次加锁的耗费,提升性能。
但由于第一次加载反应稍慢,以及 Java 内存模型的原因,偶尔还是会失败,在高并发环境下存在一定的缺陷。
/*** Double Check Lool (DCL)实现单例** @author mikechen*/public class SingletonDCL {private volatile static SingletonDCL instance = null;//构造方法私有private SingletonDCL() {}public static SingletonDCL getInstance() {//进行两次非空判断 ,第一层是为了避免不必要的同步if (instance == null) {//获取Singleton3.class的锁,避免实例化多次synchronized (SingletonDCL.class) {if (instance == null) {instance = new SingletonDCL();}}}return instance;}}
4. 静态内部类

避免了线程不安全,延迟加载,效率高。
在类内部有一个静态内部类,由于静态内部类的静态属性,仅在第一次加载类时初始化。
确保线程安全,也能够保证单例对象的唯一性,同时也延迟了单例的实例化。
缺点是无法传参,在类进行初始化时,其它线程无法进入。
/*** 静态内部类实现单例模式** @author mikechen*/public class SingletonStatic {private SingletonStatic() {}public static SingletonStatic getInstance() {return SingLineHolder.instance;}private static class SingLineHolder {private static final SingletonStatic instance = new SingletonStatic();}}
静态内部类与饿汉模式的相同之处:
都采用了类装载的机制,来保证初始化实例时只有一个线程。
静态内部类与饿汉模式的不同之处:
饿汉式:只要 Singleton 类被装载就会实例化,没有 Lazy-Loading 的作用。
静态内部类:在 Singleton 类被装载时,并不会立即实例化。而是在需要实例化时,调用 getInstance 方法,才会装载SingletonInstance 类,从而完成 Singleton 的实例化。
5. 枚举

多线程安全,写法非常简洁,自动支持序列化机制,绝对防止多次实例化。
缺点是不能通过 reflection attack 来调用私有构造方法。
/*** 枚举实现单例模式** @author mikechen*/public enum SingletonEnum {INSTANCE;SingletonEnum() {}public void getName() {}}
单例模式通常用于想要控制实例数目、节省系统资源的场景中。
一些常见的应用场景:
工具类对象;
频繁访问数据库或文件的对象;
需要频繁的进行创建和销毁的对象;
创建对象时耗时过多、或耗费资源过多,但又频繁要用到的对象。

单例模式的优点:
避免对资源的多重占用;
简化对象管理,提供全局访问点,方便在整个应用程序中共享实例;
提供了对唯一实例的受控访问。
单例模式的缺点:
由于单例模式中没有抽象层,因此单例类的扩展有很大的困难;
不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态;
单例类职责过重,在一定程度上违背了单一职责。
单例模式的选型参考:
枚举方式通常被认为是最佳实践,因为它既简洁、又线程安全;
一般情况下使用饿汉式,不使用懒汉式、懒汉式(同步方法);
只有在要明确实现 lazy loading 效果时,才会使用静态内部类方式;
如果有其他特殊的需求,可以考虑使用双重检查锁。
以上,只是提供一些参考思路。
大家在实际应用时,要结合每种实现方式的特点、具体使用场景、以及实际需求合理选型。
以上,就是单例模式的全部详解。
通过本文,我们了解并掌握了单例模式的核心知识点,包括:单例模式的 8 种实现方式、4 大应用场景、以及单例模式的选型参考思路等。
在 Java 中,单例模式是开发时应用最多的设计模式。在大厂面试中,单例模式也是高频常考,非常重要。