为什么Meyers’ Singleton是线程安全的

定义


        单例是一种创建型设计模式,让你能够保证一个类只有一个实例,并提供一个访问该实例的全局节点。

背景

单例模式同时解决了两个问题,所以违反了单一职责原则:

  • 保证一个类只有一个实例。
  • 为该实例提供一个全局访问节点。

        为什么会有人想要控制一个类所拥有的实例数量?最常见的原因是控制某些共享资源(例如数据库或文件)的访问权限。它的运作方式是这样的:如果你创建了一个对象,同时过一会儿后你决定再创建一个新对象,此时你会获得之前已创建的对象,而不是一个新对象。

        注意,普通构造函数无法实现上述行为,因为构造函数的设计决定了它必须总是返回一个新对象。

        还记得你用过的那些存储重要对象的全局变量吗?它们在使用上十分方便,但同时也非常不安全,因为任何代码都有可能覆盖掉那些变量的内容,从而引发程序崩溃。和全局变量一样,单例模式也允许在程序的任何地方访问特定对象。但是它可以保护该实例不被其他代码覆盖。

还有一点:你不会希望解决同一个问题的代码分散在程序各处的。因此更好的方式是将其放在同一个类中,特别是当其他代码已经依赖这个类时更应该如此。

 解决方案

所有单例的实现都包含以下两个相同的步骤:

  • 将默认构造函数设为私有, 防止其他对象使用单例类的new运算符。
  • 新建一个静态构建方法作为构造函数。该函数会“偷偷”调用私有构造函数来创建对象,并将其保存在一个静态成员变量中。此后所有对于该函数的调用都将返回这一缓存对象。

如果你的代码能够访问单例类,那它就能调用单例类的静态方法。无论何时调用该方法,它总是会返回相同的对象。

结构

单例(Singleton) 类声明了一个名为getInstance 获取实例的静态方法来返回其所属类的一个相同实例。

单例的构造函数必须对客户端(Client) 代码隐藏。调用获取实例方法必须是获取单例对象的唯一方式。

适用场景

  • 如果程序中的某个类对于所有客户端只有一个可用的实例,可以使用单例模式。单例模式禁止通过除特殊构建方法以外的任何方式来创建自身类的对象。该方法可以创建一个新对象,但如果该对象已经被创建,则返回已有的对象。
  • 如果你需要更加严格地控制全局变量,可以使用单例模式。单例模式与全局变量不同,它保证类只存在一个实例。除了单例类自己以外,无法通过任何方式替换缓存的实例。请注意, 你可以随时调整限制并设定生成单例实例的数量,只需修改获取实例方法, 即getInstance 中的代码即可实现。

实现方式


在类中添加一个私有静态成员变量用于保存单例实例。
声明一个公有静态构建方法用于获取单例实例。
在静态方法中实现"延迟初始化"。该方法会在首次被调用时创建一个新对象,并将其存储在静态成员变量中。此后该方法每次被调用时都返回该实例。
将类的构造函数设为私有。类的静态方法仍能调用构造函数,但是其他对象不能调用。
检查客户端代码,将对单例的构造函数的调用替换为对其静态构建方法的调用。


优点

  • 你可以保证一个类只有一个实例。
  • 你获得了一个指向该实例的全局访问节点。
  • 仅在首次请求单例对象时对其进行初始化。

缺点

  • 违反了单一职责原则。该模式同时解决了两个问题。
  • 单例模式可能掩盖不良设计,比如程序各组件之间相互了解过多等。
  • 该模式在多线程环境下需要进行特殊处理,避免多个线程多次创建单例对象。
  • 单例的客户端代码单元测试可能会比较困难,因为许多测试框架以基于继承的方式创建模拟对象。由于单例类的构造函数是私有的,而且绝大部分语言无法重写静态方法,所以你需要想出仔细考虑模拟单例的方法。要么干脆不编写测试代码,或者不使用单例模式。
  • 懒汉单例模式代码

1. 线程不安全的懒汉单例模式

注意懒汉模式在不加锁情况下是线程不安全的。
Singleton.h:

  • 构造函数私有:即单例模式只能在内部私有化
  • 实例对象static:保证全局只有一个
  • 外界通过GetInstance()获取实例对象
#ifndef SINGLETON_H_
#define SINGLETON_H_

#include <iostream>
#include <string>

class Singleton {
 public:
    static Singleton* GetInstance() {
        if (instance_ == nullptr) {
            instance_ = new Singleton();
        }
        return instance_;
    }
 private:
    Singleton() {}
    static Singleton* instance_;
};

#endif  // SINGLETON_H_


Singleton.cpp:

#include "Singleton.h"

// 静态变量instance初始化不要放在头文件中, 如果多个文件包含singleton.h会出现重复定义问题
Singleton* Singleton::instance_ = nullptr;


main.cpp:

#include <iostream>
#include "Singleton.h"

int main() {
    Singleton *s1 = Singleton::GetInstance();
    Singleton *s2 = Singleton::GetInstance();

    std::cout << "s1地址: " << s1 << std::endl;
    std::cout << "s2地址: " << s2 << std::endl;
    return 0;
}


编译运行:

$g++ -g main.cpp Singleton.cpp -std=c++11 -o singleton
$./singleton 
s1地址: 0x95a040
s2地址: 0x95a040



2. 线程安全的懒汉单例模式

上述代码并不是线程安全的,当多个线程同时调用Singleton::GetInstance(),可能会创建多个实例从而导致内存泄漏(会new多次但我们只能管理唯一的一个instance_),我们这里简单通过互斥锁实现线程安全。

Singleton.h:

#include <iostream>
#include <string>
#include <mutex>

class Singleton {
 public:
    static Singleton* GetInstance() {
        if (instance_ == nullptr) {
            // 加锁保证多个线程并发调用getInstance()时只会创建一个实例
            // std::lock_guard<std::mutex> lock(mutex); 
            // 使用lock_guard加锁
            m_mutex_.lock();
            if (instance_ == nullptr) {
                instance_ = new Singleton();
            }
            m_mutex_.unlock();
        }
        return instance_;
    }
 private:
    Singleton() {}
    static Singleton* instance_;
    static std::mutex m_mutex_;
};


Singleton.cpp:

#include "Singleton.h"

// 静态变量instance初始化不要放在头文件中, 如果多个文件包含singleton.h会出现重复定义问题
Singleton* Singleton::instance_ = nullptr;
std::mutex Singleton::m_mutex_;


main.cpp:

#include <iostream>
#include "Singleton.h"

int main() {
    Singleton *s1 = Singleton::GetInstance();
    Singleton *s2 = Singleton::GetInstance();

    std::cout << "s1地址: " << s1 << std::endl;
    std::cout << "s2地址: " << s2 << std::endl;
    return 0;
}


饿汉单例模式代码

Singleton.h:

class Singleton {
 public:
    static Singleton* GetInstance() {
        return instance_;
    }

 private:
    Singleton() {}
    static Singleton* instance_;
};


Singleton.cpp:

#include "Singleton.h"

Singleton* Singleton::instance_ = new Singleton();


main.cpp:

#include <iostream>
#include "Singleton.h"

int main() {
    Singleton *s1 = Singleton::GetInstance();
    Singleton *s2 = Singleton::GetInstance();

    std::cout << "s1地址: " << s1 << std::endl;
    std::cout << "s2地址: " << s2 << std::endl;
    return 0;
}


编译运行:

$g++ -g main.cpp Singleton.cpp -std=c++11 -o singleton
$./singleton 
s1地址: 0x18a8040
s2地址: 0x18a8040


Meyers’ Singleton

Meyers’ Singleton是Scott Meyers提出的C++单例的推荐写法。它将单例对象作为局部static对象定义在函数内部:

class Singleton {
 public:
    static Singleton& GetInstance() {
        static Singleton instance;
        return instance;
    }
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

 private:
    Singleton() {}
};


优点

● 解决了普通单例模式全局变量初始化依赖(C++只能保证在同一个文件中声明的static遍历初始化顺序和其遍历声明的顺序一致,但是不能保证不同文件中static遍历的初始化顺序)

缺点

● 需要C++11支持(C++11保证static成员初始化的线程安全)
● 性能问题(同懒汉模式一样,每次调用GetInstance()方法时需要判断局部static变量是否已经初始化,如果没有初始化就会进行初始化,这个判断逻辑会消耗一点性能)

为什么是线程安全的?

对于以上单例模式代码,在 C++11(及更高版本)中,函数局部静态AppSettings的构造保证是线程安全的。

编译器将在Singleton 旁边放置一个隐藏标志,指示它的状态:

  • Not constructed.
  • Being constructed.
  • Is constructed.

第一个线程将发现标志设置为“Not constructed”并尝试构造该对象。成功构建后,标志将设置为“Is constructed”。如果另一个线程出现并发现标志设置为“Being constructed”,它将等待,直到标志设置为“Is constructed”。

如果构造因异常而失败,则标志将设置为“Not constructed”,并且将在下一次传递时重试构造(在同一线程或不同线程上)。

对象实例将在程序的其余部分中保持构造状态,直到main() 返回,此时实例将被销毁。

每次任何执行线程通过Singleton::GetInstance() 时,它将引用完全相同的对象。

在 C++98/03 中,不保证构造是线程安全的。

如果 Singleton 的构造函数递归地进入Singleton​​​​​​​::GetInstance(),则行为未定义。

如果编译器可以“在编译时”了解如何构造实例,则允许这样做。

如果 Singleton 有一个 constexpr 构造函数(用于构造实例的构造函数),并且该实例是用 constexpr 限定的,则编译器需要在编译时构造实例。如果实例是在编译时构造的,则“未构造/构造”标志将被优化掉。

  • 14
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

**K

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值