简介

单例模式在面向对象中是一个常见、简单的模式。
英文名称:Singleton Pattern
该模式规定一个类只允许有一个实例,而且自行实例化并向整个系统提供这个实例。因此该模式的要点有:

  • 只有一个实例
  • 必须自行创建
  • 必须自行向整个系统提供这个实例

意图

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

主要解决

避免一个全局使用的类频繁地创建与销毁。

何时使用

当您想控制实例数目,节省系统资源的时候;或者系统不允许存在多实例的时候。

如何实现

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

优缺点

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

饿汉式和懒汉式单例

根据实例化的时机,单例模式一般分成饿汉式和懒汉式。

  • 饿汉式:在定义实例时直接实例化,var Instance = new(singleton)
  • 懒汉式:在实例化方法中进行实例化,

那两者有什么区别或优缺点?

  • 饿汉式单例类:在自己被加载时就将自己实例化。即便加载器是静态的,饿汉式单例类被加载时仍会将自己实例化。单从资源利用率角度讲,这个比懒汉式单例类稍差些。从速度和反应时间角度讲,则比懒汉式单例类稍好些。
  • 懒汉式单例类:在实例化时,必须处理好在多个线程同时首次引用此类时的访问限制问题,特别是当单例类作为资源控制器在实例化时必须涉及资源初始化,而资源初始化很有可能耗费时间。这意味着出现多线程同时首次引用此类的几率变得较大。

代码实现

以计数器为例子,看看Go语言中如何实现单例模式

饿汉式

  1. // 饿汉式单例模式
  2. package singleton
  3. type singleton struct {
  4. count int
  5. }
  6. var Instance = new(singleton)
  7. func (s *singleton) Add() int {
  8. s.count++
  9. return s.count
  10. }
  11. // 使用,在其他包中可以直接调用 Instance 的 Add 方法
  12. c := singleton.Instance.Add()
  • 有且只提供一个实例变量 Instance
  • 自行创建
  • Instance 是个全局的、面向整个系统的实例

懒汉式

  1. // 懒汉式单例模式
  2. package singleton
  3. import "sync"
  4. type singleton struct {
  5. count int
  6. }
  7. var (
  8. instance *singleton
  9. mutex sync.Mutex
  10. )
  11. func (s *singleton) Add() int {
  12. s.count++
  13. return s.count
  14. }
  15. // 普通实例化方法
  16. func New1() *singleton {
  17. mutex.Lock()
  18. defer mutex.Unlock()
  19. if instance == nil {
  20. instance = new(singleton)
  21. }
  22. return instance
  23. }
  24. // 双重检查实现
  25. func New2() *singleton {
  26. if instance == nil { // 第一次检查(①)
  27. // 这里可能有多于一个 goroutine 同时达到(②)
  28. mutex.Lock()
  29. // 这里每个时刻只会有一个 goroutine(③)
  30. if instance == nil { // 第二次检查(④)
  31. instance = new(singleton)
  32. }
  33. mutex.Unlock()
  34. }
  35. return instance
  36. }
  37. // sync.Once 实现
  38. var once sync.Once
  39. func New3() *singleton {
  40. once.Do(func() {
  41. instance = new(singleton)
  42. })
  43. return instance
  44. }
  • 包级变量变成非导出(instance),注意这里类型应该用指针,因为结构体的默认值不是 nil
  • 提供了实例化函数,按照 Go 的惯例,命名为 New()
  • 多 goroutine 保护,Go 使用 sync.Mutex

此外,Go语言中还有一些其他“黑魔法”,比如利用 init 函数来初始化唯一的单例。不过一般都不太建议,还是常规方式来。

使用场景

在 Go 语言中,一般推荐优先考虑使用饿汉式。但如果初始化比较耗时,懒汉式延迟初始化是更好的选择。
在 Go 语言中,如下两个场景比较适合使用单例模式:

  • 数据库实例:只想创建一个 DB 对象实例,该实例在整个应用程序中使用。
  • 日志实例:同样,只创建一个 Logger 的实例,并且在整个应用程序中使用它。

参考文章