从零开始学设计模式(三)——单例模式(Singleton Pattern)

from_Instagram.jpg

单例模式(Singleton Pattern)

单例模式也属于创建型模式,难度等级为初级,是Java中最简单和最常见的设计模式之一。由于其常见性,单例模式的实现方法衍生出很多种,不同的实现方式在延迟加载、线程安全、性能上各有千秋,后面我们会在程序代码说明章节中来具体分析。

单例模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一对象的方式,可以直接访问,不需要实例化该类的对象。

意图

确保一个类只有一个实例,并提供对它的全局访问点

主要解决:一个全局使用的类频繁地创建与销毁。

何时使用:当你想控制实例数目,节省系统资源的时候。

如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。

关键代码:构造函数是私有的。

解释

现实世界的例子

只有一座象牙塔可以让巫师们研究他们的魔法。巫师们总是使用同样的魔法象牙塔。这里的象牙塔是单例

简而言之

确保一个特定类永远只创建一个对象实例

维基百科

In software engineering, the singleton pattern is a software design pattern that restricts the instantiation of a class to one object. This is useful when exactly one object is needed to coordinate actions across the system.(在软件工程中,singleton模式是一种软件设计模式,它将类的实例化限制为一个对象。当只需要一个对象来协调整个系统的动作时,这很有用)

程序代码说明

1、懒汉式,线程不安全的

public class Singleton{
    private static Singleton instance;  
    private Singleton (){}  
  
    public static Singleton getInstance() {  
    if (instance == null) {  
        instance = new Singleton();  
    }  
    return instance;  
    }  
}

这种方式是最基本的实现方式,但由于其不支持多线程,getInstance方法没有加锁,并发的情况下可能产生多个instance实例,所以严格意义上来说它并不算单例模式,但它做到了Lazy初始化。
这种方式目前已不建议使用

2、懒汉式,线程安全的
以上面巫师们的象牙塔为例子

/**
 * Thread-safe Singleton class. The instance is lazily initialized and thus needs synchronization
 * mechanism.
 *
 * Note: if created by reflection then a singleton will not be created but multiple options in the
 * same classloader
 */
public final class ThreadSafeLazyLoadedIvoryTower {

  private static ThreadSafeLazyLoadedIvoryTower instance;

  private ThreadSafeLazyLoadedIvoryTower() {
    // protect against instantiation via reflection
    if (instance == null) {
      instance = this;
    } else {
      throw new IllegalStateException("Already initialized.");
    }
  }

  /**
   * The instance gets created only when it is called for first time. Lazy-loading
   */
  public static synchronized ThreadSafeLazyLoadedIvoryTower getInstance() {
    if (instance == null) {
      instance = new ThreadSafeLazyLoadedIvoryTower();
    }

    return instance;
  }
}

这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是由于加了同步锁,效率很低,99%情况下不需要同步。对于getInstance()的性能要求不是很高的应用程序可以考虑使用(该方法使用不频繁)

3、双检锁/双重校验锁(DCL,即 double-checked locking)

此方式需在JDK1.4之后,采用双锁机制,安全且在多线程情况下保持了较高的性能。

public final class ThreadSafeDoubleCheckLocking {

  private static volatile ThreadSafeDoubleCheckLocking instance;

  /**
   * private constructor to prevent client from instantiating.
   */
  private ThreadSafeDoubleCheckLocking() {
    // to prevent instantiating by Reflection call
    if (instance != null) {
      throw new IllegalStateException("Already initialized.");
    }
  }

  /**
   * Public accessor.
   *
   * @return an instance of the class.
   */
  public static ThreadSafeDoubleCheckLocking getInstance() {
    // local variable increases performance by 25 percent
    // Joshua Bloch "Effective Java, Second Edition", p. 283-284
    
    ThreadSafeDoubleCheckLocking result = instance;
    // Check if singleton instance is initialized. If it is initialized then we can return the instance.
    if (result == null) {
      // It is not initialized but we cannot be sure because some other thread might have initialized it
      // in the meanwhile. So to make sure we need to lock on an object to get mutual exclusion.
      synchronized (ThreadSafeDoubleCheckLocking.class) {
        // Again assign the instance to local variable to check if it was initialized by some other thread
        // while current thread was blocked to enter the locked zone. If it was initialized then we can 
        // return the previously created instance just like the previous null check.
        result = instance;
        if (result == null) {
          // The instance is still not initialized so we can safely (no other thread can enter this zone)
          // create an instance and make it our singleton instance.
          instance = result = new ThreadSafeDoubleCheckLocking();
        }
      }
    }
    return result;
  }
}

4、饿汉式

这种方式比较常用,但容易产生垃圾对象,没有锁机制,执行效率很高,但未实现Lazy初始化。

**
 * Singleton class. Eagerly initialized static instance guarantees thread safety.
 */
public final class IvoryTower {

  /**
   * Private constructor so nobody can instantiate the class.
   */
  private IvoryTower() {}

  /**
   * Static to class instance of the class.
   */
  private static final IvoryTower INSTANCE = new IvoryTower();

  /**
   * To be called by user to obtain instance of the class.
   *
   * @return instance of the singleton.
   */
  public static IvoryTower getInstance() {
    return INSTANCE;
  }
}

基于classloader机制避免了多线程的同步问题,不过由于IvoryTower类在装载时就已经实例化了,显然没有达到lazy loading的效果。

5、登记式/静态内部类

这种方式可以达到上面第三种双检锁一样的功效,具备了延迟加载和线程安全,且实现简单。
对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。

public final class InitializingOnDemandHolderIdiom {

  /**
   * Private constructor.
   */
  private InitializingOnDemandHolderIdiom() {}

  /**
   * @return Singleton instance
   */
  public static InitializingOnDemandHolderIdiom getInstance() {
    return HelperHolder.INSTANCE;
  }

  /**
   * Provides the lazy-loaded Singleton instance.
   */
  private static class HelperHolder {
    private static final InitializingOnDemandHolderIdiom INSTANCE =
        new InitializingOnDemandHolderIdiom();
  }
}

这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,它跟第 4 种方式不同的是:第 4种方式只要 IvoryTower 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 InitializingOnDemandHolderIdiom 类被装载了,instance 不一定被初始化。因为 HelperHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 HelperHolder 类,从而实例化 instance。想象一下,如果实例化 instance 很消耗资源,所以我们想让它延迟加载,这种方式相比第 4 种方式就显得更合理。
此方式对JDK版本也无任何要求,在现在能看到的JDK版本下都支持。

6、枚举

Joshua Bloch, Effective Java 2nd Edition p.18 中提出了使用枚举类来实现单例模式,书中指出这是最好的一种方式来实现单例。

A single-element enum type is the best way to implement a singleton

/**
 * Enum based singleton implementation. Effective Java 2nd Edition (Joshua Bloch) p. 18
 *
 * This implementation is thread safe, however adding any other method and its thread safety
 * is developers responsibility.
 */
public enum EnumIvoryTower {

  INSTANCE;

  public void whateverMethod() {
       // .... do something
  } 
  @Override
  public String toString() {
    return getDeclaringClass().getCanonicalName() + "@" + hashCode();
  }
}

然后我们使用它

EnumIvoryTower enumIvoryTower1 = EnumIvoryTower.INSTANCE;
EnumIvoryTower enumIvoryTower2 = EnumIvoryTower.INSTANCE;
assertEquals(enumIvoryTower1, enumIvoryTower2); // true

总结一下:一般情况下,方式1和2不建议使用,在明确不需要实现lazy loading的时候可以优先选择方式4.在需要lazy loading的时候,可以选择方式3和5。如果还涉及到反序列化创建对象时可以使用最佳方式6

应用场景

当遇到如下情况时可考虑使用单例模式:

  • 一个类必须只有一个实例,客户端必须可以从众所周知的通道访问它
  • 一个单实例应该可以被子类可扩展,并且客户端能够使用扩展实例而无需修改其代码

使用场景

  • 日志记录类
  • 管理一个数据库连接
  • 文件管理器

Java中的现实例子

优缺点

优点:
1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
2、避免对资源的多重占用(比如写文件操作)。

缺点:
1、没有接口,不能继承,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
2、通过控制自己的创建和生命周期,违反了单一责任原则SRP(Single Responsibility Principle)
3、创建紧密耦合的代码,单例模式的客户端变得难以测试

写在最后

单例模式的使用在Java应用中是非常普遍的,Spring 管理bean 实例默认配置就是单例模式。

下一章节我将介绍建造者模式(Builder Pattern)

码字不易,各位看官如果喜欢的话,请给点个赞 ❤️,关注下我,我将努力持续不断的更新

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容