【创造型模式】单例模式

文章目录

单例模式

GoF 给出的单例模式的定义:

保证一个类,只有一个实例存在,同时提供能对该实例加以访问的全局访问方法。

其实提到单例模式,只要我们接触过 Web 项目的学习,大概率都会和数据库/Redis 缓存打交道,此时我们就很有可能需要在 main 函数当中初始化一个与数据库对象交互的实例,它通常是一个线程安全的指针,这就是单例模式的一个典型应用,即:提供一个可供客户端使用的唯一对象,该对象仅提供对外操作的接口,并隐藏了初始化等其他细节,该对象应该保证线程安全,实现幂等性。

单例模式中的角色职责

Singleton(单例) :单例类的内部实现只生成一个实例,同时它提供一个静态的getInstance()工厂方法,这是客户端可以访问该类的唯一实例。为了防止在外部对其实例化,将其构造函数设计为私有。在单例类内部定义了一个 Singleton 类型的静态对象,作为外部共享的唯一实例。

一个单例模式的 Demo 如下述代码段所示:

go 复制代码
package main

import "fmt"

/*
	1. 对于单例模式的类而言, 只能有一个实例
	2. 必须自行创建这个实例
	3. 必须向整个系统提供这个实例

	与数据库交互的对象就是典型的单例模式实例
*/

// 1. 保证这个类非公有化, 外界不能通过这个类直接创建对象
//	  因此这个类的定义应该是非导出的(首字母非大写)
type singleton struct {}

// 2. 需要有一个指针指向这个唯一的对象, 且这个指针不能改变引用对象
//	  Golang 中没有常量指针的概念, 所以只能通过将指针私有化来避免外部模块访问
var instance *singleton = new(singleton)

// 3. 必须向整个系统提供这个实例, 通过函数来完成
func GetInstance() *singleton {
	return instance
}

func (s *singleton) SomeThing() {
	fmt.Println("A singleton Method")
}

func main() {
	s := GetInstance()	// 获取指针
	s.SomeThing()
}

《Easy 搞定 Golang 设计模式》中将上述方法命名为"饿汉式"的单例模式,原因是上述代码在初始化单例的唯一指针时,就为其开辟好了一个对象,不会出现线程并发创建,导致多个单例出现,这种模式的缺点也很明显,那就是即使这个对象在业务当中没有使用,也会客观创建一块内存。与之相对的模式是"懒汉式",即"懒创建",只有在第一次调用GetInstance()时才会创建实例:

go 复制代码
/*... ... ...*/

func GetInstance() *singleton {
    if instance == nil {
        instance = new(singleton)
        return instance
    }
    return instance
}

/*... ... ...*/

线程安全的单例模式

上面的"懒汉式"单例调用仍然不是线程安全的设计方式,如果多个线程或协程同时首次调用GetInstance()方法导致多个实例创建,则违背了单例的设计初衷。我们可以使用 Golang 自带的 Mutex,为"懒汉式"单例的创建加锁,从而保证线程安全。

线程安全最大的缺点在于每次调用都需要加锁,在性能上不够高效,具体的实现如下:

go 复制代码
package main

import (
	"fmt"
	"sync"
)

var lock sync.Mutex

type singleton struct{}

var instance *singleton

func GetInstance() *singleton {
	lock.Lock()
	defer lock.Unlock()
	
	if instance == nil {
		return new(singleton)
	} else {
		return instance
	}
}

func (s *singleton) SomeThing() {
	fmt.Println("SomeThing is called")
}

func main() {
	s := GetInstance()
	s.SomeThing()
}

可以借助sync/atomic来进行内存的状态存留做到互斥。atomic可以自动加载和设置标记,代码如下:

go 复制代码
package main

import (
	"fmt"
	"sync"
	"sync/atomic"
)

var initialized uint32
var lock sync.Mutex

type singleton struct{}

var instance *singleton

func GetInstance() *singleton {
	if atomic.LoadUint32(&initialized) == 1 {
		return instance
	}

	lock.Lock()
	defer lock.Unlock()

	if initialized == 0 {
		instance = &singleton{}
		atomic.StoreUint32(&initialized, 1)
	}

	return instance
}

func (s *singleton) SomeThing() {
	fmt.Println("Something has been called")
}

func main() {
	s := GetInstance()
	s.SomeThing()
}

针对上述逻辑,Golang 已经使用Once模块进行了优化:

go 复制代码
func (o *Once) Do(f func()) {
    if atomic.LoadUint32(&o.done) == 1 {
        return
    }
    o.m.Lock()
    defer o.m.Unlock()
    if o.done == 0 {
        defer atomic.StoreUint32(&o.done, 1)
        f()
    }
}

使用Once来实现单例模式:

go 复制代码
package main

import (
	"fmt"
	"sync"
)

var once sync.Once

type singleton struct{}

var instance *singleton

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

func (s *singleton) SomeThing() {
	fmt.Println("Something has been called")
}

func main() {
	s := GetInstance()
	s.SomeThing()
}

单例模式的优缺点

优点:

  • 单例模式提供了对唯一实例的受控访问;
  • 节约系统资源。在系统内存中只存在一个对象。

缺点:

  • 拓展略难,单例模式中没有抽象层;
  • 单例类的职责过重。

适用场景

  • 系统只需要一个实例对象,如系统提供一个唯一的序列号生成器(比如 Snowflake 算法)或资源管理器,或需要考虑资源消耗太大而只允许创建一个对象。
  • 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。
相关推荐
暴躁哥5 小时前
深入理解设计模式:工厂模式、单例模式
单例模式·设计模式·建造者模式·工厂模式
长勺5 小时前
单例模式总结
java·开发语言·单例模式
widder_2 天前
软考中级软件设计师——设计模式篇
单例模式·设计模式
mutianhao10243 天前
Python测试单例模式
python·单例模式·设计模式
CodeWithMe3 天前
【C/C++】探索单例模式:线程安全与性能优化
c++·单例模式
qqxhb4 天前
零基础设计模式——创建型模式 - 单例模式
单例模式·设计模式·饿汉模式·懒汉模式
野原希之助5 天前
多线程(四)
单例模式·饿汉模式·懒汉模式
码农秋5 天前
设计模式系列(04):单例模式(Singleton)
单例模式·设计模式
学习使我变快乐5 天前
C++:单例模式
开发语言·c++·单例模式