【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);//获取内存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;
    }

    实现五[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 test
        Lock 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 3
        operator new(sizeof(Singleton)); // Step 1
        new (_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 added
    static 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 added
 
 
        Singleton* 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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大江东去浪淘尽千古风流人物

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值