单例模式 (Singleton Pattern) | 塔链原创设计模式讲座之一
Posted 塔链实验室
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了单例模式 (Singleton Pattern) | 塔链原创设计模式讲座之一相关的知识,希望对你有一定的参考价值。
塔链原创设计模式讲座
- 塔链原创设计模式讲座共包含二十三节,通过适用场景、模式解说、JAVA实现样例等进行详细讲解。
塔链原创设计模式讲座之一:
单例模式 (Singleton Pattern)
适用场景
单例模式适用于需要确保整个程序进程中有且仅有一个对象实例的场合。
模式解说
以Java语言为例,一般来讲,在我们每次使用new关键字调用类的构造器(Constructor)时,都会在内存中产生一个新的对象。例如:
Prototype obj = new Prototype();
每次调用这行程序,都将产生一个新的Prototype对象实例。但是,在某些场合,我们需要确保整个系统中有且仅有一个对象实例。例如,某些持有耗时性资 源的任务对象(比如外部设备连接资源),大量地生成它们的实例对于整个系统来说将是沉重的负担。又如,某些互联网高并发场景下的无状态服务对象,它们本身 仅提供服务接口函数的实现,不存储任何请求状态。如果对于每个请求都生成一个服务实例,则在系统运行过程中会充斥大量无谓的对象创建和销毁过程,严重降低 系统的运行性能。再如,某些关键的序列号生成对象,如果系统中存在多个此类对象,则很容易对序列号的唯一性造成破坏,影响关键业务流程的展开。
为了解决这类问题,我们可以采用单例模式。
Java实现样例
public class Singleton {
private static Singleton singleton = new Singleton();
private Singleton () {
//执行一些初始化代码
}
public static Singleton getInstance () {
return singleton;
}
}
在此模式下,由于类的构造器是封闭的,类的外部无法再通过new关键字调用构造器,使得系统的其他任何地方再无可能来创建出新的Singleton对象。与此相对应的,此模式提供了静态的类方法,使得在类外部的任何地方调用它都能获得唯一的对象引用。
在上述样例中,Singleton对象的构造器实际是在类的静态构造方法中被调用,一旦类被加载完成,就会构造出一个Singleton实例并执行构造器。 之后每次调用getInstance方法,仅仅返回对象引用即可。与此相对应的,还有一种“懒加载”模式的单例写法,如下:
public class Singleton {
private static Singleton singleton;
private Singleton() {
//执行一些初始化代码
}
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton2();
}
return singleton;
}
}
在懒加载模式下,对象的初始化将被推迟到第一次访问getInstance方法时才会执行。采用这种写法的原因在于,有时候我们可能需要推迟一些高开销的对 象初始化操作,并且只有在使用这些对象时才进行初始化。所以懒加载有时又被称为延迟初始化。由于每次调用getInstance方法时都会进行为空判定, 所以对执行性能还是会有一些影响。
多线程并发问题
在多线程并发场景下,懒加载模式的单例写法有时并不能确保实例的唯一性。原因在于,如 果构造器需要执行的时间很长,假设线程A,B先后执行getInstance的代码,线程B在执行为空判定的同时,线程A仍然正在执行new Singleton()的操作且未发布引用,于是线程B也会执行new Singleton()操作,破坏了单例模式的唯一性。针对这种并发问题,对getInstance方法使用synchronized关键字看上去是个不 错的主意,但是同步代码将导致并发程序执行性能的下降。于是,出现了如下的著名方案,称为“双重检锁” (DCL: Double-Checked Locking):
public class Singleton {
private static Singleton singleton;
private Singleton() {
//执行一些初始化代码
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton3();
}
}
}
return singleton;
}
}
由于双重检锁方案缩小了同步块的作用范围,只有在singleton的为空判定成功的情况下才会进入同步块,此方案相比于getInstance的整体加锁 方案,无疑拥有更高的并发性能。而由于同步块的存在,使得同一时间仍然只能有一个线程来执行new Singleton()的操作,确保了实例的唯一性。此方案看上去“非常完美”,但是不幸的是,由于JVM指令重排序的作用,线程A可能先设置 singleton来指向内存空间,再进行对象初始化,此时线程B判定singleton将不为空,但是会访问到一个还未初始化的对象,对这种对象进行操 作很容易造成逻辑异常。在线上并发场景下,这类问题往往难以排查。
解决双重检锁问题非常简单,一般有如下2种方案:
1). 对singleton对象采用volatile关键字声明,禁止指令的重排序,确保先执行对象初始化,再发布引用。
private volatile static Singleton singleton
2). 基于类初始化的解决方案。
public class Singleton {
private static class SingletonHolder {
public static Singleton singleton = new Singleton();
}
private Singleton() {
}
public static Singleton getInstance() {
return SingletonHolder.singleton;
}
}
本文为塔链实验室原创,未经授权,不得转载。
以上是关于单例模式 (Singleton Pattern) | 塔链原创设计模式讲座之一的主要内容,如果未能解决你的问题,请参考以下文章
设计模式之- 单例模式(Singleton Pattern)