当前位置: 首页 > 编程日记 > 正文

【C++】C/C++ 中的单例模式

目录

part 0:单例模式3种经典的实现方式

Meyer's Singleton

Meyers Singleton版本二

Lazy Singleton

Eager Singleton

Testing

part 1:C++之单例模式

动机

实现一[线程不安全版本]

实现二[线程安全,锁的代价过高]

锁机制

实现三[双检查锁,由于内存读写reoder导致不安全]

实现四[C++ 11版本的跨平台实现]

实现五[pthread_once函数]

另外一个版本实现std::call_once & std::once_flag

实现六[c++ 11版本最简洁的跨平台方案]

用模板包装单例

总结

part 2:单例模式

1.传统的单例模式实现

part 3:java之单例模式

介绍

实现

步骤 1

SingleObject.java

步骤 2

SingletonPatternDemo.java

步骤 3

单例模式的几种实现方式

1、懒汉式,线程不安全

实例

2、懒汉式,线程安全

实例

3、饿汉式

实例

4、双检锁/双重校验锁(DCL,即 double-checked locking)

实例

5、登记式/静态内部类

实例

6、枚举

实例


part 0:单例模式3种经典的实现方式

单例模式是一种创建型的设计模式(creational design patterns),使用单例模式进行设计的类在程序中只拥有一个实例(single instance),这个类称为单例类,它会提供一个全局的访问入口(global access point),关于单例模式的讨论可以参考Singleton revisited;基于这两个特点,单例模式可以有以下几种实现:

Meyer's Singleton

Scott MeyersEffective C++Item 4: Make sure that objects are initialized before they're used 里面提出了一种利用 C++ 的 static 关键字来实现的单例模式,这种实现非常简洁高效,它的特点是:

  1. 仅当程序第一次执行到 GetInstance 函数时,执行 instance 对象的初始化;

  2. 在 C++ 11 之后,被 static 修饰的变量可以保证是线程安全的;

    template<typename T>
    class Singleton
    {
    public:static T& GetInstance(){static T instance;return instance;}
    ​Singleton(T&&) = delete;Singleton(const T&) = delete;void operator= (const T&) = delete;
    ​
    protected:Singleton() = default;virtual ~Singleton() = default;
    };

    通过禁用单例类的 copy constructor,move constructor 和 operator= 可以防止类的唯一实例被拷贝或移动;不暴露单例类的 constructor 和 destructor 可以保证单例类不会通过其他途径被实例化,同时将两者定义为 protected 可以让其被子类继承并使用。

    Meyers Singleton版本二

    Meyers Singleton的实现方式基于"static variables with block scope"的自动线程安全特性,非常简单易懂。

    class MeyersSingleton{
    public:static MySingleton& getInstance(){static MySingleton instance;// volatile int dummy{};return instance;}
    private:MySingleton()= default;~MySingleton()= default;MySingleton(const MySingleton&)= delete;MySingleton& operator=(const MySingleton&)= delete;
    };

    Lazy Singleton

    Lazy Singleton 是一种比较传统的实现方法,通过其名字可以看出来它也具有 lazy-evaluation 的特点,但在实现的时候需要考虑线程安全的问题:

    template<typename T, bool is_thread_safe = true>
    class LazySingleton
    {
    private:static unique_ptr<T> t_;static mutex mtx_;
    ​
    public:static T& GetInstance(){if (is_thread_safe == false){if (t_ == nullptr)t_ = unique_ptr<T>(new T);return *t_;}
    ​if (t_ == nullptr){unique_lock<mutex> unique_locker(mtx_);if (t_ == nullptr)t_ = unique_ptr<T>(new T);return *t_;}
    ​}
    ​LazySingleton(T&&) = delete;LazySingleton(const T&) = delete;void operator= (const T&) = delete;
    ​
    protected:LazySingleton() = default;virtual ~LazySingleton() = default;
    };
    ​
    template<typename T, bool is_thread_safe>
    unique_ptr<T> LazySingleton<T, is_thread_safe>::t_;
    ​
    template<typename T, bool is_thread_safe>
    mutex LazySingleton<T, is_thread_safe>::mtx_;

    我们通过模板参数 is_thread_safe 来控制这个类是否是线程安全的,因为在某些场景下我们会希望每个线程拥有一个实例:

    1. is_thread_safe == false,即非线程安全时,我们在 GetInstance 函数中直接判断,初始化并返回单例对象;这里使用了 unique_ptr 防止线程销毁时发生内存泄漏,也可以在析构函数中销毁指针;

    2. is_thread_safe == true 时,我们通过 double-checked locking 来进行检查并加锁,防止单例类在每个线程上都被实例化。

    Eager Singleton

    和 Lazy Singleton 相反,Eager Singleton 利用 static member variable 的特性,在程序进入 main 函数之前进行初始化,这样就绕开了线程安全的问题:

    template<typename T>
    class EagerSingleton
    {
    private:static T* t_;
    ​
    public:static T& GetInstance(){return *t_;}
    ​EagerSingleton(T&&) = delete;EagerSingleton(const T&) = delete;void operator= (const T&) = delete;
    ​
    protected:EagerSingleton() = default;virtual ~EagerSingleton() = default;
    };
    ​
    template<typename T>
    T* EagerSingleton<T>::t_ = new (std::nothrow) T;

    但是它也有两个问题:

    1. 即使单例对象不被使用,单例类对象也会进行初始化;

    2. static initialization order fiasco,即 t_ 对象和 GetInstance 函数的初始化先后顺序是不固定的;

      Testing

      将上面实现的四种 Singleton 分别继承下来作为 functor 传入线程对象进行测试:

      class Foo : public Singleton<Foo>
      {
      public:void operator() (){cout << &GetInstance() << endl;}
      };
      ​
      class LazyFoo : public LazySingleton<LazyFoo, false>
      {
      public:void operator() (){cout << &GetInstance() << endl;}
      };
      ​
      class ThreadSafeLazyFoo : public LazySingleton<ThreadSafeLazyFoo>
      {
      public:void operator() (){cout << &GetInstance() << endl;}
      };
      ​
      class EagerFoo : public EagerSingleton<EagerFoo>
      {
      public:void operator() (){cout << &GetInstance() << endl;}
      };
      ​
      void SingletonTest()
      {thread t1((Foo()));thread t2((Foo()));t1.join();t2.join();this_thread::sleep_for(chrono::milliseconds(100));
      ​t1 = thread((LazyFoo()));t2 = thread((LazyFoo()));t1.join();t2.join();this_thread::sleep_for(chrono::milliseconds(100));
      ​t1 = thread((ThreadSafeLazyFoo()));t2 = thread((ThreadSafeLazyFoo()));t1.join();t2.join();this_thread::sleep_for(chrono::milliseconds(100));
      ​t1 = thread((EagerFoo()));t2 = thread((EagerFoo()));t1.join();t2.join();
      }

      输出结果为:

      0x60d110
      0x60d110
      0x7f92380008c0
      0x7f92300008c0
      0x7f92300008e0
      0x7f92300008e0
      0x1132010
      0x1132010

      可以看到只有第二组非线程安全的 LazySingleton 在两个线程中输出的实例地址是不同的,其它的 Singleton 均是线程安全的。

part 1:C++之单例模式

动机

保证一个类仅有一个实例,并提供一个该实例的全局访问点。 ——《设计模式》GoF

在软件系统中,经常有这样一些特殊的类,必须保证他们在系统中只存在一个实例,才能确保它们的逻辑正确性、以及良好的效率。

所以得考虑如何绕过常规的构造器(不允许使用者new出一个对象),提供一种机制来保证一个类只有一个实例。

应用场景:

  • Windows的Task Manager(任务管理器)就是很典型的单例模式,你不能同时打开两个任务管理器。Windows的回收站也是同理。

  • 如连接池、类工厂、文件系统等。这就是设计模式中的单例模式(Singleton Pattern)。

  • 应用程序的日志应用,一般都可以用单例模式实现,只能有一个实例去操作文件。

  • 读取配置文件,读取的配置项是公有的,一个地方读取了所有地方都能用,没有必要所有的地方都能读取一遍配置。

  • 数据库连接池,多线程的线程池。

    实现一[线程不安全版本]

    class Singleton{
    public:static Singleton* getInstance(){// 先检查对象是否存在if (m_instance == nullptr) {m_instance = new Singleton();}return m_instance;}
    private:Singleton(); //私有构造函数,不允许使用者自己生成对象Singleton(const Singleton& other);static Singleton* m_instance; //静态成员变量 
    };
    ​
    Singleton* Singleton::m_instance=nullptr; //静态成员需要先初始化

    这是单例模式最经典的实现方式,将构造函数和拷贝构造函数都设为私有的,而且采用了延迟初始化的方式,在第一次调用getInstance()的时候才会生成对象,不调用就不会生成对象,不占据内存。然而,在多线程的情况下,这种方法是不安全的。

    分析:正常情况下,如果线程A调用getInstance()时,将m_instance 初始化了,那么线程B再调用getInstance()时,就不会再执行new了,直接返回之前构造好的对象。然而存在这种情况,线程A执行m_instance = new Singleton()还没完成,这个时候m_instance仍然为nullptr,线程B也正在执行m_instance = new Singleton(),这是就会产生两个对象,线程AB可能使用的是同一个对象,也可能是两个对象,这样就可能导致程序错误,同时,还会发生内存泄漏。

    实现二[线程安全,锁的代价过高]

    //线程安全版本,但锁的代价过高
    Singleton* Singleton::getInstance() {Lock lock; //伪代码 加锁if (m_instance == nullptr) {m_instance = new Singleton();}return m_instance;
    }

    分析:这种写法不会出现上面两个线程都执行new的情况,当线程A在执行m_instance = new Singleton()的时候,线程B如果调用了getInstance(),一定会被阻塞在加锁处,等待线程A执行结束后释放这个锁。从而是线程安全的。

    但这种写法的性能不高,因为每次调用getInstance()都会加锁释放锁,而这个步骤只有在第一次new Singleton()才是有必要的,只要m_instance被创建出来了,不管多少线程同时访问,使用if (m_instance == nullptr)进行判断都是足够的(只是读操作,不需要加锁),没有线程安全问题,加了锁之后反而存在性能问题。

    锁机制

    std::mutex myMutex;
    ​
    class MySingleton{
    public:static MySingleton& getInstance(){std::lock_guard<std::mutex> myLock(myMutex);if ( !instance ){instance= new MySingleton();}// volatile int dummy{};return *instance;}
    private:MySingleton()= default;~MySingleton()= default;MySingleton(const MySingleton&)= delete;MySingleton& operator=(const MySingleton&)= delete;
    ​static MySingleton* instance;
    };
    ​
    ​
    MySingleton* MySingleton::instance= nullptr;

    每次getInstance方法调用,都需要申请和释放锁,开销非常大。

    实现三[双检查锁,由于内存读写reoder导致不安全]

    上面的做法是不管三七二十一,某个线程要访问的时候,先锁上再说,这样会导致不必要的锁的消耗,那么,是否可以先判断下if (m_instance == nullptr)呢,如果满足,说明根本不需要锁啊!这就是所谓的双检查锁(DCL)的思想,DCL即double-checked locking。

    //双检查锁,但由于内存读写reorder不安全
    Singleton* Singleton::getInstance() {//先判断是不是初始化了,如果初始化过,就再也不会使用锁了if(m_instance==nullptr){Lock lock; //伪代码if (m_instance == nullptr) {m_instance = new Singleton();}}return m_instance;
    }

    这样看起来很棒!只有在第一次必要的时候才会使用锁,之后就和实现一中一样了。

    在相当长的一段时间,迷惑了很多人,在2000年的时候才被人发现漏洞,而且在每种语言上都发现了。原因是内存读写的乱序执行(编译器的问题)。

    分析:m_instance = new Singleton()这句话可以分成三个步骤来执行:

    1. 分配了一个Singleton类型对象所需要的内存。

    2. 在分配的内存处构造Singleton类型的对象。

    3. 把分配的内存的地址赋给指针m_instance

    可能会认为这三个步骤是按顺序执行的,但实际上只能确定步骤1是最先执行的,步骤23却不一定。问题就出现在这。假如某个线程A在调用执行m_instance = new Singleton()的时候是按照1,3,2的顺序的,那么刚刚执行完步骤3Singleton类型分配了内存(此时m_instance就不是nullptr了)就切换到了线程B,由于m_instance已经不是nullptr了,所以线程B会直接执行return m_instance得到一个对象,而这个对象并没有真正的被构造!!严重bug就这么发生了。

    实现四[C++ 11版本的跨平台实现]

    javac#发现这个问题后,就加了一个关键字volatile,在声明m_instance变量的时候,要加上volatile修饰,编译器看到之后,就知道这个地方不能够reorder(一定要先分配内存,在执行构造器,都完成之后再赋值)。

    而对于c++标准却一直没有改正,所以VC++2005版本也加入了这个关键字,但是这并不能够跨平台(只支持微软平台)。

    而到了c++ 11版本,终于有了这样的机制帮助我们实现跨平台的方案。

    //C++ 11版本之后的跨平台实现 
    // atomic c++11中提供的原子操作
    std::atomic<Singleton*> Singleton::m_instance;
    std::mutex Singleton::m_mutex;
    ​
    /*
    * std::atomic_thread_fence(std::memory_order_acquire); 
    * std::atomic_thread_fence(std::memory_order_release);
    * 这两句话可以保证他们之间的语句不会发生乱序执行。
    */
    Singleton* Singleton::getInstance() {Singleton* tmp = m_instance.load(std::memory_order_relaxed);std::atomic_thread_fence(std::memory_order_acquire);//获取内存fenceif (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);//释放内存fencem_instance.store(tmp, std::memory_order_relaxed);}}return tmp;
    }

    实现五[pthread_once函数]

    在linux中,pthread_once()函数可以保证某个函数只执行一次。

    声明: int pthread_once(pthread_once_t once_control, void (init_routine) (void));
    ​
    功能: 本函数使用初值为PTHREAD_ONCE_INIT的once_control
    变量保证init_routine()函数在本进程执行序列中仅执行一次。 

    示例如下:

    class Singleton{
    public:static Singleton* getInstance(){// init函数只会执行一次pthread_once(&ponce_, &Singleton::init);return m_instance;}
    private:Singleton(); //私有构造函数,不允许使用者自己生成对象Singleton(const Singleton& other);//要写成静态方法的原因:类成员函数隐含传递this指针(第一个参数)static void init() {m_instance = new Singleton();}static pthread_once_t ponce_;static Singleton* m_instance; //静态成员变量 
    };
    pthread_once_t Singleton::ponce_ = PTHREAD_ONCE_INIT;
    Singleton* Singleton::m_instance=nullptr; //静态成员需要先初始化

    另外一个版本实现std::call_once & std::once_flag

    这种方式基于C++新特性,保证多线程下实例化方法只被调用一次。

    class CallOnceSingleton{
    public:static MySingleton& getInstance(){std::call_once(initInstanceFlag, &MySingleton::initSingleton);// volatile int dummy{};return *instance;}
    private:MySingleton()= default;~MySingleton()= default;MySingleton(const MySingleton&)= delete;MySingleton& operator=(const MySingleton&)= delete;static MySingleton* instance;static std::once_flag initInstanceFlag;static void initSingleton(){instance= new MySingleton;}
    };
    ​
    MySingleton* MySingleton::instance= nullptr;
    std::once_flag MySingleton::initInstanceFlag;

    实现六[c++ 11版本最简洁的跨平台方案]

    实现四的方案有点麻烦,实现五的方案不能跨平台。其实c++ 11中已经提供了std::call_once方法来保证函数在多线程环境中只被调用一次,同样,他也需要一个once_flag的参数。用法和pthread_once类似,并且支持跨平台。

    实际上,还有一种最为简单的方案!

    在C++memory model中对static local variable,说道:The initialization of such a variable is defined to occur the first time control passes through its declaration; for multiple threads calling the function, this means there’s the potential for a race condition to define first.

    局部静态变量不仅只会初始化一次,而且还是线程安全的。

    class Singleton{
    public:// 注意返回的是引用。static Singleton& getInstance(){static Singleton m_instance;  //局部静态变量return m_instance;}
    private:Singleton(); //私有构造函数,不允许使用者自己生成对象Singleton(const Singleton& other);
    };

    这种单例被称为Meyers' Singleton。这种方法很简洁,也很完美,但是注意:

    1. gcc 4.0之后的编译器支持这种写法。

    2. C++11及以后的版本(如C++14)的多线程下,正确。

    3. C++11之前不能这么写。

    但是现在都18年了。。新项目一般都支持了c++11了。

    用模板包装单例

    从上面已经知道了单例模式的各种实现方式。但是有没有感到一点不和谐的地方?如果我class A需要做成单例,需要这么改造class A,如果class B也需要做成单例,还是需要这样改造一番,是不是有点重复劳动的感觉?利用c++的模板语法可以避免这样的重复劳动。

    template<typename T>
    class Singleton
    {
    public:static T& getInstance() {static T value_; //静态局部变量return value_;}
    ​
    private:Singleton();~Singleton();Singleton(const Singleton&); //拷贝构造函数Singleton& operator=(const Singleton&); // =运算符重载
    };

    假如有AB两个类,用Singleton类可以很容易的把他们也包装成单例。

    class A{
    public:A(){a = 1;}void func(){cout << "A.a = " << a << endl;}
    ​
    private:int a;
    };
    ​
    class B{
    public:B(){b = 2;}
    ​void func(){cout << "B.b = " << b << endl;}
    private:int b;
    };
    ​
    // 使用demo
    int main()
    {Singleton<A>::getInstance().func();Singleton<B>::getInstance().func();return 0;
    }

    假如类A的构造函数具有参数呢?上面的写法还是没有通用性。可以使用C++11的可变参数模板解决这个问题。但是感觉实际中这种需求并不是很多,因为构造只需要一次,每次getInstance()传个参数不是很麻烦吗。。。

    总结

    单例模式本身十分简单,但是实现上却发现各种麻烦,主要是多线程编程确实是个难点。而对于c++的对象模型、内存模型,并没有什么深入的了解,还在一知半解的阶段,仍需努力。

    需要注意的一点是,上面讨论的线程安全指的是getInstance()是线程安全的,假如多个线程都获取类A的对象,如果只是只读操作,完全OK,但是如果有线程要修改,有线程要读取,那么类A自身的函数需要自己加锁防护,不是说线程安全的单例也能保证修改和读取该对象自身的资源也是线程安全的。

part 2:单例模式

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

注意:

  • 1、单例类只能有一个实例。

  • 2、单例类必须自己创建自己的唯一实例。

  • 3、单例类必须给所有其他对象提供这一实例。

1.传统的单例模式实现

class Singleton
{
private:Singleton(){}
public:static Singleton* instance(){if(_instance == 0){_instance = new Singleton();}return _instance;}
private:static Singleton* _instance;public:int atestvalue;
};Singleton* Singleton::_instance = 0;

上面这种实现在单线程环境下是没有问题的,可是多线程下就有问题了。

当:

  1. 例如线程A进入函数instance执行判断语句,这句执行后就挂起了,这时线程A已经认为_instance为NULL,但是线程A还没有创建singleton对象。

  2. 又有一个线程B进入函数instance执行判断语句,此时同样认为_instance变量为null,因为A没有创建singleton对象。线程B继续执行,创建了一个singleton对象。

  3. 稍后,线程A接着执行,也创建了一个新的singleton对象。

  4. 这时,单例就会同时创建2个对象。

针对上面的分析可以看出,需要对_instance变量加上互斥锁:

Singleton* Singleton::instance() {Lock lock; // acquire lock (params omitted for simplicity)if (_instance == 0) {_instance = new Singleton;}return _instance;
} // release lock (via Lock destructor)

上锁后是解决了线程安全问题,但是有些资源浪费。稍微分析一下:每次instance函数调用时候都需要请求加锁,其实并不需要,instance函数只需第一次调用的时候上锁就行了。这时可以用DCLP解决。

Singleton* Singleton::instance() {if (_instance == 0) { // 1st testLock lock;if (_instance == 0) { // 2nd test_instance = new Singleton;}}return _instance;
}

站在编译器的角度关注下这句代码的执行顺序:

_instance  = new singleton()

为了执行这句代码,机器需要做三样事儿:

1.singleton对象分配空间。

2.在分配的空间中构造对象

3.使_instance指向分配的空间

遗憾的是编译器并不是严格按照上面的顺序来执行的。可以交换2和3.

将上面三个步骤标记到代码中就是这样:

Singleton* Singleton::instance() {if (_instance == 0) {Lock lock;if (_instance == 0) {_instance = // Step 3operator new(sizeof(Singleton)); // Step 1new (_instance) Singleton; // Step 2}}return _instance;
}

好了,紧张的时刻到了,如果发生下面两件事:

线程A进入了instance函数,并且执行了step1和step3,然后挂起。这时的状态是:instance不NULL,而instance指向的内存去没有对象! 线程B进入了instance函数,发现_instance不为null,就直接return _instance了。 貌似这时无法解决的问题了,咋办呢。搞嵌入式的程序员可能想到用c++中的volatile关键字。对,就是用volatile,但是用volatile就要一用到底,用了之后就是下面这种丑陋的代码了。

class Singleton {
public:static volatile Singleton* volatile instance();
...
private:
// one more volatile addedstatic Singleton* volatile _instance;
};// from the implementation file
volatile Singleton* volatile Singleton::_instance = 0;
volatile Singleton* volatile Singleton::instance() {if (_instance == 0) {Lock lock;if (_instance == 0) {// one more volatile addedSingleton* volatile temp = new Singleton;_instance = temp;}}return _instance;
}

其实上面完全使用volatile关键字的代码也不能保证正常工作在多线程环境中。具体原因分析请参考C++ and the Perils of Double-Checked Locking这篇论文,文章也给出了终极解决方法。

part 3:java之单例模式

介绍

意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

主要解决:一个全局使用的类频繁地创建与销毁。

何时使用:当您想控制实例数目,节省系统资源的时候。

如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。

关键代码:构造函数是私有的。

应用实例:

  • 1、一个班级只有一个班主任。

  • 2、Windows 是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行。

  • 3、一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。

优点:

  • 1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。

  • 2、避免对资源的多重占用(比如写文件操作)。

缺点:没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。

使用场景:

  • 1、要求生产唯一序列号。

  • 2、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。

  • 3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。

注意事项:getInstance() 方法中需要使用同步锁 synchronized (Singleton.class) 防止多线程同时进入造成 instance 被多次实例化。

实现

我们将创建一个 SingleObject 类。SingleObject 类有它的私有构造函数和本身的一个静态实例。

SingleObject 类提供了一个静态方法,供外界获取它的静态实例。SingletonPatternDemo 类使用 SingleObject 类来获取 SingleObject 对象。

单例模式的 UML 图

步骤 1

创建一个 Singleton 类。

SingleObject.java

public class SingleObject { //创建 SingleObject 的一个对象 private static SingleObject instance = new SingleObject(); //让构造函数为 private,这样该类就不会被实例化 private SingleObject(){} //获取唯一可用的对象 public static SingleObject getInstance(){ return instance; } public void showMessage(){ System.out.println("Hello World!"); } }

步骤 2

从 singleton 类获取唯一的对象。

SingletonPatternDemo.java

public class SingletonPatternDemo { public static void main(String[] args) { //不合法的构造函数 //编译时错误:构造函数 SingleObject() 是不可见的 //SingleObject object = new SingleObject(); //获取唯一可用的对象 SingleObject object = SingleObject.getInstance(); //显示消息 object.showMessage(); } }

步骤 3

执行程序,输出结果:

Hello World!

单例模式的几种实现方式

单例模式的实现有多种方式,如下所示:

1、懒汉式,线程不安全

是否 Lazy 初始化:

是否多线程安全:

实现难度:

描述:这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。 这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作。

实例

public class Singleton { private static Singleton instance; private Singleton (){} public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }

接下来介绍的几种实现方式都支持多线程,但是在性能上有所差异。

2、懒汉式,线程安全

是否 Lazy 初始化:

是否多线程安全:

实现难度:

描述:这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。 优点:第一次调用才初始化,避免内存浪费。 缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。 getInstance() 的性能对应用程序不是很关键(该方法使用不太频繁)。

实例

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

3、饿汉式

是否 Lazy 初始化:

是否多线程安全:

实现难度:

描述:这种方式比较常用,但容易产生垃圾对象。 优点:没有加锁,执行效率会提高。 缺点:类加载时就初始化,浪费内存。 它基于 classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。

实例

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

4、双检锁/双重校验锁(DCL,即 double-checked locking)

JDK 版本:JDK1.5 起

是否 Lazy 初始化:

是否多线程安全:

实现难度:较复杂

描述:这种方式采用双锁机制,安全且在多线程情况下能保持高性能。 getInstance() 的性能对应用程序很关键。

实例

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

5、登记式/静态内部类

是否 Lazy 初始化:

是否多线程安全:

实现难度:一般

描述:这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。 这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,它跟第 3 种方式不同的是:第 3 种方式只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance。想象一下,如果实例化 instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比第 3 种方式就显得很合理。

实例

public class Singleton { private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } private Singleton (){} public static final Singleton getInstance() { return SingletonHolder.INSTANCE; } }

6、枚举

JDK 版本:JDK1.5 起

是否 Lazy 初始化:

是否多线程安全:

实现难度:

描述:这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。 这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。 不能通过 reflection attack 来调用私有构造方法。

实例

public enum Singleton { INSTANCE; public void whateverMethod() { } }

经验之谈:一般情况下,不建议使用第 1 种和第 2 种懒汉方式,建议使用第 3 种饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用第 5 种登记方式。如果涉及到反序列化创建对象时,可以尝试使用第 6 种枚举方式。如果有其他特殊的需求,可以考虑使用第 4 种双检锁方式。

参考:

  1. Scott Meyers. Effective C++:55 Specific Ways to Improve Your Programs and Designs,3rd Edition. 电子工业出版社, 2011

  2. Stanley B. Lippman. 深度探索C++对象模型. 电子工业出版社, 2012

  3. Scott Meyers. C++ and the Perils of Double-Checked Locking. 2004

  4. 陈良乔(译). C++11 FAQ中文版

  5. Bjarne Stroustrup. C++11 FAQ

  6. Paul E. McKenney, Hans-J. Boehm, Lawrence Crowl. C++ Data-Dependency Ordering: Atomics and Memory Model. 2008

  7. Wikipedia. Out-of-order execution

  8. Loïc. Mutex And Memory Visibility, 2009

  9. Randal E.Bryant, David O'Hallaron. 深入理解计算机系统(第2版). 机械工业出版社, 2010

  10. Martin Thompson. Memory Barriers/Fences, 2011

  11. Working Draft, Standard For Programing Language C++. 2012

  12. W.Richard Stevens. UNIX环境高级编程(第3版), 人民邮电出版社, 2014

  13. stackoverflow. Is Meyers implementation of Singleton pattern thread safe

  14. stackoverflow. When are static C++ class members initialized

  1. https://segmentfault.com/a/1190000015950693

  1. https://www.cnblogs.com/liyuan989/p/4264889.html

相关文章:

计算图像波峰点

#include "opencv2/highgui/highgui.hpp" #include "opencv2/imgproc/imgproc.hpp" #include <iostream> #include <stdio.h> #include <iostream> #include <vector> using namespace std; using namespace cv; // 计算图像的波峰…

锁的算法,隔离级别的问题

锁的算法 InnoDB存储引擎有3中行锁的算法设计&#xff0c;分别是&#xff1a; Record Lock&#xff1a;单个行记录上的锁。Gap Lock&#xff1a;间隙锁&#xff0c;锁定一个范围&#xff0c;但不包含记录本身。Next-Key Lock&#xff1a;Gap LockRecord Lock&#xff0c;锁定一…

好程序员分享24个canvas基础知识小结

好程序员分享24个canvas基础知识小结&#xff0c;非常全面详尽&#xff0c;推荐给大家。 现把canvas的知识点总结如下&#xff0c;以便随时查阅。 1、填充矩形 fillRect(x,y,width,height); 2、绘制矩形边框 strokeRect(x,y,width,height); 3、擦除矩形 clearRect(x,y,width,he…

【leetcode】二叉树与经典问题

文章目录笔记leetcode [114. 二叉树展开为链表](https://leetcode-cn.com/problems/flatten-binary-tree-to-linked-list/)解法一: 后序遍历、递归leetcode [226. 翻转二叉树](https://leetcode-cn.com/problems/invert-binary-tree/)思路与算法复杂度分析leetcode [剑指 Offer…

PHP PSR-1 基本代码规范(中文版)

基本代码规范 本篇规范制定了代码基本元素的相关标准&#xff0c;以确保共享的PHP代码间具有较高程度的技术互通性。 关键词 “必须”("MUST")、“一定不可/一定不能”("MUST NOT")、“需要”("REQUIRED")、“将会”("SHALL")、“不会…

最近邻插值实现:图像任意尺寸变换

#<opencv2/imgproc/imgproc.hpp> #include <opencv2/core/core.hpp> #include <opencv2/highgui/highgui.hpp> #include <iostream> using namespace cv; using namespace std; // 实现最近邻插值图像缩放 cv::Mat nNeighbourInterpolatio…

软件测试-培训的套路-log3

最新的套路&#xff01;我是没了解过--下图中描述-log3 Dotest-董浩 但是我知道不管什么没有白吃的午餐而且还会给钱…如果真的有&#xff0c;请醒醒&#xff01; 当然话又回来&#xff0c;套路不套路&#xff0c;关键看你是否需要&#xff1b;你如果需要我觉得是帮你…不需要&…

ALSA声卡驱动中的DAPM详解之四:在驱动程序中初始化并注册widget和route

前几篇文章我们从dapm的数据结构入手&#xff0c;了解了代表音频控件的widget&#xff0c;代表连接路径的route以及用于连接两个widget的path。之前都是一些概念的讲解以及对数据结构中各个字段的说明&#xff0c;从本章开始&#xff0c;我们要从代码入手&#xff0c;分析dapm的…

双线性插值实现

#include <opencv2/imgproc/imgproc.hpp> #include <opencv2/core/core.hpp> #include <opencv2/highgui/highgui.hpp> #include <iostream> // 实现双线性插值图像缩放 cv::Mat BilinearInterpolation(cv::Mat srcImage) {CV_Assert(srcI…

【C++】C++ 强制转换运算符

C 运算符 强制转换运算符是一种特殊的运算符&#xff0c;它把一种数据类型转换为另一种数据类型。强制转换运算符是一元运算符&#xff0c;它的优先级与其他一元运算符相同。 大多数的 C 编译器都支持大部分通用的强制转换运算符&#xff1a; (type) expression 其中&…

WPS 2019 更新版(8392)发布,搭配优麒麟 19.04 运行更奇妙!

WPS 2019 支持全新的外观界面、目录更新、方框打勾、智能填充、内置浏览器、窗口拆组、个人中心等功能。特别是全新的新建页面&#xff0c;让你可以整合最近打开的文档、本地模版、公文模版、在线模板等。 随着优麒麟即将发布的新版 19.04 的到来&#xff0c;金山办公软件也带来…

图像金字塔操作,上采样、下采样、缩放

#include "opencv2/imgproc/imgproc.hpp" #include "opencv2/highgui/highgui.hpp" #include <iostream> // 图像金子塔采样操作 void Pyramid(cv::Mat srcImage) {// 根据图像源尺寸判断是否需要缩放if(srcImage.rows > 400 && srcImage…

【C++】【OpenCv】图片加噪声处理,计时,及键盘事件响应捕捉

图像噪声添加 cv::Mat addGuassianNoise(cv::Mat& src, double a, double b) {cv::Mat temp src.clone();cv::Mat dst(src.size(), src.type()); ​// Construct a matrix containing Gaussian noisecv::Mat noise(temp.size(), temp.type());cv::RNG rng(time(NULL));//…

透视学理论(十四)

2019独角兽企业重金招聘Python工程师标准>>> 已知左右距点分别位于透视画面两侧&#xff0c;因此我们可以左距点来得出透视画面的纵深长度。 距点&#xff0c;指的是与透视画面成45的水平线的消失点。我们把画面边缘按1米的宽度&#xff08;A点&#xff09;&#xf…

适用于Mac上的SQL Server

适用于Mac上的SQL Server&#xff1f; 众所周知&#xff0c;很多人都在电脑上安装了SQL Server软件&#xff0c;普通用户直接去官网下载安装即可&#xff0c;Mac用户则该如何在Mac电脑上安装SQL Server呢&#xff1f;想要一款适用于Mac上的SQL Server&#xff1f;点击进入&…

【MATLAB】————拷贝指定文件路径下的有序文件(选择后),可处理固定规律的文件图片数据或者文件

总体上来说这种数据有2中处理思路。第一种如下所示&#xff0c;从一组数据中挑选出符合要求的数据&#xff1b; 第二中就是数据中不需要的数据删除&#xff0c;选择处理不需要的数据&#xff0c;留下的补集就是需要的数库。一般情况下需要看问题是否明确&#xff0c;需求明确的…

mouseover与mouseenter,mouseout与mouseleave的区别

mouseover与mouseenter 不论鼠标指针穿过被选元素或其子元素&#xff0c;都会触发 mouseover 事件。只有在鼠标指针穿过被选元素时&#xff0c;才会触发 mouseenter 事件。 mouseout与mouseleave不论鼠标指针离开被选元素还是任何子元素&#xff0c;都会触发 mouseout 事件。只…

图像掩码操作的两种实现

#include <opencv2/imgproc/imgproc.hpp> #include <opencv2/core/core.hpp> #include <opencv2/highgui/highgui.hpp> #include <iostream> #include <stdio.h> using namespace cv; using namespace std; // 基于像素邻域掩码操作…

控制反转 IOC

2019独角兽企业重金招聘Python工程师标准>>> 控制反转&#xff08;Inversion of Control&#xff0c;缩写为IoC&#xff09;面向对象设计原则&#xff0c;降低代码耦合度 依赖注入&#xff08;Dependency Injection&#xff0c;简称DI&#xff09; 依赖查找&#xf…

【C++】explicit关键字

explicit的优点是可以避免不合时宜的类型变换&#xff0c;缺点无。所以google约定所有单参数的构造函数都必须是显式的** explicit关键字只需用于类内的单参数构造函数前面。由于无参数的构造函数和多参数的构造函数总是显式调用&#xff0c;这种情况在构造函数前加explicit无…

mongodb3 分片集群平滑迁移

分片集群平滑迁移实验(成功)过程概述&#xff1a;为每个分片添加多个从节点&#xff0c;然后自动同步。同步完后&#xff0c;切换主节点到新服务器节点。导出原来的config 数据库&#xff0c;并导入到新服务器的config数据库停掉整个集群&#xff0c;可以使用kill 命令停掉新服…

图像添加椒盐噪声

#include <opencv2/core/core.hpp> #include <opencv2/highgui/highgui.hpp> #include <cstdlib> // 图像添加椒盐噪声 cv::Mat addSaltNoise(const cv::Mat srcImage, int n) {cv::Mat resultIamge srcImage.clone() ;for(int k0; k<n; k){// 随机取值行…

我用python10年后,我发现学python必看这三本书!

非常喜欢python 我非常喜欢python&#xff0c;在前面5年里&#xff0c;它一直是我热衷使用并不断研究的语言&#xff0c;迄今为止&#xff0c;python都非常友好并且易于学习&#xff01; 它几乎可以做任何事&#xff0c;从简单的脚本创建、web&#xff0c;到数据可视化以及AI人…

【OpenCV】内核的形状函数使用记录

opencv getStructuringElement函数 为形态操作返回指定大小和形状的结构化元素。 该函数构造并返回结构化元素&#xff0c;这些元素可以进一步传递给侵蚀、扩张或morphologyEx。但是您也可以自己构造一个任意的二进制掩码&#xff0c;并将它用作结构化元素。 getStructuringE…

boxFilter 滤波器实现

cv::Ptr<cv::FilterEngine> cv::createBoxFilter( int srcType, int dstType, Size ksize,Point anchor, bool normalize, int borderType ) {// 基础参数设置 图像深度int sdepth CV_MAT_DEPTH(srcType);int cn CV_MAT_CN(srcType), sumType CV_64F;if( sdepth < …

git代理设置

2019独角兽企业重金招聘Python工程师标准>>> git config --global http.proxy http://127.0.0.1:1080 git config --global https.proxy https://127.0.0.1:1080 git config --global http.sslVerify false删除 git config --global --unset http.proxy git config …

获得PMP证书的这一年

很荣幸&#xff0c;通过2018年12月的PMP考试&#xff0c;这不仅是一张证书的收获&#xff0c;更体现了我的成长&#xff0c;明确了以后的道路。在考证的过程中&#xff0c;我收获了很多&#xff0c;不仅是工作技能方面&#xff0c;还包括思想的升华。  首先&#xff0c;重拾了…

【OpenCV】图片操作小结:RAW图转image以及image连续保存

opencv将RAW图转image uint32_t ReadRawImage(cv::Mat& image,const std::string& path,int width,int height,int cv_image_type) {cv::Mat input_image(height, width, cv_image_type);std::ifstream in(path, std::ios::in | std::ios::binary);if (!in) {std::cou…

Windows server2008服务器设置多用户登录

添加用户 右击我的电脑-->管理-->本地用户和组-->新用户 启用远程服务并添加远程用户 启用 右键我的电脑--->属性--->远程设置--->勾上允许远程连接到此电脑 添加用户 点击选择用户--->添加--->高级--->立即查找 防火墙允许远程连接设置 控制面板(c…

Linux6版本系统搭建Open***远程访问

前言&#xff1a;open***是一个***工具,用于创建虚拟专用网络(Virtual Private Network)加密通道的免费开源软件,提供证书验证功能,也支持用户名密码认证登录方式,当然也支持两者合一,为服务器登录和连接提供更加安全的方式,可以在不同网络访问场所之间搭建类似于局域网的专用网…