单例模式
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
一、特点
1.1 属性
意图保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决:一个全局使用的类频繁地创建与销毁。
何时使用:当您想控制实例数目,节省系统资源的时候。
如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
关键代码:构造函数是私有的。
应用实例:一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。
优点:
- 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
- 避免对资源的多重占用(比如写文件操作)。
缺点:没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
使用场景:
- 要求生产唯一序列号
- WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来
- 创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。
注意事项:getInstance() 方法中需要使用同步锁 synchronized (Singleton.class) 防止多线程同时进入造成 instance 被多次实例化。
二、单例模式的实现
三个步骤:
- 构造函数私有化
- 自行对外提供实例
- 提供外界可以获得该实例的方法
2.1 传统的创建类的方式
1 | public class s1 { |
上述代码中,每次new Singleton()
都会创建一个Signleton实例。
2.2 恶汉模式
是否 Lazy 初始化:否
是否多线程安全:是
实现难度:易
- 描述:这种方式比较常用,但容易产生垃圾对象。
- 优点:没有加锁,执行效率会提高。
- 缺点:类加载时就初始化,浪费内存。
它基于 classloader 机制避免了多线程的同步问题,不过,instance在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法,但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance显然没有达到 lazy loading 的效果。
lazy loading(延迟加载):例如创建某一对象时需要花费很大的开销,而这一对象在系统的运行过程中不一定会用到,这时就可以使用延迟加载,在第一次使用该对象时再对其进行初始化,如果没有用到则不需要进行初始化,这样的话,使用延迟初始化就提高程序的效率,从而使程序占用更少的内存。
1 | public class Singleton { |
2.3 懒汉模式
是否Lazy初始化:是
是否多线程安全:是
实现难度:易
- 描述:这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是效率很低,99% 情况下不需要同步。
- 优点:第一次调用才初始化,避免内存浪费。
- 缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。
getInstance() 的性能对应用程序不是很关键(该方法使用不太频繁)。
1 | public class Singleton { |
2.4 双检锁/双重校验锁(DCL,即 double-checked locking)
添加synchronized锁虽然可以保证线程安全,但是每次访问getInstance()方法的时候,都会有加锁和解锁操作,同时synchronized锁加在方法上面,锁的范围过大,会成为系统的瓶颈。
是否 Lazy 初始化:是
是否多线程安全:是
实现难度:较复杂
- 描述:这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
getInstance() 的性能对应用程序很关键
1 | public class Singleton { |
2.5 双检锁/双重校验锁(增加volatile)
双重校验锁会出现指令重排的问题,singleton = new Singleton();
并非一个原子操作,实际上,它可以抽象为下面几个JVM指令:
1 | //1. 分配对象内存空间 |
操作2依赖于操作1,但操作3并不依赖于操作1,所以JVM是可以针对它们进行指令优化,优化后如下:
1 | //1. 分配对象内存空间 |
可以看到,指令重排之后,singleton指向分配好的内存放在前面,而这段内存的初始化被排在了后面。
线程A执行这段赋值语句,在初始化分配对象之前就已经将其赋值singleton引用,恰好线程B进入方法判断singleton的引用不为空,然后就将其返回使用,导致程序出错。
为了解决指令重排问题,可以使用volatile
关键字修饰singleton字段,禁止指令的重排序优化。
1 | class Singleton { |
2.6 静态内部类
是否 Lazy 初始化:是
是否多线程安全:是
实现难度:一般
描述:当第一次访问类中的静态字段时,会触发类加载,并且保证同一个类只加载一次。静态内部类也是如此,类加载过程有类加载器负责加锁。这种写法相对于双重检验锁的写法,更加简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
静态域:如果将域定义为static,每个类中只有一个这样的域,这个类的所有对象将共享这个域,这个域称为静态域。这个域属于类,而不属于任何独立的对象。
1 | class Singleton { |
2.6 枚举
是否 Lazy 初始化:否
是否多线程安全:是
实现难度:易
描述:这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。不能通过 reflection attack 来调用私有构造方法。
1 | public enum Singleton { |
推荐阅读
参考
[2]https://www.runoob.com/design-pattern/singleton-pattern.html
[3]https://blog.csdn.net/u013894997/article/details/81111236
发布时间: 2020-07-02 22:59:10
更新时间: 2022-04-21 16:19:32
本文链接: https://wyatt.ink/posts/Code/1a4b62fe.html
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!