002 单例模式

单例模式(Singleton Pattern)

前言

😊 按照 001 篇讲的,以后的每个模式都将按照:模式名称、问题、解决方案以及效果这几个主要的要素研究。

  • 学习难度:😏😏😏😏😏
  • 使用频率:😉😉😉😉😉

开始吧

模式名称

中文:单例模式

English: Singleton Pattern

含义:单例对象的类必须保证只有一个实例存在。

问题

何时使用

当我们需要统一管理资源,共享资源的时候就需要使用单例模式。保障

场景:

  1. Windows的Task(任务管理器) 就是很经典的单例模式。一台电脑正常情况下只能打开一个任务管理器。
  2. 网站的计数器,一般也采用单例模式实现,否则难以同步。
  3. 应用程序的日志应用,一般都才会用单例模式实现,这通常是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。
  4. Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享的资源。
  5. 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,因为何用单例模式来维护,就可以大大降低这种损耗。
  6. 多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。
  7. 操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。
  8. HttpApplication 也是单例的典型应用。熟悉ASP.Net(IIS)的整个请求生命周期的人应该知道HttpApplication也是单例模式,所有的HttpModule都共享一个HttpApplication实例.

存在的问题

  • 优点
    • 单例模式提供了唯一实例的受控访问,因为单例模式封装了他的唯一实例,所以他可以严格控制客户怎样以及何时访问它。
    • 由于系统中只存在一个资源,对于一些需要频繁创建和销毁的对象单例模式可以提高性能。
    • 允许可变数目的实例。基于单例模式我们可以进行扩展,使用与单例模式相似的方法来来获得指定个数的对象实例,既节省系统资源,又解决了单例对象共享过多有损性能的问题。
  • 缺点
    • 由于单例模式没有抽象层,因此单例类的扩展有很大的困难。
    • 单例类的职责过重,在一定程度上违背了‘单一职责的原则’。因为单例类既承担了工厂的角色,提供了工厂方法,又充当了产品的角色,包含了一些业务方法,将产品的创建和产品本身的功能融合到一起。
    • 现在很多面相对象语言的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的共享对象长时间不被利用,系统会认为他是垃圾,自动销毁回收。下次使用时再重新实例化,这将导致共享的单例对象状态的丢失。

请带着问题找答案:stuck_out_tongue_winking_eye:。

解决方案

来个UML图先

Singleton UML
Singleton UML


( Singleton UML )


科普一下:什么叫懒汉模式,什么叫饿汉模式

  • 懒汉模式 --> 特点是懒,不用的时候我就睡觉(不实例化)。
  • 饿汉模式 --> 特点是饿,一上来我就要吃(实例化)。

最简单的实现方法

// 懒汉,你不调用getInstance() 就不会实例化
public class Singleton {
    private static Singleton instance;
    public static Singleton getInstance() {
         if (instance == null) {
             instance = new Singleton();
         }
         return instance;
     }
}

解析:这是一个最简单的实现方法。通过 getInstance() 获取 Singleton 这个类的实例。instance == null 的情况下 new 一个实例。不为空就返回这个实例。可以说,这个方法最适合教学,一眼就能看的很明白什么是单例。

BUT , 想一想,我是不是在外面也能 通过 new Singleton() 来创建一个实例 ?那都是多个实例了。还怎么单例?

So ,我们要开始进阶了。脱离学生手法,开始进阶。

// 懒汉,你不调用getInstance() 就不会实例化
public class Singleton {
    private static Singleton instance;
    public static Singleton getInstance() {
         if (instance == null) {
             instance = new Singleton();
         }
         return instance;
     }
     // add 1.1 begin (将构造方法私有化)
     private Singleton() {
     }
     // add 1.1 end
}

经过 v1.1 版本的改造。发现在外面再也 new 不出来 Singleton 的实例。😛我学会了?给你个眼神😏

我们再想:两个线程几乎同时调用 getInstance() 第一个进入的线程判断 instance 为空,开始走这一行 instance = new Singleton();。,注意,是开始走这一行,并未完成实例化! 此时第二个线程也走到 if (instance == null)此时判断也为空。那么 这两个线程都会得到一个新的实例,那么就产生两个实例。那么还怎么单例?

以上为懒汉模式 -- (线程不安全)

懒汉模式 - 线程安全

为了解决 上面那个方法在多线程下使用不安全的问题。我们再次进阶。
这次吊了😏这从线程安全了!

我反手就给你一段代码

// 懒汉,你不调用getInstance() 就不会实例化
public class Singleton {
    private static Singleton instance;
    // modif synchronized
    public static synchronized  Singleton getInstance() {
         if (instance == null) {
             instance = new Singleton();
         }
         return instance;
     }
     // add 1.1 begin (将构造方法私有化)
     private Singleton() {
     }
     // add 1.1 end
}

看,加一个synchronized同步,来一个要等着。等里面的代码执行完了,第二个线程再进去。第二个再进去的时候,instance就不是空了,就又能单例了。😏😏


Too young。这样搞的话,我们每次进来都可能要同步一下。多数时候我们并不是两个或多个线程同时来访问,我们并不需要去同步。这样做其实造成了不必要的开销。有木有更好的方法呢?


Double Checked locking pattern 【 双重检验锁 】



我反手又是一串代码(关键代码)

只贴上关键代码。才好

public static Singleton getInstance() {
    if (instance == null) {               //Single Checked
        synchronized (Singleton.class) {
            if (instance == null) {       //Double Checked
                instance = new Singleton();
            }
        }
    }
    return instance ;
}

看到没,你想到这样搞了吗? 先检测再同步。Single Checked足以应付多数检测。当一个以上的线程同时访问时就用上了Double Checked防止多线程下重复创建实例。 😵即使我们这么想到这么吊的方法,还是不行。。。为啥?

为啥? instance = new Singleton() 这个不是原子操作。当我们 new 的时候 JVM 大概做了这些事:

  1. instance分配内存
  2. 调用 Singleton的构造函数,来初始化成员变量
  3. instance对象指向分配的内存空间(执行完这步 instance 就不为 null 了)

但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

解决方法也很简单:加上 volatile 就可以了。使用 volatile 的主要原因是其一个特性:禁止指令重排序优化

 private volatile static Singleton instance; //声明成 volatile

注意: 在 Java 5 以前的版本使用了 volatile 的双检锁还是有问题的。其原因是 Java 5 以前的 JMM (Java 内存模型)是存在缺陷的,即时将变量声明成 volatile 也不能完全避免重排序,主要是 volatile 变量前后的代码仍然存在重排序问题。这个 volatile 屏蔽重排序的问题在 Java 5 中才得以修复,所以在这之后才可以放心使用 volatile。


注解
原子操作:
如果这个操作所处的层(layer)的更高层不能发现其内部实现与结构,那么这个操作是一个原子(atomic)操作。
原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分。
将整个操作视作一个整体是原子性的核心特征。

这个方法解决了上面所有的不安全因素,但是!在 Java 5 以前的版本上跑却还是会有问题,所以,这个也不是最好的方法。。。

饿汉来了

🤤

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

是这样的,因为单例的实例 instance 被声明称 staticfinal 了,在第一次加载类到内存中就会被实例化。所以创建实例本身是线程安全的。一上来就加载,所以是饿汉。

缺点:太着急加载。有时候我们想加点料也不给机会。有时候我们创建实例需要依赖参数或者配置文件。这样就不能使用饿汉模式。🤤

怎么办?

内部静态类

先瞅代码

public class Singleton {
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    private Singleton() {}
    public static final Singleton getInstance(int a) {
        return SingletonHolder.INSTANCE;
    }
}

由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本。能完美应对多数场景。如果你觉得写着麻烦,其实还有这一种很简单的写法。好像不太常用。就是下面这个枚举

枚举单例

《Effective Java》:单元素的枚举类型已经成为实现Singleton的最佳方法

public enum Singleton {
     //理解为 public static final Singleton INSTANCE;
     INSTANCE;
 }

实例化:Singleton.INSTANCE 就是这么简单。但是单例这样用的人我觉得还是不多。猜想是大家对枚举了解不太多吧。如果看到枚举这个方式一脸懵B,就看看枚举相关的知识。反正我一开始也是一脸**

效果

以上介绍了:

  • 简单写法(入门)
  • 懒汉模式线程安全
  • 双重检验锁(DLC)
  • 饿汉模式
  • 内部静态类
  • 枚举

几种方法不重要。最重要的知道什么是单例模式,为啥用单例。甚至不在代码中使用,工作、生活、学习、游戏中充满单例思想。掌握这个思想以及解决办法,生活会很精彩😄😄

再说一下:《Effective Java》推荐 DLC 和枚举,那些明显有问题的写法就不要用了。那些写法都是用来教学理解单例的。

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

推荐阅读更多精彩内容