单例模式
定义
单例模式理解起来非常简单。一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。
用处
从业务概念上,如果有些数据在系统中只应保存一份,那就比较适合设计为单例类。
比如配置信息类和数据库连接对象。在系统中,我们只有一份配置文件,当配置文件被加载到内存之后,以对象的方式存在,也理所当然只有一份。
如何实现单例模式
单例模式实现起来有两种方式:
饿汉式单例:在类加载的期间,就已经将 instance 静态实例初始化好了,所以,instance 实例的创建是线程安全的。不过,这样的实现方式不支持延迟加载实例。
懒汉式单例:懒汉式相对于饿汉式的优势是支持延迟加载。这种实现方式会导致频繁加锁、释放锁,以及并发度低等问题,频繁的调用会产生性能瓶颈。
饿汉式单例
饿汉式单例表示在程序初始化的时候就将实例创建并且初始化好了。饿汉式单例在 Go 语言实现时,借助 Go 的 init
函数来实现特别方便。下面是在 Go 中比较常见的代码:
1 | var db *grom.DB |
有人觉得这种实现方式不好,因为不支持延迟加载,如果实例占用资源多(比如占用内存多)或初始化耗时长(比如需要加载各种配置文件),提前初始化实例是一种浪费资源的行为。最好的方法应该在用到的时候再去初始化。不过,我个人并不认同这样的观点。
如果初始化耗时长,那我们最好不要等到真正要用它的时候,才去执行这个耗时长的初始化过程,这会影响到系统的性能(比如,在响应客户端接口请求的时候,做这个初始化操作,会导致此请求的响应时间变长,甚至超时)。
采用饿汉式实现方式,将耗时的初始化操作,提前到程序启动的时候完成,这样就能避免在程序运行的时候,再去初始化导致的性能问题。
如果实例占用资源多,按照 fail-fast 的设计原则(有问题及早暴露),那我们也希望在程序启动时就将这个实例初始化好。如果资源不够,就会在程序启动的时候触发报错(比如 OOM),我们可以立即去修复。
这样也能避免在程序运行一段时间后,突然因为初始化这个实例占用资源过多,导致系统崩溃,影响系统的可用性。
懒汉式单例
有饿汉式,对应的,就有懒汉式。懒汉式相对于饿汉式的优势是支持延迟加载。
不过这块特别注意,要考虑并发环境下,你的判断实例是否已经创建时,是不是用的当前读。在一些教设计模式的教程里,一般这种情况下会举一个例子–用 Java 双重锁实现线程安全的单例模式,双重锁指的是 volatile 和 synchronized。
1 | public class IdGenerator { |
在这种实现方式中,只要 instance 被创建之后,即便再调用 getInstance() 函数也不会再进入到加锁逻辑中了。所以,这种实现方式解决了懒汉式并发度低的问题。
在 Go 中没有 volatile
这种机制,那如何操作呢?
一般是借助 sync
库自带的兵法同步原语 Once
来实现:
1 | package singleton |