详解单例设计模式

单例模式

单例模式介绍

单例模式指的是, 无论怎么获取, 永远只能得到该类类型的唯一实例对象.

一般遇到写进程池类日志类内存池的话就会用到 单例模式。

设计单例必须满足下面三个条件:

  1. 全局只有一个实例,用 static 特性实现,构造函数设为私有

  2. 通过公有接口获得实例.

  3. 线程安全.

// 饿汉单例模式
class CSingleton
{
public:
    static CSingleton *getInstance()
    {
        return &single;
    }

private:
    static CSingleton single;
    CSingleton()
    {
        cout << "CSingleton()" << endl;
    }
    ~CSingleton()
    {
        cout << "~CSingleton()" << endl;
    }
    CSingleton(const CSingleton &); // 防止外部拷贝构造.
};

CSingleton CSingleton::single;


// 1.懒汉单例模式

class CSingleton
{
public:
    static CSingleton *getInstance()
    {
        if (single == nullptr)
        {
            single = new CSingleton(); // 没有 delete.
        }
        return single;
    }

private:
    static CSingleton *single;
    CSingleton() { cout << " CSingleton()" << endl; }
    ~CSingleton() { cout << " ~CSingleton()" << endl; }
    CSingleton(const CSingleton &);
};
CSingleton *CSingleton::single = nullptr;


// 2. 资源没释放,存在内存泄漏问题.. 加个嵌套类.
class CSingleton
{
public:
    static CSingleton *getInstance()
    {
        if (single == nullptr)
        {
            single = new CSingleton();
        }
        return single;
    }

private:
    static CSingleton *single;
    CSingleton() { cout << " CSingleton()" << endl; }
    ~CSingleton() { cout << " ~CSingleton()" << endl; }
    CSingleton(const CSingleton &);

    // 定义一个嵌套类, 在该类的析构函数中, 自动释放外层类的资源.
    class CRelease
    {
    public:
        ~CRelease()
        {
            delete single;
        }
    };
    // 通过该静态对象在程序结束时自动析构的特点,来释放外层类的对象资源.
    static CRelease release;
};

CSingleton *CSingleton::single = nullptr;
CSingleton::CRelease CSingleton::release;

上述代码存在线程安全问题,故需要设计线程安全的单例模式。

线程安全的单例模式

1、饿汉单例模式的线程安全特性:

饿汉单例模式中, 单例对象定义成了一个 static 静态对象,他是在程序启动时, main 函数运行之前就初始化好的, 因此不存在线程安全问题,可以在多线程运行。

2、懒汉单例模式的线程安全特性

// 懒汉单例模式获取单例对象方法如下:
static CSingleton* getInstance()
{
    if(single == nullptr)
    {
        single = new CSingleton();
    }
    return single;
}

显然, getInstance 是个 不可重入函数, 在多线程中会出现竞态条件问题, single = new CSingleton() 会做三件事:
1.开辟内存 2.调用构造函数 3.给 single 指针赋值.

在多线程中, 可能出现如下可能:

  1. 线程 A 先调用 getInstance 函数,由于 single 为 nullptr, 进入 if 语句.
  2. new 操作先开辟内存, 此时 A 现成的 CPU 时间片到了, 切换到 B 线程.
  3. B 线程由于 single 为 nullptr 也进入 if 语句了, 开始 new 操作.

线程安全的懒汉单例模式

//  使用 pthread库 中提供的线程互斥操作方法 mutex 互斥锁.
class CSingleton
{
public:
    static CSingleton *getInstance1()
    {
        // 获取互斥锁 (效率太低, 每次获取实例, 第一次加锁有必要, 后面都没必要)
        pthread_mutex_lock(&mutex);
        if (nullptr == single)
        {
            single = new CSingleton();
        }
        // 释放互斥锁
        pthread_mutex_unlock(&mutex);
        return single;
    }

     static CSingleton *getInstance2()
     {
        if(single == nullptr)
        {
            // 获取互斥锁.
            pthread_mutex_lock(&mutex);
            // 锁 + 双重判断 
            if(single == nullptr)
            {
                single = new CSingleton();
            }
            // 释放互斥锁.
            pthread_mutex_unlock(&mutex);
        }
        return single;
     }

private:
    static CSingleton *single;
    CSingleton() { cout << "CSingleton()" << endl; }
    ~CSingleton()
    {
        pthread_mutex_destroy(&mutex); // 释放互斥锁
        cout << "~CSingleton()" << endl;
    }
    CSingleton(const CSingleton &);

    class CRelease
    {
    public:
        ~CRelease() { delete single; }
    };
    static CRelease release;

    // 定义线程间的互斥锁
    static pthread_mutex_t mutex;
};
CSingleton *CSingleton::single = nullptr;
CSingleton::CRelease CSingleton::release;
// 互斥锁的初始化
pthread_mutex_t CSingleton::mutex = PTHREAD_MUTEX_INITIALIZER;

封装互斥锁的线程安全懒汉单例模式

class CMutex
{
public:
    CMutex() { pthread_mutex_init(&mutex, NULL); };  // 初始化互斥锁.
    ~CMutex() { pthread_mutex_destroy(&mutex); };    // 销毁互斥锁.
    void lock() { pthread_mutex_lock(&mutex); };     // 加锁,获取互斥锁.
    void unlock() { pthread_mutex_unlock(&mutex); }; // 解锁,释放互斥锁.

private:
    pthread_mutex_t mutex; // 定义互斥锁对象.
};

class CSingleton
{
public:
    static CSingleton *getInstance1()
    {	
        if (single == nullptr)
        {
            mutex.lock();
            if (single == nullptr)
            {
                single = new CSingleton();
            }
            mutex.unlock();
        }
        return single;
    }

    // 以下代码是否线程安全?
     static CSingleton *getInstance2()
     {
        static CSingleton single;
        return &single;
     }
     // 对于 static 静态局部变量的初始化, 编译器会自动对它的初始化进行加锁和解锁控制, 使静态局部变量的初始化成为线程安全的操作,
     //	不用担心多线程都会初始化静态局部变量!

private:
    CSingleton() { cout << "CSingleton()" << endl; }
    ~CSingleton() { cout << "~CSingleton()" << endl; }
    CSingleton(const CSingleton &);
    class CRelease
    {
    public:
        ~CRelease()
        {
            delete single;
        }
    };

    static CRelease release;
    static CSingleton *single;
    // 线程间的静态互斥锁.
    static CMutex mutex;
};
CSingleton *CSingleton::single = nullptr;
CSingleton::CRelease CSingleton::release;
// 定义互斥锁静态对象.
CMutex CSingleton::mutex;

Double-Checked Locking thesis (DCLP)

// from the header file
class Singleton
{
public:
    static Singleton* instance();

private:
    static Singleton* pInstance;
};

// from the implementation file
Singleton* Singleton::instance()
{
    if(pInstance == 0)
    {
        pInstance = new Singleton();
    }
    return pInstance;
} // unsafe

// 采用锁可实现安全获取单例对象.
Singleton* Singleton::instance()
{
    Lock lock;  //acquire lock.
    if(pInstance == 0)
    {
        pInstance = new Singleton();
    }
    // release lock
    return pInstance;
}
// 这种方法能做到线程安全, 缺点就是资源耗费严重, 每次获取实例都需要获取锁,实际上只需要在第一次获取对象时需要加锁.

// The Double-Checked Locking Pattern (DCLP) 实现.
Singleton* Singleton::instance()
{
    if(pInstance == 0)      // 1st
    {
        Lock lock;
        if(pInstance == 0)  // 2st
          {
              pInstance = new Singleton();
          }
          // release lock
    }
    return pInstance;
}

// This statement causes three things to happen:
1.  Allocate memory to hold a Singleton object.            (分配内存以保存 Singleton 对象).
2.  Construct a Singleton object in the allocated memory.  (在分配的内存中构造一个 Singleton 对象).
3.  Make pInstance point to the allocated memory.          (使 pInstance 指向分配的内存).

// 有时编译器交换步骤 2和 3. 如下:
Singleton* Singleton::instance()
{
    if(pInstance == 0)
    {
        Lock lock;
        if(pInstance == 0)
        {
            pInstance =                         			       // Step 3.
              void* rawMemory = operator new (sizeof(Singleton));  // Step 1.
              new (rawMemory) Singleton;       				      //  Step 2.
        }
    }
    return pInstance;
}


// volatile 关键字
class Singleton
{
public:
    static Singleton* instance();

private:    
    static Singleton* volatile pInstance; // volatile added.
    int x;
    Singleton()
        :x(5)
    {}
};

// from the implementation file
Singleton* volatile Singleton::pInstance = 0;

Singleton* Singleton::instance()
{
    if(pInstance == 0)
    {
        Lock lock;
        if(pInstance == 0)
        {
            Singleton* volatile temp = new Singleton(); //volatile added
            pInstance = temp;
        }
    }
    return pInstance;
}

// 内联函数展开之后...
// After inlining the constructor, the code looks like this:
if(pInstance == 0)
{
    Lock lock;
    if(pInstance == 0)
    {
        Singleton* volatile temp =
            static_cast<Singleton*>(operator new (sizeof(Singleton)));
            temp->x = 5;     // inlined Single constructor
        pInstance = temp;
    }
}

// problem:
//	编译器可以对 temp->x 的赋值重新排序,以重新排序对 pInstance 的赋值。如果这样做,pInstance 将在它指向的数据初始化之前分配,
//	这再次导致其他线程读取未初始化的 x 的可能性.
//  可能解决的方法:
class Singleton
{   
public:
    static volatile Singleton* volatile instance();

private:
    static volatile Singleton* volatile pInstance;
};

volatile Singleton* volatile Singleton::pInstance = 0;

volatile Singleton* volatile Singleton::instance1()
{
    if(pInstance == 0)
    {
        Lock lock;
        if(pInstance == 0)
        {
            volatile Singleton* volatile temp =
                new volatile Singleton();
            pInstance = temp;
        }
    }
    return pInstance;
}

volatile Singleton* volatile Singleton::instance2()
{
    if(pInstance == 0)
    {
        Lock lock;
        if(pInstance == 0)
        {
             Singleton* volatile temp =
               static_cast<Singleton*> (operator new (sizeof(Singleton)));
            static_cast<volatile int&>(temp->x) = 5;
            pInstance = temp;
        }
    }
    return pInstance;
}

// 缓存一致性问题的一般解决方案是使用内存屏障
Singleton* Singleton::instance()
{
    Singleton* tmp = pInstance;
    //....               insert memory barrier.
    if(tmp == 0)
    {
        Lock lock;
        tmp = pInstance;
        if(tmp == 0)
        {
            tmp = new Singleton();
            //...        insert memory barrier.
            pInstance = tmp;
        }
    }
    return tmp;
}

面试考题

单例对象为什么是 static ?

  1. 唯一性静态变量属于类,而不是类的某个实例。这意味着静态变量在所有实例间共享。通过将单例实例声明为 static,我们确保该类只有一个实例,即使有多个对象引用,所有这些引用都指向同一个 static 实例。这个机制可以避免创建多个实例,从而保持单例的唯一性。

  2. 全局访问:静态变量和方法可以通过类名直接访问,不需要创建类的对象。这使得单例模式中的实例可以方便地在整个应用中被访问。这一点是单例模式的一个重要特性:通过类名直接访问单例对象,而无需实例化新对象。

  3. 生命周期控制:静态变量的生命周期随着类的加载而开始,随着类的卸载而结束。这意味着单例对象的生命周期通常贯穿整个程序的运行时间,它的初始化发生在第一次访问类时,而销毁则发生在程序结束时。这保证了单例对象在程序运行期间始终存在,且可以全局访问。

如何初始化单例对象?

  1. 饿汉式单例(Eager Initialization)
    饿汉式的单例通常通过静态变量在程序开始时直接初始化,这个过程是线程安全的,因为 C++ 的静态初始化由编译器保证是安全的。

优点:
1. 实现简单:静态局部变量的初始化是 C++11 以后线程安全的,所以不需要额外的锁或同步机制。
2. 类加载时即完成实例化:单例对象在类第一次被使用时初始化,避免了多线程竞争问题。

缺点:
1. 资源浪费:即使程序可能不需要这个实例,也会在类加载时创建对象。如果单例对象较大,可能会占用不必要的资源。

  1. 懒汉式单例(Lazy Singleton)

懒汉式是只有在第一次调用时才初始化单例对象的方式。通常使用双重检查锁定来确保线程安全。在 C++ 中,为了保证双重检查锁定的线程安全性,std::mutexstd::lock_guard 可以用于线程同步。

优点:

  1. 延迟加载:只有在真正需要时才创建对象,节省了内存和资源。
  2. 节省资源:比饿汉式更加灵活,可以避免不必要的对象创建。

缺点:

  1. 实现复杂:需要使用双重检查锁定和 std::mutex 来确保线程安全,增加了代码复杂度。
  2. 线程同步问题:如果不正确处理线程安全,会导致在多线程环境下创建多个实例。

如何销毁单例对象?

1. 手动销毁

可以通过提供一个销毁单例对象的方法手动控制对象的生命周期。比如,定义一个 destroyInstance 函数来手动删除单例对象:

class Singleton {
public:
    static Singleton* getInstance() {
        if (instance == nullptr)
         {
            instance = new Singleton();
        }
        return instance;
    }

    static void destroyInstance() {
        delete instance;
        instance = nullptr;
    }

private:
    Singleton() {}
    ~Singleton() {}

    static Singleton* instance;
};

Singleton* Singleton::instance = nullptr;

优点:

  • 可以显式地控制对象的销毁时间,避免对象的生命周期贯穿整个程序。

缺点:

  • 需要程序员主动调用 destroyInstance,如果忘记调用,可能会造成内存泄漏。
  • 如果在多线程环境下,需要注意同步问题

2. 智能指针
通过使用 std::unique_ptr 管理单例对象的生命周期,std::unique_ptr 会在程序结束时自动销毁单例对象,从而避免手动管理内存。

#include <memory>

class Singleton {
public:
    static Singleton& getInstance() {
        static std::unique_ptr<Singleton> instance(new Singleton());
        return *instance;
    }

private:
    Singleton() {}
    ~Singleton() {}
};

优点:

  • 自动管理内存:使用 std::unique_ptr 确保对象在不再需要时自动销毁,避免内存泄漏。
  • 简单性:智能指针会自动管理对象的生命周期,不需要显式调用 delete

缺点:

  • 无法手动控制销毁时间:智能指针会在作用域结束时销毁对象,但无法精确控制销毁时间。如果你希望对象在程序某一特定时刻销毁,则不适用。
  • 性能开销:相较于静态局部变量,智能指针会带来一些额外的管理开销。

3. 使用静态局部变量

通过静态局部变量的方式实现单例对象,C++11 以后,静态局部变量的初始化是线程安全的,而且对象会在程序结束时自动销毁。

class Singleton {
public:
    static Singleton& getInstance() {
        static Singleton instance;  // 静态局部变量,线程安全
        return instance;
    }

private:
    Singleton() {}
    ~Singleton() {}
};

优点:

  • 实现简洁:使用静态局部变量可以简化单例的实现,静态局部变量只会在第一次访问时被初始化。
  • 线程安全:从 C++11 开始,静态局部变量的初始化是线程安全的,因此无需手动同步。
  • 自动销毁:程序结束时,静态局部变量会自动销毁,无需显式删除对象。

缺点:

无法手动控制对象的销毁:静态局部变量的生命周期与程序一致,它会在程序结束时销毁,而不能在程序运行中显式控制对象的销毁时机。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

沫羽皓ღღ

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

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

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

打赏作者

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

抵扣说明:

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

余额充值