单例模式

定义

单例模式理解起来非常简单。一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。

用处

从业务概念上,如果有些数据在系统中只应保存一份,那就比较适合设计为单例类。

比如配置信息类和数据库连接对象。在系统中,我们只有一份配置文件,当配置文件被加载到内存之后,以对象的方式存在,也理所当然只有一份。

如何实现单例模式

单例模式实现起来有两种方式:

  • 饿汉式单例:在类加载的期间,就已经将 instance 静态实例初始化好了,所以,instance 实例的创建是线程安全的。不过,这样的实现方式不支持延迟加载实例。

  • 懒汉式单例:懒汉式相对于饿汉式的优势是支持延迟加载。这种实现方式会导致频繁加锁、释放锁,以及并发度低等问题,频繁的调用会产生性能瓶颈。

饿汉式单例

饿汉式单例表示在程序初始化的时候就将实例创建并且初始化好了。饿汉式单例在 Go 语言实现时,借助 Go 的 init 函数来实现特别方便。下面是在 Go 中比较常见的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var db *grom.DB

func init() {
db, err := New()
if err != nil {
panic(err)
}
}

// New 根据MySQL选项去构建gorm对象
func New() (*gorm.DB, error) {
dsn := fmt.Sprintf(`%s:%s@tcp(%s)/%s?charset=utf8&parseTime=%t&loc=%s`,
"root",
"pass",
fmt.Sprintf("%s:%s", "127.0.0.1", "3306"),
"suv",
true,
"Local")
return gorm.Open(mysql.Open(dsn), getConfig())
}

// 自定义gorm配置
func getConfig() *gorm.Config {
c := &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true}
_default := logger.New(log2.New(os.Stdout, "\r\n", log2.LstdFlags), logger.Config{
SlowThreshold: 200 * time.Millisecond, // 打印慢SQL
LogLevel: logger.Info, // 打印级别为info
Colorful: true, // 是否为彩色输出到控制台
})
c.Logger = _default.LogMode(logger.Error)
return c
}

有人觉得这种实现方式不好,因为不支持延迟加载,如果实例占用资源多(比如占用内存多)或初始化耗时长(比如需要加载各种配置文件),提前初始化实例是一种浪费资源的行为。最好的方法应该在用到的时候再去初始化。不过,我个人并不认同这样的观点。

如果初始化耗时长,那我们最好不要等到真正要用它的时候,才去执行这个耗时长的初始化过程,这会影响到系统的性能(比如,在响应客户端接口请求的时候,做这个初始化操作,会导致此请求的响应时间变长,甚至超时)。

采用饿汉式实现方式,将耗时的初始化操作,提前到程序启动的时候完成,这样就能避免在程序运行的时候,再去初始化导致的性能问题。

如果实例占用资源多,按照 fail-fast 的设计原则(有问题及早暴露),那我们也希望在程序启动时就将这个实例初始化好。如果资源不够,就会在程序启动的时候触发报错(比如 OOM),我们可以立即去修复。

这样也能避免在程序运行一段时间后,突然因为初始化这个实例占用资源过多,导致系统崩溃,影响系统的可用性。

懒汉式单例

有饿汉式,对应的,就有懒汉式。懒汉式相对于饿汉式的优势是支持延迟加载。

不过这块特别注意,要考虑并发环境下,你的判断实例是否已经创建时,是不是用的当前读。在一些教设计模式的教程里,一般这种情况下会举一个例子–用 Java 双重锁实现线程安全的单例模式,双重锁指的是 volatile 和 synchronized。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class IdGenerator { 
private AtomicLong id = new AtomicLong(0);
private static IdGenerator instance;
private IdGenerator() {}
public static IdGenerator getInstance() {
if (instance == null) {
synchronized(IdGenerator.class) { // 此处为类级别的锁
if (instance == null) {
instance = new IdGenerator();
}
}
}
return instance;
}
public long getId() {
return id.incrementAndGet();
}
}

在这种实现方式中,只要 instance 被创建之后,即便再调用 getInstance() 函数也不会再进入到加锁逻辑中了。所以,这种实现方式解决了懒汉式并发度低的问题。

在 Go 中没有 volatile 这种机制,那如何操作呢?

一般是借助 sync 库自带的兵法同步原语 Once 来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package singleton

import (
"sync"
)

type singleton struct {}

var instance *singleton
var once sync.Once

func GetInstance() *singleton {
once.Do(func() {
instance = &singleton{}
})
return instance
}