单例模式

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

单例模式

一、特点

1.1 属性

意图保证一个类仅有一个实例,并提供一个访问它的全局访问点

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

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

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

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

应用实例:一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。

  • 优点:

    1. 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
    2. 避免对资源的多重占用(比如写文件操作)。
  • 缺点:没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。

  • 使用场景:

    1. 要求生产唯一序列号
    2. WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来
    3. 创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。
  • 注意事项:getInstance() 方法中需要使用同步锁 synchronized (Singleton.class) 防止多线程同时进入造成 instance 被多次实例化。



二、单例模式的实现

三个步骤:

  1. 构造函数私有化
  2. 自行对外提供实例
  3. 提供外界可以获得该实例的方法

2.1 传统的创建类的方式

public class s1 {
    public static void main(String[] args) {

        Singleton singleton1 = new Singleton();
        Singleton singleton2 = new Singleton();
    }
}

class Singleton {

}

上述代码中,每次new Singleton()都会创建一个Signleton实例。

2.2 恶汉模式

是否 Lazy 初始化:否
是否多线程安全:是
实现难度:易

  • 描述:这种方式比较常用,但容易产生垃圾对象
  • 优点:没有加锁,执行效率会提高
  • 缺点:类加载时就初始化,浪费内存

它基于 classloader 机制避免了多线程的同步问题,不过,instance在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法,但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance显然没有达到 lazy loading 的效果。

lazy loading(延迟加载):例如创建某一对象时需要花费很大的开销,而这一对象在系统的运行过程中不一定会用到,这时就可以使用延迟加载,在第一次使用该对象时再对其进行初始化,如果没有用到则不需要进行初始化,这样的话,使用延迟初始化就提高程序的效率,从而使程序占用更少的内存。

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

    private Singleton() {
    }

    public static Singleton getInstance() {
        return instance;
    }
}

2.3 懒汉模式

是否Lazy初始化:是
是否多线程安全:是
实现难度:易

  • 描述:这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是效率很低,99% 情况下不需要同步。
  • 优点:第一次调用才初始化,避免内存浪费
  • 缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率

getInstance() 的性能对应用程序不是很关键(该方法使用不太频繁)。

public class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

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

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

添加synchronized锁虽然可以保证线程安全,但是每次访问getInstance()方法的时候,都会有加锁和解锁操作,同时synchronized锁加在方法上面,锁的范围过大,会成为系统的瓶颈

是否 Lazy 初始化:是
是否多线程安全:是
实现难度:较复杂

  • 描述:这种方式采用双锁机制,安全且在多线程情况下能保持高性能。

getInstance() 的性能对应用程序很关键

public class Singleton {
    private volatile static Singleton singleton;

    private Singleton() {
    }

    public static Singleton getSingleton() {
        if (singleton == null) {    //第一次校验
            synchronized (Singleton.class) {    //只对这一部分加锁
                if (singleton == null) {    //第二次校验
                    singleton = new Singleton();    //非原子操作
                }
            }
        }
        return singleton;
    }
}

2.5 双检锁/双重校验锁(增加volatile)

双重校验锁会出现指令重排的问题,singleton = new Singleton();并非一个原子操作,实际上,它可以抽象为下面几个JVM指令:

//1. 分配对象内存空间
memory = allocate();
//2. 初始化对象
ctorInstance(memory);
//3. 设置instance指向刚分配的内存地址
singleton = memory;

操作2依赖于操作1,但操作3并不依赖于操作1,所以JVM是可以针对它们进行指令优化,优化后如下:

//1. 分配对象内存空间
memory = allocate();
//3. 设置instance指向刚分配的内存地址
singleton = memory;
//2. 初始化对象
ctorInstance(memory);

可以看到,指令重排之后,singleton指向分配好的内存放在前面,而这段内存的初始化被排在了后面。
线程A执行这段赋值语句,在初始化分配对象之前就已经将其赋值singleton引用,恰好线程B进入方法判断singleton的引用不为空,然后就将其返回使用,导致程序出错。
为了解决指令重排问题,可以使用volatile关键字修饰singleton字段,禁止指令的重排序优化

class Singleton {
    private static volatile Singleton singleton = null;

    private Singleton();

    public static Singleton getInstance() {
        if (singleton = null) {
            synchronized (Singleton.class) {
                if (singleton = null) {
                    singleton = new Singleton();
                }
            }
        }
        return signleton;
    }
}


2.6 静态内部类

是否 Lazy 初始化:是
是否多线程安全:是
实现难度:一般

描述:当第一次访问类中的静态字段时,会触发类加载,并且保证同一个类只加载一次。静态内部类也是如此,类加载过程有类加载器负责加锁。这种写法相对于双重检验锁的写法,更加简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。

静态域:如果将域定义为static,每个类中只有一个这样的域,这个类的所有对象将共享这个域,这个域称为静态域。这个域属于类,而不属于任何独立的对象。

class Singleton {
    //私有的静态内部类,类加载器负责加锁
    private static class SingletonHolder {
        private static Singleton singleton = new Singleton();
    }

    private Singleton() {
    }

    public static Singleton getInstance() {
        return Singleton.singleton;
    }
}


2.6 枚举

是否 Lazy 初始化:否
是否多线程安全:是
实现难度:易

描述:这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化
这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。不能通过 reflection attack 来调用私有构造方法。

public enum Singleton {
    INSTANCE;

    public void whateverMethod() {
    }
}








推荐阅读

参考

[1]黄文毅,Spring MVC + MyBatis快速开发与项目实战.北京:清华出版社,2019.
[2]https://www.runoob.com/design-pattern/singleton-pattern.html
[3]https://blog.csdn.net/u013894997/article/details/81111236
原文地址:https://www.cnblogs.com/ccink/p/13367246.html