zoukankan      html  css  js  c++  java
  • 单例模式,reorder详解,线程安全,双检查锁

    单例模式的构造函数是私有的,目的是让用户无法直接new出实例,而只有通过其他的接口来获取实例,单例模式在这里作文章,使得多次获取到的实例,都是同一个实例。

    单例模式,分为饿汉式单例 和 懒汉式单例。

    先把本类对象所需内存在main函数执行前就new出来,这是饿汉式单例。

    个人思考:

    为什么饿汉式不独霸天下,还有什么必要去研究使用cpp11上支持的双检查锁机制(这是懒汉式,用到类实例时才去申请内存)?就因为饿汉式单例事先就占用了一些类内存?反正迟早都要占用内存啊。

    或者说,饿汉式单例有什么缺陷。

     

    饿汉式单例:

     个人理解:

    优点: 编程上使用很简单  

     缺点: 整个程序运行期间会一直占用内存,不可以在程序运行期间将其delete。

    饿汉式单例模式的重要特点是运用了全局对象的构造过程先于main函数执行之前的特点。

    如果程序运行期将实例化的单例对象delete之后,如果有再次创建该单例对象的需求,

    正因为饿汉式单例的上述特点,将无法达到满意的目标效果(目标效果是支持线程安全的单例模式)。

    因为程序不可能重新从main函数前再重头执行一次(在嵌入式平台,只有设备重新上电了)。

    即: 饿汉式单例模式不支持动态创建、销毁单例对象。

    普通的懒汉式单例 动态支持的单例对象的申请和释放

    class Singleton{
    private:
        Singleton();
        Singleton(const Singleton& other);
    public:
        static Singleton* getInstance();
        static Singleton* m_instance;
    };
    //线程安全,但锁的代价过高
    Singleton* Singleton::getInstance() {
        Lock lock;
        if (m_instance == nullptr) {
            m_instance = new Singleton();
        }
        return m_instance;
    }

    普通的懒汉式(该获取实例函数内执行,先上锁,再判断指针,最后分配内存)也是线程安全的,只是其内部实现,即获取实例函数内,不管三七二十一,每次都先上锁,考虑到锁的代码过高,不满足高并发编程要求

    普通的懒汉式单例也适用于我们针对大多数场景使用,因为,大多时候,嵌入式程序员不需要考虑高并发场景。  

    普通的双检查锁

    //普通写法的双检查锁,但由于内存读写reorder, 所以是线程不安全
    Singleton* Singleton::getInstance() {
        
        if(m_instance==nullptr){ 
            Lock lock;
            if (m_instance == nullptr) {  // 这句代码并不是多余的,有其作用
                m_instance = new Singleton();
            }
        }
        return m_instance;
    }

     reorder详解:

    假设某个时刻:

    线程A 执行到m_instance = new Singleton(); 并且已经完成步骤1和 步骤3, 但是步骤2尚未执行,也就是说,虽然此时m_instance已经不是NULL,但是其

    所指向的内存尚未完成构造。 

    此时,线程B被调度,执行getInstance(),进入上述函数内部,先判断if(m_instance==nullptr)(注意,这句代码是未上锁的,所以B线程可以执行),由于此时m_instance已经不是NULL,所以该函数即将退出,

    线程B认为自己已经获取到了该单实例的句柄,接下来就很有可能使用该单实例的句柄进行操作。 显然,这不是线程安全的。

    另外,解释下第二个if判断为什么不是多余的:

    线程A有可能在执行第一个if判断后,立即被调度到线程B执行,而此时线程A尚未执行到Lock lock;的这句上锁代码。m_instance被线程B实例化(完成了单例模式整个过程),

    再次调度回线程A时,线程A继续执行上锁代码,此时,有必要再次判断m_instance指针是否为空,如果已经是非空,则不能执行单例类的构造和赋值。

         

    上述的双检查锁的代码,整体代码逻辑是没问题的,虽然是线程非安全的,但这不是程序员能够解决的了。究其原因,此处线程非安全是因为reorder机制。

    所以,我们程序员需要借助编译器的新特性才能解决该问题。

     

    线程安全的双检查锁 :  从支持C++ 11特性的编译器开始,提供了通用的跨平台实现。

    //线程安全的双检查锁 -- C++ 11版本之后的跨平台实现 (volatile)
    std::atomic<Singleton*> Singleton::m_instance;
    std::mutex Singleton::m_mutex;
    
    Singleton* Singleton::getInstance() {
        Singleton* tmp = m_instance.load(std::memory_order_relaxed);
        std::atomic_thread_fence(std::memory_order_acquire);//获取内存fence
        if (tmp == nullptr) {
            std::lock_guard<std::mutex> lock(m_mutex);
            tmp = m_instance.load(std::memory_order_relaxed);
            if (tmp == nullptr) {
                tmp = new Singleton;
                std::atomic_thread_fence(std::memory_order_release);//释放内存fence
                m_instance.store(tmp, std::memory_order_relaxed);
            }
        }
        return tmp;
    }

    使用cpp11特性支持的双检查方式的懒汉式单例不是必须的,只是这种方式是专用于高并发场景下的,满足高并发要求(ps:这种方式一定是线程安全的)。   

      

     

    补充点:

    全局变量的构造时机,和main函数被执行的时机。

    全局变量的构造,这是crt (c run time)做的事情,它保证全局变量初始化在main之前运行。

     

     

     编写本博客参考过的博客:

    https://www.cnblogs.com/goodAndyxublog/p/11356402.html

     

    .

    /************* 社会的有色眼光是:博士生、研究生、本科生、车间工人; 重点大学高材生、普通院校、二流院校、野鸡大学; 年薪百万、五十万、五万; 这些都只是帽子,可以失败千百次,但我和社会都觉得,人只要成功一次,就能换一顶帽子,只是社会看不见你之前的失败的帽子。 当然,换帽子决不是最终目的,走好自己的路就行。 杭州.大话西游 *******/
  • 相关阅读:
    分解让复杂问题简单化:字符串的排列
    分解让复杂问题简单化:二叉搜索树与双向链表
    分解让复杂问题简单化:复杂链表的复制
    举例让抽象问题具体化:二叉树中和为某一值的路径
    举例让抽象问题具体化:二叉搜索树的后序遍历序列
    Java Collection Framework
    Spring Boot 项目部署到本地Tomcat,出现访问路径问题
    happens-before规则
    NoClassDefFoundError
    《Java编程思想》笔记 第十六章 数组
  • 原文地址:https://www.cnblogs.com/happybirthdaytoyou/p/13665079.html
Copyright © 2011-2022 走看看