美团一面:会单例模式吗,写个单例看看?(8大单例模式实现方式总结)

来源:mikechen的互联网架构

一位小伙伴前几天参加美团一面,被问到了这道题目:会单例模式吗,用静态内部类写个单例。

在 Java 中,单例模式是开发时应用最多的设计模式。例如,常见的 Spring 默认创建的 bean ,就是单例模式。

在大厂面试中,单例模式也是高频常考。通过这个题目,可以考察很多知识点。例如:单例模式的线程安全问题,枚举的实现、类加载机制、synchronized 的原理.....。

由于单例模式没回答好,小伙子最终败倒美团一面,错失了大厂机会。

详细面试过程就不细说了,更重要的是,咱不能在同一个地方跌倒两次。

我们今天就来介绍单例模式的 8 种实现方式看完本文,面试再被问到单例模式的实现,就能给到面试官满意的答案了。

本文目录:

  1. 单例模式的定义

  2. 单例模式的使用原因

  3. 单例模式的 8 种实现方式

  4. 单例模式的 4 大应用场景

  5. 单例模式的优点、缺点

  6. 单例模式的选型参考思路

  7. 总结

大家好,我是mikechen,本文是设计模式系列篇之一。为了方便大家学习,我已将本文内容归纳到《深入浅出设计模式》PDF,一共约 2+万字,81页,图文并茂非常详细。

夯实基础、复习备面都用得上,需要的同学文末自取。


01
  单例模式的定义

单例模式(Singleton Pattern),又称为单子模式,属于创建型模式。

单例模式是一种常见的设计模式,确保一个类只有一个实例,并提供一个全局访问点以获取该实例。

单例模式的特点:

  • 单例类只能有一个实例。

  • 单例类必须自己创建自己的唯一实例。

  • 单例类必须给所有其他对象提供这一实例。

 

02
  单例模式的使用原因

单例模式:

  • 保证了全局对象的唯一性。在整个应用中有、且只有一个实例,并提供一个全局访问点,以获取该实例。


  • 避免了因为创建了多个实例造成资源的浪费,且多个实例由于多次调用、容易导致结果出现错误。


示例:



















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 方法。

这样会占用大量内存,导致系统运行变慢。


03
  单例模式的 8 种实现

常见的单例模式实现方式有 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() {}
}


 

04
  单例模式的应用场景

单例模式通常用于想要控制实例数目、节省系统资源的场景中。

一些常见的应用场景:

  •  工具类对象;


  •  频繁访问数据库或文件的对象;


  • 需要频繁的进行创建和销毁的对象;


  • 创建对象时耗时过多、或耗费资源过多,但又频繁要用到的对象。



05
  单例模式的优缺点

单例模式的优点:

  • 避免对资源的多重占用;


  • 简化对象管理,提供全局访问点,方便在整个应用程序中共享实例;


  • 提供了对唯一实例的受控访问。

 

单例模式的缺点:

  • 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难;


  • 不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态;


  • 单例类职责过重,在一定程度上违背了单一职责。

 

06
  单例模式选型参考

单例模式的选型参考:

  • 枚举方式通常被认为是最佳实践,因为它既简洁、又线程安全;


  • 一般情况下使用饿汉式,不使用懒汉式、懒汉式(同步方法);


  • 只有在要明确实现 lazy loading 效果时,才会使用静态内部类方式;


  • 如果有其他特殊的需求,可以考虑使用双重检查锁。


以上,只是提供一些参考思路。

大家在实际应用时,要结合每种实现方式的特点、具体使用场景、以及实际需求合理选型。


总结
  

以上,就是单例模式的全部详解。

通过本文,我们了解并掌握了单例模式的核心知识点,包括:单例模式的 8 种实现方式、4 大应用场景、以及单例模式的选型参考思路等。

在 Java 中,单例模式是开发时应用最多的设计模式。在大厂面试中,单例模式也是高频常考,非常重要。

请使用浏览器的分享功能分享到微信等