设计模式之单例模式

一、单例模式简介

  在单例模式中,类的实例化只会发生一次,而后续的访问都会返回同一个实例。这样可以保证在整个应用程序中,只有一个实例存在,从而避免了多个实例对资源的重复使用或竞争的问题。单例模式通常被用于需要共享某些资源或状态的情况,例如数据库连接、日志记录器、配置管理器等。它可以提供一种简单而有效的方式来管理这些资源,同时确保在整个应用程序中只有一个实例。

单例模式的关键特点包括:

  1. 私有构造函数:单例类的构造函数被设置为私有,防止外部代码创建多个实例。
  2. 静态实例变量:单例类内部维护一个静态变量,用于保存类的唯一实例。
  3. 静态访问方法:通过一个静态的访问方法来获取单例实例,该方法负责创建实例(如果实例不存在)并返回该实例
  4. 拷贝构造函数和赋值构造函数是私有类型,目的是禁止外部拷贝和赋值,确保实例的唯一性。

单例模式可以分为懒汉式 和饿汉式 ,懒汉式和饿汉式是单例模式中两种常见的实现方式,它们在实例化单例对象的时机上有所不同。

  1. 懒汉式(Lazy Initialization):

    • 懒汉式单例模式是在需要时才创建实例。也就是说,当第一次请求获取单例实例时才进行实例化。
    • 在懒汉式中,单例对象的实例化是延迟进行的,因此也被称为延迟加载。
    • 懒汉式实现相对简单,但在多线程环境下需要考虑线程安全性,需要进行同步控制,以避免多个线程同时创建多个实例。
  2. 饿汉式(Eager Initialization):

    • 饿汉式单例模式在程序启动时就创建实例。也就是说,单例对象的实例化发生在类加载阶段或者应用程序启动时。
    • 在饿汉式中,单例对象的实例在整个生命周期内都存在,并且可以被立即访问。
    • 饿汉式实现相对简单,不存在线程安全问题,但在某些情况下可能造成不必要的资源浪费,因为实例被提前创建而不管是否被使用。

区别总结如下:

  • 实例化时机:懒汉式是在需要时才进行实例化,而饿汉式是在程序启动时或类加载阶段就进行实例化。
  • 延迟加载:懒汉式是延迟加载实例,只有在需要时才创建,而饿汉式是提前创建实例,立即可用。
  • 线程安全性:懒汉式在多线程环境下需要考虑线程安全性,需要进行同步控制,而饿汉式不存在线程安全问题。
  • 资源消耗:懒汉式避免了不必要的资源消耗,只有在需要时才创建实例,而饿汉式可能造成不必要的资源浪费,因为实例被提前创建而不管是否被使用。

选择使用懒汉式还是饿汉式取决于具体的应用场景和需求。如果资源消耗较大或需要延迟加载,懒汉式是一个较好的选择。如果资源消耗较小且需要立即可用,饿汉式是一个简单有效的方案。另外,还可以考虑其他的单例实现方式,如双重检查锁定、静态内部类等,以满足特定的需求。

二、饿汉式单例模式在程序启动时就创建实例

#pragma once
class Singleton 
{
private:
    Singleton();
    Singleton(const Singleton &);
    Singleton& operator=(const Singleton &);
    ~Singleton();

public:
    static Singleton* getInstance();

private:
    static Singleton m_instance;
};
#include "Singleton.h"
#include <iostream>

Singleton Singleton::m_instance;

Singleton::Singleton()
{
    std::cout << "创建饿汉式单例对象" << std::endl;
}

Singleton::Singleton(const Singleton&)
{

}

Singleton& Singleton::operator=(const Singleton&)
{
    return m_instance;
}

Singleton::~Singleton()
{
    std::cout << "删除饿汉式单例对象" << std::endl;
}

Singleton* Singleton::getInstance()
{
    return &m_instance;
}
#include <iostream>
#include "Singleton.h"

int main() {
    system("pause");
    return 0;
}

打印结果:

 三、懒汉式单例模式是在需要时才创建实例

#pragma once
class Singleton 
{
private:
    Singleton();
    Singleton(const Singleton &);
    Singleton& operator=(const Singleton &);
    ~Singleton();

public:
    static Singleton* getInstance();

private:
    static Singleton *m_instance;
};
#include "Singleton.h"
#include <iostream>

Singleton *Singleton::m_instance = nullptr;

Singleton::Singleton()
{
    std::cout << "创建懒汉式单例对象" << std::endl;
}

Singleton::Singleton(const Singleton&)
{

}

Singleton& Singleton::operator=(const Singleton&)
{
    return *m_instance;
}

Singleton::~Singleton()
{
    std::cout << "删除懒汉式单例对象" << std::endl;
}

Singleton* Singleton::getInstance()
{
    if (m_instance == nullptr) {
        m_instance = new Singleton();
    }

    return m_instance;
}
#include <iostream>
#include "Singleton.h"

int main() {
    Singleton *sin1 = Singleton::getInstance();
    Singleton* sin2 = Singleton::getInstance();
    Singleton* sin3 = Singleton::getInstance();
    return 0;
}

打印结果:

 可以看到,获取了三次类的实例,却只有一次类的构造函数被调用,表明只生成了唯一实例,这是个最基础版本的单例实现。但是同样上述的单例模式也有两个问题:

1.线程安全的问题:当多线程获取单例时有可能引发竞态条件:第一个线程在if中判断 m_instance是空的,于是开始实例化单例;同时第2个线程也尝试获取单例,这个时候判断m_instance还是空的,于是也开始实例化单例;这样就会实例化出两个对象,这就是线程安全问题的由来; 
2.内存泄漏:注意到类中只负责new出对象,却没有负责delete对象,因此只有构造函数被调用,析构函数却没有被调用;因此会导致内存泄漏。

因此,这里提供一个改进的,线程安全的、使用智能指针的实现;

#pragma once

#include <memory>
#include <mutex>

class Singleton 
{
private:
    Singleton();
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

public:
    ~Singleton();

public:
    static std::shared_ptr<Singleton> getInstance();

private:
    static std::shared_ptr<Singleton> m_instance;
    static std::mutex m_mutex;
};
#include "Singleton.h"
#include <iostream>

std::shared_ptr<Singleton> Singleton::m_instance = nullptr;
std::mutex Singleton::m_mutex;

Singleton::Singleton()
{
    std::cout << "创建懒汉式单例对象" << std::endl;
}

Singleton::~Singleton()
{
    std::cout << "删除懒汉式单例对象" << std::endl;
}

std::shared_ptr<Singleton> Singleton::getInstance()
{
    if (m_instance == nullptr) {
        std::lock_guard<std::mutex> lock_mutex(m_mutex);
        if (m_instance == nullptr) {
            m_instance = std::shared_ptr<Singleton>(new Singleton());
        }
    }

    return m_instance;
}
#include <iostream>
#include "Singleton.h"

int main() {
    std::shared_ptr<Singleton> ptr1 = Singleton::getInstance();
    std::shared_ptr<Singleton> ptr2 = Singleton::getInstance();
    
    return 0;
}

打印结果如下,发现确实只构造了一次实例,并且发生了析构

 shared_ptr和mutex都是C++11的标准,以上这种方法的优点是

  • 基于 shared_ptr, 用了C++比较倡导的 RAII思想,用对象管理资源,当 shared_ptr 析构的时候,new 出来的对象也会被 delete掉。以此避免内存泄漏。
  • 加了锁,使用互斥量来达到线程安全。这里使用了两个 if判断语句的技术称为双检锁;好处是,只有判断指针为空的时候才加锁,避免每次调用 getInstance的方法都加锁,锁的开销毕竟还是有点大的。

四、局部静态变量实现懒汉式单例

#pragma once

class Singleton 
{
private:
    Singleton();
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

public:
    ~Singleton();

public:
    static Singleton& getInstance();

};
#include "Singleton.h"
#include <iostream>

Singleton::Singleton()
{
    std::cout << "创建懒汉式单例对象" << std::endl;
}

Singleton::~Singleton()
{
    std::cout << "删除懒汉式单例对象" << std::endl;
}

Singleton& Singleton::getInstance()
{
    static Singleton instance;

    return instance;
}
#include <iostream>
#include "Singleton.h"

int main() {
    Singleton& sin1 = Singleton::getInstance();
    Singleton& sin2 = Singleton::getInstance();
    
    return 0;
}

打印结果:

这是最推荐的一种单例实现方式:

  1. 通过局部静态变量的特性保证了线程安全;
  2. 不需要使用共享指针,代码简洁;
  3. 注意在使用的时候需要声明单例的引用 Single& 才能获取对象。

 

原文地址:https://www.cnblogs.com/QingYiShouJiuRen/p/17465011.html