C++单例模式

本文深入探讨了单例设计模式的原理,包括构造函数私有化、静态属性指向唯一实例、饿汉式与懒汉式的区别,以及其实例化过程中的线程安全问题。介绍了单例模式的优点和缺点,适合的使用场景和注意事项,如资源管理和线程池控制。
摘要由CSDN通过智能技术生成

什么是单例

单例 Singleton 是设计模式的一种,其特点是只提供唯一一个类的实例,具有全局变量的特点即提供一个访问它的全局访问点,该实例被所有程序模块共享。这个类在全局只有唯一的一个实例对象,在所有位置都可以通过该类提供的接口访问到这个唯一实例。

单例优缺点

单例类主要解决了一个全局使用的类的频繁的创建与销毁

  • 优点
    • 1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例;
    • 2、避免对资源的多重占用。
  • 缺点
    • 单例类没有接口,不能继承。
C++实现单例模式要点
  • 构造函数需私有化

    • 将该类的构造函数私有化,目的是禁止其他程序创建该类的对象,同时也是为了提醒查看代码的人我这里是在使用单例模式,防止他人将这里任意修改。此时,需要提供一个可访问类自定义对象的类成员方法(对外提供该对象的访问方式)。即把构造函数设置为private可以防止用户自己定义实例。
  • 指向本身实例的类属性为静态
    指向自己实例的私有引用在被类方法(Getinstance)调用时被初始化,只有静态成员变量才能在没有创建对象时进行初始化,并且类的静态成员在第一次使用时不会再被初始化,保证了单例,因此设置为静态。

  • 通过一个static静态成员方法返回唯一的对象实例
    通过类方法(GetInstance) 获取instance,类属性instance为静态的(static),则需要类的静态方法才能调用,因此该类方法应设为静态的。通过一个静态成员函数GetInstance来获取实例。静态成员函数可以在不定义对象的情况下调用。

单例应用场景
  • 优点
    (1)在单利模式中,活动的实例只有一个实例,对单例类的所有实例化得到的都是相同的一个实例,这样就防止其他对象自己实例化,确保所有对象都访问一个实例
    (2)单例模式中的类自己来控制实例化进程,类就在改变实例化进程上有相应的伸缩性。
    (3)提供了对唯一实例的受控访问
    (4)避免对共享资源的多重使用
    (5)由于在系统只存在一个对象,因此可以节约资源,当需要偏饭创建和销毁对象时,单例模式无疑可以提高系统性能

  • 缺点
    (1)不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。
    (2)滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。
    (3)由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。

  • 应用场景
    (1)资源共享的情况下,避免由于资源操作时导致的性能或损耗等。如上述中的日志文件,应用配置。
    (2)控制资源的情况下,方便资源之间的互相通信。如线程池等。

具体运用场景
  • 最经典的使用场景是公用缓存,由一个线程周期性地写,由多个线程读取,此时就可以用单例模式来保证不同线程写和读的是同一个实例。
  • 设备管理器,系统中可能有多个设备,但是只有一个设备管理器,用于管理设备驱动;
  • 数据池,用来缓存数据的数据结构,需要在一处写,多处读取或者多处写,多处读取;
饿汉式:在单例类定义时即实例化

饿汉式指的是无论会不会有人调用getInstance获取实例,都会事先把这个实例给创建好。饿汉式可以通过在类里加入一个静态成员变量来实现(注意C++的静态成员变量必须在类外初始化)

public class One {
    //私有化构造方法使得该诶无法通过外部new   进行实例化
    private One(){}
    //准备一个类属性,指向一个实例化对象。 因为是类属性,所以只有一个
    private static One instance = new One();

    public static One getInstance(){
        return instance;
    }
}
//One 应该只有一只,通过私有化其构造方法,使得外部无法通过new 得到新的实例。
//One 提供了一个public static的getInstance方法,外部调用者通过该方法获取的对象,而且每一次都是获取同一个对象。 从而达到单例的目的。
饿汉式:多线程安全问题

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

🥥即无论是否调用该类的实例,在程序开始时就会产生一个该类的实例,并在以后仅返回此实例。由静态初始化实例保证其线程安全性,WHY?
🥥因为静态实例初始化在程序开始时进入主函数之前就由主线程以单线程方式完成了初始化,不必担心多线程问题。
🥥故在性能需求较高时,应使用这种模式,避免频繁的锁争夺。
🥥类或全局静态变量,cpp中初始化,类似于java的内部类构建单例线程安全的方法(最为高效)

饿汉式:多线程安全代码
 1 class SingletonStatic
 2 {
 3 private:
 4     static const SingletonStatic* m_instance;
 5     SingletonStatic(){}
 6 public:
 7     static const SingletonStatic* getInstance()
 8     {
 9         return m_instance;
10     }
11 };
12 
13 //外部初始化 before invoke main
14 const SingletonStatic* SingletonStatic::m_instance = new SingletonStatic;
饿汉式:boost代码

🥥boost 的实现方式是:单例对象作为静态局部变量,然后增加一个辅助类,并声明一个该辅助类的类静态成员变量,在该辅助类的构造函数中,初始化单例对象。

class Singleton
{
public:
    static Singleton* GetInstance()
    {
        static Singleton instance;
        return &instance;
    }
protected:
    struct Object_Creator// 辅助代理类
    {
        Object_Creator()
        {
            Singleton::GetInstance();
        }
    };
    static Object_Creator _object_creator;
    
    Singleton() {}
    ~Singleton() {}
};
Singleton::Object_Creator Singleton::_object_creator; 
懒汉式:只有在第一次用到类实例的时候才实例化

懒汉式(Lazy-Initialization)的方法是直到使用时才实例化对象,也就说直到调用getInstance() 方法的时候才 new 一个单例的对象, 如果不被调用就不会占用内存。

public class One {
    //私有化构造方法使得该诶无法通过外部new   进行实例化
    private One(){}
    //准备一个类属性,指向一个实例化对象。 因为是类属性,所以只有一个
    private static One instance ;

    public static One getInstance(){
        if(null==instance){
            instance = new One();
        }
        //返回 instance 指向的对象
        return instance;
    }
}
  • 懒汉模式的singleton类有以下特点:

    • 有一个指向唯一实例的静态指针,并且是私有的
    • 有一个公有的函数,可以获取这个唯一的实例,并且在需要的时候创建该实例
    • 其构造函数是私有的,这样就不能从别处创建该类的实例
懒汉式:多线程安全问题

🥥当实例没有被创建的时候,如果有多个线程都调用getInstance方法,就可能创建多个实例,就存在线程安全问题
🥥但是实例一旦创建好,后面线程调用getInstance方法就不会出现线程安全问题
🥥⭐线程安全问题出现在首次创建实例的时候

懒汉式:多线程安全代码

—————————————————————————————————————

  • 双重校验锁 PS:JAVA
    • 使用双重if判定,降低竞争锁频率
    • 使用volatile修饰instance
//java中synchronized
public class Singleton {
    private static volatile Singleton instance = null;
    private Singleton(){
 
    }
    public static Singleton getInstance(){
        if(instance == null){ //外层的if判断:如果实例被创建直接return,不让线程再继续竞争锁
            //在没有创建实例时,多个线程已经进入if判断了
            //一个线程竞争到锁,其他线程阻塞等待
            synchronized (Singleton.class) {
                //内层的if判断,目的是让竞争失败的锁如果再次竞争成功的话判断实例是否被创建,创建释放锁return,没有则创建
                if(instance == null){ 
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

🥥对双重if的解析:

  • 外层的if判断:实例只是被创建一次,当实例已经被创建好了就不要后续操作,直接return返回

  • 内层的if判断:实例未被创建时,多个线程同时竞争锁,只有一个线程竞争成功并创建实例,其他竞争失败的线程就会阻塞等待,当第一线程释放锁后,这些竞争失败的线程就会继续竞争,但是实例已经创建好了,所以需要再次进行if判断

  • 双重校验锁 PS:C++

#include<mutex>
using namespace std;
mutex mt;
class Singleton {
private:
	static Singleton* _instance;
	Singleton() {}
public:
	static Singleton* getInstance() {
		if (_instance == nullptr) {
			mt.lock(); // 解锁
			if (_instance == nullptr)
				_instance = new Singleton();
			mt.unlock(); // 加锁
		}
		return _instance;
	}
};

⭐采用双锁机制的前提是锁必须是带有内存屏障的(否则可能多次new),即lock前后代码编译器不得有任何汇编指令顺序优化,这里采用boost或c++11的锁是可以的,不能直接调用系统底层锁API,更不建议用不带原子判断而直接调用系统级锁API;另外更佳的是call_once方法;但是这些方式至少会有底层指令级原子判断及内存屏障消耗(已经算最小代价了);在性能要求高的地方,更佳的方式是在启动多线程前就初始化,后续直接用更佳,比如现代编译器可以直接static const,会在main前就执行初始化,当然也可以另外自定义初始顺序。
————————————————————————————————————

  • 静态成员实例的懒汉模式,2次判空,内部加锁:
 1 class Singleton
 2 {
 3 private:
 4     static Singleton* m_instance;
 5     Singleton(){}
 6 public:
 7     static Singleton* getInstance();
 8 };
 9 
10 Singleton* Singleton::getInstance()
11 {
12     if(NULL == m_instance)
13     {
14         Lock();//借用其它类来实现,如boost
15         if(NULL == m_instance)
16         {
17             m_instance = new Singleton;
18         }
19         UnLock();
20     }
21     return m_instance;
22 }
  • 局部静态变量懒汉模式
    C++11规定了local static在多线程条件下的初始化行为,要求编译器保证了内部静态变量的线程安全性。在C++11标准下,《Effective C++》提出了一种更优雅的单例模式实现,使用函数内的 local static 对象。这样,只有当第一次访问getInstance()方法时才创建实例。
class Singleton
{
public:
    static Singleton& getInstance() 
    {
        static Singleton instance;
        return instance;
    }
	
private:
	Singleton() {};
	~Singleton() {};
	Singleton(const Singleton&);
	Singleton& operator=(const Singleton&);
};
class Singleton {
public:
	static Singleton& getInstance() {
		static Singleton instance;
		return instance;
	}
private:
	Singleton() = default;
	Singleton(const Singleton& other) = delete;
	Singleton& operator = (const Singleton&) = delete;
};
  • 通过在get_instance函数中引入静态变量,达到了在不使用智能指针和锁的情况下,也能够保证线程安全和内存安全的效果,

    • C++11标准中的Magic Static特性保证线程安全,如果当变量在初始化的时候,并发同时进入声明语句,并发线程将会阻塞等待初始化结束。
    • 没有使用指针来new对象,没有内存泄漏的问题。
  • 由于静态局部变量在程序执行到该对象的声明处时才被首次初始化,所以这也是一种懒汉式。

  • 智能指针的懒汉模式

    • 使用C++11提供的call_once
#include<thread>
#include<mutex>
once_flag flag;
class Singleton {
public:
	static Singleton& getInstance() {
		call_once(flag, []() {_instance.reset(new Singleton())});
		return *_instance;
	}
private:
	static unique_ptr<Singleton> _instance;
	Singleton() = default;
	Singleton(const Singleton& other) = delete;
	Singleton& operator = (const Singleton&) = delete;
};
unique_ptr<Singleton> Singleton::_instance;
  • 懒汉模式可用智能指针实现资源管理,此时不需要deleteInstance()
//实例指针类型改为:
std::shared_ptr<Singleton>

//m_SingletonInstance = new Singleton();
//只需改动为:
m_SingletonInstance = std::shared_ptr<Singleton>(new Singleton()); 
  • share_ptr
#include <iostream>
#include <memory> // shared_ptr
#include <mutex>  // mutex
// version 2:
// with problems below fixed:
// 1. thread is safe now
// 2. memory doesn't leak

class Singleton{
public:
    typedef std::shared_ptr<Singleton> Ptr;
    ~Singleton(){
        std::cout<<"destructor called!"<<std::endl;
    }
    Singleton(Singleton&)=delete;
    Singleton& operator=(const Singleton&)=delete;
    static Ptr get_instance(){

        // "double checked lock"
        if(m_instance_ptr==nullptr){
            std::lock_guard<std::mutex> lk(m_mutex);
            if(m_instance_ptr == nullptr){
              m_instance_ptr = std::shared_ptr<Singleton>(new Singleton);
            }
        }
        return m_instance_ptr;
    }
private:
    Singleton(){
        std::cout<<"constructor called!"<<std::endl;
    }
    static Ptr m_instance_ptr;
    static std::mutex m_mutex;
};
// initialization static variables out of class
Singleton::Ptr Singleton::m_instance_ptr = nullptr;
std::mutex Singleton::m_mutex;

int main(){
    Singleton::Ptr instance = Singleton::get_instance();
    Singleton::Ptr instance2 = Singleton::get_instance();
    return 0;
}
  • 用智能指针shared_ptr创建实例,当不再有指针指向这个实例时,shared_ptr会自动销毁该实例,回收其占用的空间。

  • 加锁获取实例,保证只会创建一个实例。而且这里用了双检锁的思想,先判断是否为null,然后再加锁,避免因为把加锁操作放在外面而阻塞了正常的在已有实例的情况下获取实例的情况。

  • 基于 shared_ptr, 用了C++比较倡导的 RAII思想,用对象管理资源,当 shared_ptr 析构的时候,new 出来的对象也会被 delete掉。以此避免内存泄漏。

  • 加了锁,使用互斥量来达到线程安全。这里使用了两个 if判断语句的技术称为双检锁;好处是,只有判断指针为空的时候才加锁,避免每次调用 get_instance的方法都加锁,锁的开销毕竟还是有点大的。

  • 不足之处在于: 使用智能指针会要求用户也得使用智能指针,非必要不应该提出这种约束; 使用锁也有开销; 同时代码量也增多了,实现上我们希望越简单越好。

  • 精简版懒汉模式

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

⭐**注意:**这个精简版之所以也是线程安全的,是因为,在底层对于static静态局部变量的初始化,编译器会自动加锁和解锁。

单例模式释放

  • 问题:

    • 通过new出一个对象来实现的单例,不论单例是通过饿汉方式,还是懒汉方式来实现,都面临一个问题,即new出来的对象由谁释放,何时释放,怎么释放 ?

    • 如果对象没有被释放,在运行期间可能会存在内存泄露问题。有人可能会说,在进程结束时,操作系统会进行必要的清理工作,包括释放进程的所有堆栈等信息,即使存在内存泄露,操作系统也会收回的;且对于单例来讲,进程运行期间仅有一个对象实例,而且该实例有可能根本就没有进行内存的申请操作,不释放实例所占内存,对进程的运行也不会造成影响。这么说好像很有道理的样子,既然操作系统会清理一切后续工作,那么我们还有必要进行内存释放工作吗?

  • 方法

    • 调用delete来释放:一:new出来的对象,必须用与之对应的delete显示的来释放,程序并不会自动调用析构函数来析构new出来的对象;二:在delete的时候会调用析构函数,析构函数中又调用了delete,然后又调用了析构函数……这样就进入了一个无限的循环之中。

    • C标准库的atexit()函数注册释放函数:atexit()函数可以用来注册终止函数。如果打算在main()结束后执行某些操作,可以使用该函数来注册相关函数。标准规定atexit()至少可以注册32个终止函数,如果系统中有多个单例,我们可能要注册多个函数,或者在同一个终止函数中释放所有单例对象。但是方式一中的问题依然存在。必须由程序猿/媴手工注册,且有可能遗漏某个对象。

    • 由单例类提供释放接口

class Singleton {
public:
    void del_object() {
        if (instance) {
            delete instance;
            instance = 0;
        }
    }
};
int main(int argc, char ** argv)
{
    Singleton::get_instance()->del_object();
}
  • 操作系统自动释放 :
    我们知道,进程结束时,静态对象的生命周期随之结束,其析构函数会被调用来释放对象。因此,我们可以利用这一特性,在单例类中声明一个内嵌类,该类的析构函数专门用来释放new出来的单例对象,并声明一个该类类型的static对象。
class Singleton {
public:
    // ...
private:
    // ...
    static Singleton * instance;
    class GarbageCollector {
    public:
        ~GarbageCollector() {
            if (Singleton::instance) {
                delete Singleton::instance;
                Singleton::instance = 0;
            }
        }
    };
    static GarbageCollector gc;
};

// 定义
Singleton::GarbargeCollector Singleton::gc;
// ...

我们可以像之前一样使用单例了,不需要再关心对象的释放问题。进程结束时,操作系统会帮我们去释放的。

补充:C++中static对象的初始化

non-local static对象(函数外)

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

懒汉和饿汉用法区别:
  • 懒汉式是以时间换空间,适应于访问量较小时;推荐使用内部静态变量的懒汉单例。
  • 饿汉式是以空间换时间,适应于访问量较大、操作比较频繁的情况。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值