《剑指offer》02--实现Singleton模式[C++]

论坛 期权论坛 编程之家     
选择匿名的用户   2021-6-2 20:58   3632   0

Singleton(单例)模式

特点

  1. 保证一个类只创建一个实例。
  2. 提供对该实例的全局访问点。

应用

  • 日志类,一个应用往往只对应一个日志实例。
  • 配置类,应用的配置集中管理,并提供全局访问。
  • 管理器,比如windows系统的任务管理器就是一个例子,总是只有一个管理器的实例。
  • 共享资源类,加载资源需要较长时间,使用单例可以避免重复加载资源,并被多个地方共享访问。

原则

  1. 构造私有。
  2. 以静态方法或者枚举返回实例。
  3. 确保实例只有一个,尤其是多线程环境。
  4. 确保反序列化时不会重新构建对象。

常用解法

Lazy Singleton,Eager Singleton,Meyers Singleton,Double-Checked Locking Pattern,静态内部类实现单例,etc.

1 Lazy Singleton:只适用于单线程环境

//头文件中
class Singleton {
public:
    static Singleton& Instance() {
        if (instance_ == NULL) instance_ = new Singleton();
        return *instance_;
    }
private:
    Singleton();
    ~Singleton();
    Singleton(const Singleton&);
    Singleton& operator=(const Singleton&);

    static Singleton* instance_; //static成员需在类外初始化
};

// Singleton实例初始化
Singleton* Singleton::instance_ = 0; //NULL = 0, 前面不能加static,会和类外全局static混淆

实现中构造函数被声明为私有方法,这样从根本上杜绝外部使用构造函数生成新的实例,同时禁用拷贝函数与赋值操作符(声明为私有但是不提供实现)避免通过拷贝函数或赋值操作生成新实例。

提供静态方法Instance()作为实例全局访问点,该方法中先判断有没有现成的实例,如果有直接返回,如果没有则生成新实例并把实例的指针保存到私有的静态属性中。

注意,这里Instance()返回的是实例的引用而不是指针,如果返回的是指针可能会有被外部调用者delete掉的隐患,所以这里返回引用会更加保险一些。并且直到Instance()被访问,才会生成实例,这种特性被称为延迟初始化(Lazy initialization),这在一些初始化时消耗较大的情况有很大优势。

Lazy Singleton不是线程安全的,比如现在有线程A和线程B,都通过instance_ == NULL的判断,那么线程A和B都会创建新实例。单例模式保证生成唯一实例的规则被打破了。

2 Eager Singleton

//头文件中
class Singleton {
public:
    static Singleton& Instance() {
        return instance;
    }
private:
    Singleton();
    ~Singleton();
    Singleton(const Singleton&);
    Singleton& operator=(const Singleton&);

    static Singleton instance;
}

// Singleton实例初始化,前面也可以定义为指针(此时需要new Singleton())
Singleton Singleton::instance; // 前面不能加static,会和类外全局static混淆

这种实现在程序开始(静态属性instance初始化)时就完成了实例的创建。这正好和上述的Lazy Singleton相反。

由于在main函数之前初始化,所以没有线程安全的问题,但是潜在问题在于no-local static对象(函数外的static对象)在不同编译单元(可理解为cpp文件和其包含的头文件)中的初始化顺序是未定义的。如果在初始化完成之前调用 Instance()方法会返回一个未定义的实例。假设调用该静态函数可以实现单例模式,仍然会因为过早地创建实例,从而降低内存的使用效率。

3 Meyers Singleton

它其实是使用了C++中成员函数的静态变量的特点:静态局部变量在第一次使用时初始化,并不会销毁直到程序退出。


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

Scott Meyers使用local static对象(函数内的static对象)。当第一次访问Instance()方法时才创建实例。C++0x之后是该实现线程安全的,编译器支持程度不一定,G++4.0及以上是支持的。

4 懒汉模式加锁:支持多线程,但效率不高

//头文件中
class Singleton {
public:
    static Singleton& Instance() {
        Lock lock;
        if (instance_ == NULL) instance_ = new Singleton(); 
        return *instance_;
    }
private:
    Singleton();
    ~Singleton();
    Singleton(const Singleton&);
    Singleton& operator=(const Singleton&);

    static Singleton* instance_; //static成员需在类外初始化
};

// Singleton实例初始化
Singleton* Singleton::instance_ = 0; //NULL = 0, 前面不能加static,会和类外全局static混淆

5 Double-Checked Locking Pattern

Lazy Singleton的一种线程安全改造是在Instance()中每次判断是否为NULL前加锁,但是加锁是很慢的。
而实际上只有第一次实例创建的时候才需要加锁。双检测锁模式被提出来,改造之后大致是这样

class Singleton {
public:
    static Singleton& Instance() {
        if (instance_ == NULL) {
            Lock lock; //基于作用域的加锁,超出作用域,自动调用析构函数解锁
            if (instance_ == NULL) instance_ = new Singleton();
        }
        return *instance_;
    }
private:
    Singleton();
    ~Singleton();
    Singleton(const Singleton&);
    Singleton& operator=(const Singleton&);
   
    static Singleton* instance_; //static成员需在类外初始化
};

// Singleton实例初始化
Singleton* Singleton::instance_ = 0; //NULL = 0, 前面不能加static,会和类外全局static混淆

既然只需要在第一次初始化的时候加锁,那么在这之前判断一下实例有没有被创建就可以了,所以多在加锁之前多加一层判断,需要判断两次所有叫Double-Checked。理论上问题解决了,但是在实践中有很多坑,如指令重排、多核处理器等问题让DCLP实现起来比较复杂比如需要使用内存屏障。

6 静态内部类实现单例:按需创建

class Singleton {
public:
    static Singleton& Instance() {
        return *Nested->instance_;
    }
private:
    Singleton();
    ~Singleton();
    Singleton(const Singleton&);
    Singleton& operator=(const Singleton&);

    class Nested {
    private:
        static Nested();
        ~Nested();
        static Singleton* instance_ = new Singleton(); 
    };
    static Nested Nested;
};

静态内部类的优点是:外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化instance_,故而不占内存。

分享到 :
0 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:3875789
帖子:775174
精华:0
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP