设计模式—单例模式深度分析与实现【c++与golang】


前言

最近在写项目的时候用到单例模式,之前只是简单的使用。在 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
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值