• C++ Singleton模式


    C++ Singleton

    Lazy Singleton

    C++11的一个Lazy Singleton(懒汉)版本:

    class Singleton {
    public:
        Singleton(const Singleton&) = delete;
        Singleton& operator=(const Singleton&) = delete;
    
        static Singleton* getInstance() {
            if (!instance) {
                instance = new Singleton();
            }
            return instance;
        }
    
    private:
        Singleton() {}
        static Singleton* instance;
    };
    
    Singleton* Singleton::instance = 0;

    Lazy Singleton存在2个问题:

    1. 内存泄露(instance如何释放?);
    2. 非线程安全;

    内存释放

    线程安全

    能在多线程环境下实现单例模式,我们首先想到的是利用加锁来正确的保护我们的shared data,如下

    class Singleton {
    public:
        Singleton(const Singleton&) = delete;
        Singleton& operator=(const Singleton&) = delete;
    
        static Singleton* getInstance() {
            if (!instance) {
                std::lock_guard<std::mutex> lockGuard(lock);
                instance = new Singleton();
            }
            return instance;
        }
    
    private:
        Singleton() {}
        static Singleton* instance;
        static std::mutex lock;
    };
    
    Singleton* Singleton::instance = 0;
    std::mutex Singleton::lock;

    看起来不错哦,应该没有问题了吧?但如果有多个线程同时通过 if(!instance) 的条件检查(因为他们并行运行),虽然我们的lock_guard会帮助我们同步所有的线程,让我们并行线程变成串行的一个一个去new,那不还是一样的吗?同样会出现很多实例。

    这里可以使用双检测锁模式(DCL: Double-Checked Locking Pattern):

        static Singleton* getInstance() {
            if (!instance) {
                std::lock_guard<std::mutex> lockGuard(lock);
                if (!instance) {
                    instance = new Singleton();
                }
            }
            return instance;
        }

    注意,加锁仅在第一次初始化(new)过程中,而在后面获取该实例的时候并不会再遇到,也就没有必要再使用lock。

    双检测锁很好地解决了这个问题,它通过加锁前检测是否已经初始化,避免了每次获取实例时都要首先获取锁资源。

    加入DCL后,其实还是有问题的,new运算符并不是原子操作,它分为三步:1、申请内存;2、调用构造函数;3、返回指针;

    由于编译器的优化以及运行时优化等等原因,最终的执行顺序可能是 1-2-3 ,也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

    我们可以再改为原子操作:

    class Singleton {
    public:
        Singleton(const Singleton&) = delete;
        Singleton& operator=(const Singleton&) = delete;
    
        static Singleton* getInstance() {
            if (!instance) {
                std::lock_guard<std::mutex> lockGuard(lock);
                if (!instance) {
                    instance.store(new Singleton());
                }
            }
            return instance.load();
        }
    
    private:
        Singleton() {}
        static atomic<Singleton*> instance;
        static std::mutex lock;
    };
    
    atomic<Singleton*> Singleton::instance(NULL);
    std::mutex Singleton::lock;

    局部static变量

    local static 对象(函数内)

    对于local static 对象,其初始化发生在控制流第一次执行到该对象的初始化语句时。多个线程的控制流可能同时到达其初始化语句。

    在C++11之前,在多线程环境下local static对象的初始化并不是线程安全的。具体表现就是:如果一个线程正在执行local static对象的初始化语句但还没有完成初始化,此时若其它线程也执行到该语句,那么这个线程会认为自己是第一次执行该语句并进入该local static对象的构造函数中。这会造成这个local static对象的重复构造,进而产生内存泄露问题。所以,local static对象在多线程环境下的重复构造问题是需要解决的。

    而C++11则在语言规范中解决了这个问题。C++11规定,在一个线程开始local static 对象的初始化后到完成初始化前,其他线程执行到这个local static对象的初始化语句就会等待,直到该local static 对象初始化完成。

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

     这样,只有当第一次访问getInstance()方法时才创建实例。这种方法也被称为Meyers' Singleton。C++0x之后该实现是线程安全的,C++0x之前仍需加锁。

    Eager Singleton(饿汉版)

    non-local static对象(函数外)

    C++规定,non-local static 对象的初始化发生在main函数执行之前,也即main函数之前的单线程启动阶段,所以不存在线程安全问题。但C++没有规定多个non-local static 对象的初始化顺序,尤其是来自多个编译单元的non-local static对象,他们的初始化顺序是随机的。

    instance 在程序运行时(先于main函数)被立即执行初始化

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

    由于在main函数之前初始化,所以没有线程安全的问题。但是潜在问题在于no-local static对象(函数外的static对象)在不同编译单元中的初始化顺序是未定义的。

    也即,static Singleton instance;和static Singleton& getInstance()二者的初始化顺序不确定,如果在初始化完成之前调用 getInstance() 方法会返回一个未定义的实例。

    Java 版本

    看一个java的实现

    public class Singleton
    {
         private volatile static Singleton singleton = null ;
         private Singleton()  {    }
         public static Singleton getInstance()   {
             if (singleton== null )  {
                 synchronized (Singleton. class ) {
                     if (singleton== null )  {
                         singleton= new Singleton();
                     }
                 }
             }
             return singleton;
         }
    }

    这里没有用原子操作,而是使用 volatile, 它有两个功用:

    1)这个变量不会在多个线程中存在复本,直接从内存读取。

    2)这个关键字会禁止指令重排序优化。也就是说,在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。

    同样,Java的static版本

    public class Singleton
    {
         private volatile static Singleton singleton = new Singleton();
         private Singleton()  {    }
         public static Singleton getInstance()   {
             return singleton;
         }
    }

    当这个类被加载的时候,new Singleton() 这句话就会被执行,就算是getInstance()没有被调用,类也被初始化了。

    参考:

    https://zhuanlan.zhihu.com/p/37469260

    https://blog.csdn.net/haoel/article/details/4028232

  • 相关阅读:
    飞入飞出效果
    【JSOI 2008】星球大战 Starwar
    POJ 1094 Sorting It All Out
    POJ 2728 Desert King
    【ZJOI 2008】树的统计 Count
    【SCOI 2009】生日快乐
    POJ 3580 SuperMemo
    POJ 1639 Picnic Planning
    POJ 2976 Dropping Tests
    SPOJ QTREE
  • 原文地址:https://www.cnblogs.com/chenny7/p/14067331.html
Copyright © 2020-2023  润新知