首页 > 其他分享 >单例模式线程安全reorder问题

单例模式线程安全reorder问题

时间:2024-06-01 19:33:05浏览次数:14  
标签:std Singleton nullptr instance 线程 单例 mutex reorder

单例模式是一种常用的软件设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。下面是一个使用C++实现的线程安全的单例模式的例子:
class Singleton {
private:
static std::atomic<Singleton*> instance; // 静态私有实例指针使用原子操作类atomic线程安全
static std::mutex mutex; // 静态互斥锁

// 私有构造函数,防止外部直接创建实例,只能内部调用 相当于只会创建一个
Singleton() {
    std::cout << "Singleton instance created." << std::endl;
}

// 禁止复制构造函数和赋值操作符
Singleton(const Singleton& other) = delete;
Singleton& operator=(const Singleton& other) = delete;

public:
// 静态公有方法,提供全局访问点
static Singleton* getInstance() {
// 双重检查锁定模式,减少锁的使用
if (instance == nullptr) {
std::lock_guardstd::mutex lock(mutex);
if (instance == nullptr) { // 再次检查,确保线程安全
instance = new Singleton();
}
}
return instance;
}

// 示例方法
void doSomething() {
    std::cout << "Doing something." << std::endl;
}

// 销毁单例实例的方法(通常在程序结束时调用)
static void destroyInstance() {
    delete instance;
    instance = nullptr;
    std::cout << "Singleton instance destroyed." << std::endl;
}

};

// 初始化静态成员变量
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mutex;

getInstance方法返回的一个对象在单线程是绝对安全的,但是在多线程的情况下,我们就不一定了。加入线程A访问了if (instance == nullptr)判断instance为空,这个时候时间片结束了,线程B执行那么线程B也判断instance为空,那么就会new一个新的instance,再回到线程A的时候继续执行,就会创建另外一个新的instance,就违背了单例模式的初衷。这个时候就可以用锁或者是volatile关键字或者是atomic原子操作类模板。
假如我们使用的是锁的机制来确保线程安全的话第一种getInstance方法就是:
static Singleton* getInstance() {
// 双重检查锁定模式,减少锁的使用
std::lock_guardstd::mutex lock(mutex);
if (instance == nullptr) {
instance = new Singleton();
}
return instance;
}
我们在判断instance为空前加锁,这个时候的确可以确保线程安全,但是开销太大,此时这个线程结束了,那第二个线程进来获取instance的时候就会阻塞拿不到锁,导致整体的效率变低。

第二种方法使用volatile关键字来修饰我们的volatile static Singleton* instance ,这种方法是让编译器不要进行优化,每次使用实例的时候都从内存里面去取出来,而不是使用存在寄存器内的副本。但是他不能保证复合操作的原子性。
第三种就是双检查锁相比于第一种的检查锁,我们在判断条件之后进行加锁:
static Singleton* getInstance() {
// 双重检查锁定模式,减少锁的使用
if (instance == nullptr) {
std::lock_guardstd::mutex lock(mutex);
if (instance == nullptr) { // 再次检查,确保线程安全
instance = new Singleton();
}
}
return instance;
}
为什么要判断两次呢? 因为第一次如果线程A和线程B都进入了这行代码都判断为空,然后线程A拿到锁创建了新的instance返回,线程A结束了。但是线程B已经判断为空,B开始执行就去拿锁,拿到锁就直接创建一个新的instance没有判断是不是已经有其他线程创建了新的对象。这个时候加上二次判断就可以保证返回的只有一个实例。
但是这样也会造成一个问题也就是:内存读写reorder问题
这个问题是什么呢?其实很简单。
在我们的内存创建一个新的对象的时候,是首先在内存开辟新的内存空间,然后调用构造函数,然后在返回内存地址给一个指针。这里分为三步走,但是reorder就有可能会把第二步和第三步交换。先返回一个内存地址,但是这个时候还没有进行第三步构造函数还没调用呢,成员变量都没初始化是一个空的内存空间。这个时候就容易出现问题,有的线程判断了instance为空,但是拿到的对象其实是空的没有初始化。也就被编译器给欺骗了。那我们该如何解决这个问题呢?在之前遇到这个问题是没有办法的,但是C++11之后引入了std::atomic模板类我们就可以解决这个问题,下面是新的getInstance方法的代码:
std::atomic<Singleton*> Singleton::m_instance;
std::mutex Singleton::m_mutex;

Singleton* Singleton::getInstance() {
Singleton* tmp = m_instance.load(std::memory_order_relaxed);
//确保内存不会被reorder优化
std::atomic_thread_fence(std::memory_order_acquire);//获取内存fence
if (tmp == nullptr) {
std::lock_guardstd::mutex lock(m_mutex);
tmp = m_instance.load(std::memory_order_relaxed);
if (tmp == nullptr) {
tmp = new Singleton;
//配套使用
atomic_thread_fence(std::memory_order_release);//释放内存fence
m_instance.store(tmp, std::memory_order_relaxed);
}
}
return tmp;
}
这个时候就是一个真正的线程安全的单例模式。

标签:std,Singleton,nullptr,instance,线程,单例,mutex,reorder
From: https://www.cnblogs.com/chhblogs/p/18226293

相关文章

  • [转帖]在Linux上查看活跃线程数与连接数
     https://www.cnblogs.com/xibuhaohao/p/11413669.html 原创:扣钉日记(微信公众号ID:codelogs),欢迎分享,非公众号转载保留此声明。简介现如今,有两种常见的软件资源几乎成了Java后端程序的标配,即线程池与连接池,但这些池化资源非常的重要,一旦不够用了,就会导致程序阻塞、性能......
  • Linux系统编程之线程
    一.线程介绍1.进程与线程典型的UNIX/Linux进程可以看成只有一个控制线程:一个进程在同一时刻只做一件事情。有了多个控制线程后,在程序设计时可以把进程设计成在同一时刻做不止一件事,每个线程各自处理独立的任务。进程是程序执行时的一个实例,是担当分配系统资源(CP......
  • Java多线程编程:提高程序性能与响应性
            多线程编程是利用计算机的多核心优势来提高程序的性能和响应性的重要手段之一。在Java中,通过多线程可以实现同时执行多个任务,充分利用CPU资源,加速程序的运行。本文将深入探讨Java多线程编程的基本概念、常用类库、并发问题以及最佳实践。####1.多线程基础概......
  • 总结常用9种下载(限速、多线程加速、ZIP、导Excel)
    一、前言下载文件在我们项目很常见,有下载视频、文件、图片、附件、导出Excel、导出Zip压缩文件等等,这里我对常见的下载做个简单的总结,主要有文件下载、限速下载、多文件打包下载、URL文件打包下载、Excel导出下载、Excel批量导出Zip包下载、多线程加速下载。二、搭建SpringBoo......
  • 在进程中使用条件量和互斥锁实现线程的同步以及临界资源的互斥访问
    /******************************************************************** author :北极甜虾呦* date :2024/06/01* function:进程中使用条件量和互斥锁实现线程的同步以及临界资源的互斥访问* note :None* CopyRight(c)[email protected]......
  • 5种方法,教你判断线程池是否全部完成
    5种方法,教你判断线程池是否全部完成前言isTerminated方式getCompletedTaskCountCountDownLatch计数器CountDownLatch概述维护一个公共计数Future判断任务执行状态前言最近写代码的时候用到了CountDownLatch计数器,然后顺便想了想判断线程池全部结束有多少种方......
  • 系统编程练习题----利用条件量和互斥锁,实现两个线程之间的同步与互斥
    目录题目解析代码结果展示题目解析​ 该题主要依靠条件量和互斥锁来实现线程之间的同步与互斥,分析主线程、线程A和线程B的任务如下:主线程:打开LCD屏和触摸屏的硬件文件,并分别存储两个文件的文件描述符,方便后面进行条件判断。开启线程A和线程B。定义并初始化条件量和互斥......
  • 使用条件量和互斥锁实现线程的同步以及临界资源的互斥访问
    /********************************************************************** name :* function:主线程需要创建2个子线程之后主线程终止,此时进程中有2个子线程A和B,此时进程中有一个临界资源fag,子线程A获取触摸屏坐标并判断坐标值是否在LCD屏的左上角,如果坐标范围满足......
  • 【自定义线程池】超详细!一文轻松理解JDK线程池 - java
    【自定义线程池】超详细!一文轻松理解JDK线程池-java通过手敲一遍自定义线程池代码,轻松理解jdk中线程池的原理,可以放心告诉面试官研究过jdk线程池源码!本文参考b站黑马程序员满一航老师的JUC课程p200-208https://www.bilibili.com/video/BV16J411h7Rd?p=207&vd_sour......
  • 说说你对单例模式的理解?如何实现?
     一、是什么单例模式(SingletonPattern):创建型模式,提供了一种创建对象的最佳方式,这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建在应用程序运行期间,单例模式只会在全局作用域下创建一次实例对象,让所有需要调用的地方都共享这一单例对象,如下图所......