什么是单例模式

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

    对于系统中的某些类来说,只有一个实例很重要,例如,一个系统中可以存在多个打印任务,但是只能有一个正在工作的任务;售票时,一共有100张票,可有有多个窗口同时售票,但需要保证不要超售(这里的票数余量就是单例,售票涉及到多线程)。如果不是用机制对窗口对象进行唯一化将弹出多个窗口,如果这些窗口显示的都是相同的内容,重复创建就会浪费资源。
    优点

    提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它,并为设计及开发团队提供了共享的概念。
    由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能

    缺点

    由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
    单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。
    滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致对象状态的丢失。

    单例模式结构图

    单例模式-设计模式(C#) - 图1 :::info 单例模式第一版 :::

    1. class Singleton
    2. {
    3. private static Singleton instance;
    4. //构造方法让其private,这就堵死了外界利用new创建此类实例的可能
    5. private Singleton()
    6. {
    7. }
    8. //此方法是获得本类实例的唯一全局访问点
    9. public static Singleton GetInstance()
    10. {
    11. //若实例不存在,则new一个新实例,否则返回已有的实例
    12. if (instance == null)
    13. {
    14. instance = new Singleton();
    15. }
    16. return instance;
    17. }
    18. }

    为什么这样写呢?我们来解释几个关键点:

    要想让一个类只能构建一个对象,自然不能让它随便去做new操作,因此Signleton的构造方法是私有的。

    instance是Singleton类的静态成员,也是我们的单例对象。它的初始值可以写成Null,也可以写成new Singleton()。至于其中的区别后来会做解释。

    getInstance是获取单例对象的方法。
    如果单例初始值是null,还未构建,则构建单例对象并返回。这个写法属于单例模式当中的懒汉模式。
    如果单例对象一开始就被new Singleton()主动构建,则不再需要判空操作,这种写法属于饿汉模式。
    这两个名字很形象:饿汉主动找食物吃,懒汉躺在地上等着人喂。

    但是刚才的代码不是线程安全,为什么说刚才的代码不是线程安全呢?

    假设Singleton类刚刚被初始化,instance对象还是空,这时候两个线程同时访问getInstance方法:

    单例模式-设计模式(C#) - 图2
    因为Instance是空,所以两个线程同时通过了条件判断,开始执行new操作:
    单例模式-设计模式(C#) - 图3
    这样一来,显然instance被构建了两次。让我们对代码做一下修改:
    单例模式第二版

    1. public class Singleton {
    2. private Singleton() {} //私有构造函数
    3. private static Singleton instance = null; //单例对象
    4. //静态工厂方法
    5. public static Singleton getInstance() {
    6. if (instance == null) { //双重检测机制
    7. synchronized (Singleton.class){ //同步锁
    8. if (instance == null) { //双重检测机制
    9. instance = new Singleton();
    10. }
    11. }
    12. }
    13. return instance;
    14. }
    15. }

    为什么这样写呢?我们来解释几个关键点:

    为了防止new Singleton被执行多次,因此在new操作之前加上Synchronized 同步锁,锁住整个类(注意,这里不能使用对象锁)。

    进入Synchronized 临界区以后,还要再做一次判空。因为当两个线程同时访问的时候,线程A构建完对象,线程B也已经通过了最初的判空验证,不做第二次判空的话,线程B还是会再次构建instance对象。

    单例模式-设计模式(C#) - 图4
    单例模式-设计模式(C#) - 图5
    单例模式-设计模式(C#) - 图6
    单例模式-设计模式(C#) - 图7
    单例模式-设计模式(C#) - 图8

    像这样两次判空的机制叫做双重检测机制。

    但是我们的这段代码仍然不是绝对的线程安全

    假设这样的场景,当两个线程一先一后访问getInstance方法的时候,当A线程正在构建对象,B线程刚刚进入方法:

    单例模式-设计模式(C#) - 图9

    这种情况表面看似没什么问题,要么Instance还没被线程A构建,线程B执行 if(instance == null)的时候得到false;要么Instance已经被线程A构建完成,线程B执行 if(instance == null)的时候得到true。

    真的如此吗?答案是否定的。这里涉及到了JVM编译器的指令重排。