前言
最近在写项目的时候用到单例模式,之前只是简单的使用。在 C++ 中,实现单例模式的方法有多种,今天对单例模式做一个深度的总结与对比,将介绍单例模式的原理、实现方法以及各种方法的优缺点。
一、单例模式的原理
单例模式是一种常用的设计模式,用于保证一个类只有一个实例存在,并提供一个全局访问点。
单例模式的核心思想是让一个类只能创建一个实例,并提供一个全局访问点,以便在程序中任何地方都可以访问该实例。在 C++ 中,可以通过以下步骤来实现单例模式:定义静态成员变量的目的是为了保存单例对象,使得工厂方法能够返回这个唯一的实例。而将默认构造函数设为私有,则是为了防止外部程序随意创建单例对象。
将类的构造函数设为私有,禁止外部创建实例。
定义一个静态成员变量,用于保存单例对象。
构造一个全局的工厂方法,用于获取单例对象。
简单例子
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // 创建静态变量保存唯一实例
return instance;
}
void printMessage() {
std::cout << "Hello, World!" << std::endl;
}
private:
Singleton() {} // 将构造函数设为私有,禁止外部创建实例
Singleton(const Singleton&) = delete; // 禁止拷贝构造
Singleton& operator=(const Singleton&) = delete; // 禁止赋值操作
};
int main() {
Singleton& instance1 = Singleton::getInstance();
Singleton& instance2 = Singleton::getInstance();
std::cout << std::boolalpha << (&instance1 == &instance2) << std::endl;
instance1.printMessage(); // 输出 "Hello, World!"
instance2.printMessage(); // 输出 "Hello, World!"
return 0;
}
在上面的例子中,Singleton 类的构造函数被设为私有,禁止外部创建实例。getInstance() 函数返回一个静态的 Singleton 类型变量,用于保存类的唯一实例。由于该变量是静态的,因此只会在程序第一次调用 getInstance() 函数时被创建。
在 main() 函数中,我们分别通过 Singleton::getInstance() 函数获取两个 Singleton 类型的实例 instance1 和 instance2。由于 Singleton 类只有一个实例,因此我们可以使用地址比较运算符 & 来验证它们是否是同一个实例。最后,我们通过 instance1 和 instance2 分别调用 Singleton 类的成员函数 printMessage(),输出 “Hello, World!”。
二、实现单例模式的几种方法
单例模式有两种类型:
懒汉式:在真正需要使用对象时才去创建该单例类对象
饿汉式:在类加载时已经创建好该单例对象,等待被程序使用
1.加锁的懒汉式
加锁的懒汉式是指在第一次调用 getInstance() 函数时,通过加锁的方式来保证线程安全。如果实例还没有被创建,则创建实例并返回实例的指针;如果实例已经被创建,则直接返回实例的指针。以下是一个加锁的懒汉式的实现:
class Singleton {
public:
static Singleton* getInstance() {
if (instance == nullptr) {
std::lock_guard<std::mutex> lock(mutex);
if (instance == nullptr) {
instance = new Singleton();
}
}
return instance;
}
void printMessage() {
std::cout << "Hello, World!" << std::endl;
}
private:
Singleton() {}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton* instance;
static std::mutex mutex;
};
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mutex;
int main() {
Singleton* instance1 = Singleton::getInstance();
Singleton* instance2 = Singleton::getInstance();
std::cout << std::boolalpha << (instance1 == instance2) << std::endl;
instance1->printMessage(); // 输出 "Hello, World!"
instance2->printMessage(); // 输出 "Hello, World!"
return 0;
}
instance 和 mutex 都是静态的成员变量。getInstance() 函数首先检查 instance 是否为 nullptr,如果是则加锁,再次检查 instance 是否为 nullptr,如果是则创建实例并返回实例的指针。如果 instance 不为 nullptr,则直接返回实例的指针。由于加锁操作可以保证线程安全,因此该实现方法可以在多线程环境下使用。
在这段代码中,为了实现单例模式并保证线程安全,使用了两次判断为空的机制。
第一次为空判断是为了提高性能,减少不必要的锁开销。当第一个线程访问getInstance()方法时,会检查实例是否已经存在,如果是空的,才会进行加锁操作。这样可以避免每次调用getInstance()方法时都进行加锁,提高了代码的效率和性能。
然而,如果只有这一个判断,当多个线程同时进入这个判断时,可能会导致重复创建实例的问题。所以,在加锁之后进行了第二次判断为空,这样能够确保只有一个线程能够成功创建实例,其他线程在加锁之后,会发现实例已经存在,就不会再次创建实例了。
通过这种两次判断为空的方式,可以保证在多线程环境下,只有一个实例被创建,并且避免了竞争条件和不必要的资源消耗。
2.局部变量的懒汉式
局部变量的懒汉式是指在第一次调用 getInstance() 函数时,通过定义一个局部静态变量来保证线程安全和实例的唯一性。局部静态变量的生命周期与程序的生命周期相同,因此可以保证实例的唯一性。以下是一个局部变量的懒汉式的实现:
class Singleton {
public:
static Singleton* getInstance() {
static Singleton instance;
return &instance;
}
void printMessage() {
std::cout << "Hello, World!" << std::endl;
}
private:
Singleton() {}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
int main() {
Singleton* instance1 = Singleton::getInstance();
Singleton* instance2 = Singleton::getInstance();
std::cout << std::boolalpha << (instance1 == instance2) << std::endl;
instance1->printMessage(); // 输出 "Hello, World!"
instance2->printMessage(); // 输出 "Hello, World!"
return 0;
}
在上面的代码中,instance 是 getInstance() 函数中的一个局部静态变量,用于保存实例。由于局部静态变量的生命周期与程序的生命周期相同,因此可以保证实例的唯一性。由于局部静态变量的初始化是线程安全的,因此该实现方法可以在多线程环境下使用。
该实现方法的优点在于可以保证线程安全和实例的唯一性,缺点在于无法控制实例的创建时机,即实例会在程序第一次调用 getInstance() 时被创建,而不是在程序启动时就被创建
3.饿汉式
饿汉式是指在程序启动时就创建实例,并将实例定义为静态常量。由于实例在程序启动时就被创建,因此可以保证实例的唯一性和线程安全。以下是一个饿汉式的实现:
class Singleton {
public:
static Singleton* getInstance() {
return &instance;
}
void printMessage() {
std::cout << "Hello, World!" << std::endl;
}
private:
Singleton() {}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton instance;
};
Singleton Singleton::instance;
int main() {
Singleton* instance1 = Singleton::getInstance();
Singleton* instance2 = Singleton::getInstance();
std::cout << std::boolalpha << (instance1 == instance2) << std::endl;
instance1->printMessage(); // 输出 "Hello, World!"
instance2->printMessage(); // 输出 "Hello, World!"
return 0;
}
它在程序启动时就被创建,并且在整个程序生命周期内保持不变。由于实例的创建是在程序启动时完成的,因此可以保证实例的唯一性和线程安全。
该实现方法的优点在于可以保证实例的唯一性和线程安全,缺点在于无法控制实例的创建时机,即实例会在程序启动时就被创建,而不是在第一次调用 getInstance() 时才被创建,可能会影响程序的启动时间和内存使用。
三、go语言实现单例模式
在go语言中实现单例模式也分为饿汉方式和懒汉方式,并且在 Go 语言中,可以通过使用 package 的特性,结合 sync.Once 实现单例模式。下面逐个进行介绍。
1、懒汉式
懒汉式在平时使用比较多,但是在并发环境中他是不安全的,需要进行加锁。
普通版本
package singleton
type School struct {}
var (
instance *School
lock *sync.Mutex = &sync.Mutex{}
)
func GetInstance() *School {
lock.Lock()
defer lock.Unlock()
if instance == nil {
instance = new(School)
}
return instance
}
双重检查锁定
首先检查 instance 变量是否为 nil,如果是,则获取锁并再次检查 instance 变量是否为 nil。如果 instance 变量仍然为 nil,才会创建新的对象并赋值给 instance 变量
func GetInstance() *School {
if instance == nil {
lock.Lock()
defer lock.Unlock()
if instance == nil {
instance = new(School)
}
}
return instance
}
2、饿汉式
直接创建对象,线程安全。
package singleton
type School struct{}
var (
instance *School
)
func init(){
instance = new(School)
}
func GetInstance() *School {
return instance
}
3、采用Once实现单例模式
当第一个 Goroutine 调用 GetInstance 函数时,once.Do 方法会执行传入的函数,即创建一个新的 Singleton 对象,并将其赋值给 instance 变量。而后续的 Goroutine 调用 GetInstance 函数时,由于 once.Do 方法已经被调用过了,因此不会再次执行创建对象的代码,而是直接返回已经创建好的 instance 对象。
package singleton
import "sync"
var (
instance *Singleton
once sync.Once
)
type Singleton struct {
// 单例对象的属性和方法
}
func GetInstance() *Singleton {
once.Do(func() {
instance = &Singleton{}
})
return instance
}