设计模式:单例模式

发布于:2025-09-15 ⋅ 阅读:(25) ⋅ 点赞:(0)

目录

一、引言

二、单例模式概述

三、单例模式的结构与实现

3.1 单例模式结构

3.2 实现单例模式

四、单例模式的两种实现方式

4.1 饿汉式单例模式

4.2 懒汉式单例模式

4.3 饿汉式与懒汉式的对比

五、单例模式总结

5.1 单例模式的优点

5.2 单例模式的缺点

5.3 单例模式的使用场景

六、结语



一、引言

假设有一个大型的网站,该网站的流量非常大,需要通过一个服务器集群来处理用户的请求。为了合理分配请求到不同的服务器,我们需要一个负载均衡器。这个负载均衡器必须是全局唯一的,所有请求都通过这个负载均衡器来分配,避免多个负载均衡器同时工作导致混乱。单例模式可以很好的适配这个场景。

二、单例模式概述

单例模式:确保一个类只有一个实例,并提供一个全局访问点来访问这个唯一实例。

Singleton Pattern:Ensue a class has only one instance,and provide a global point of access to it.

1)为了确保只有一个实例,构造函数必须私有,这样在类外部就创建不了对象了。同时,也要将拷贝构造函数和赋值重载函数禁掉。

2)在类内部声明一个类型为自身的静态私有成员变量。

3)在类内部定义一个公有的静态成员方法,用于获取唯一的实例。

光说理论很抽象,下面进入实战。

三、单例模式的结构与实现

3.1 单例模式结构

单例模式的结构很简单,看下面的实现就知道了。

3.2 实现单例模式

例子:假设有一个大型的网站,该网站的流量非常大,需要通过一个服务器集群来处理用户的请求。为了合理分配请求到不同的服务器,我们需要一个负载均衡器。

下面我们就用单例模式来设计。

//.hpp
#include <vector>
#include <string>

class LoadBalancer {
private:
    LoadBalancer() {}//私有构造函数
    LoadBalancer(const LoadBalancer&) = delete;//禁止拷贝构造函数
    LoadBalancer& operator=(const LoadBalancer&) = delete;//禁止赋值操作符
public:
    //公有静态方法,返回唯一的实例
    static LoadBalancer* getLoadBalancer() {
        if (_instance == nullptr) {
            _instance = new LoadBalancer();
        }
        return _instance;
    }
    //添加服务器
    void addServer(std::string server) {
        _servers.push_back(server);
    }
    //选择服务器
    std::string getServer() {
        if (_servers.empty()) {
            return "";
        }
        int index = rand() % _servers.size();
        return _servers[index];
    }
    //删除服务器
    void removeServer(std::string server) {
        for (auto it = _servers.begin(); it != _servers.end(); it++) {
            if (*it == server) {
                _servers.erase(it);
                break;
            }
        }
    }
private:
    static LoadBalancer* _instance;//在类内声明
    std::vector<std::string> _servers;//服务器列表
};

LoadBalancer* LoadBalancer::_instance = nullptr;//在类外定义


//test.cpp
#include <iostream>
#include "singleton.hpp"

int main()
{
    LoadBalancer* lb = LoadBalancer::getLoadBalancer();
    LoadBalancer* lb2 = LoadBalancer::getLoadBalancer();

    // 检查两个实例是否相同
    if (lb == lb2) {
        std::cout << "两个实例是相同的" << std::endl;
    }

    lb->addServer("server-1");
    lb->addServer("server-2");
    lb->addServer("server-3");
    lb->addServer("server-4");
    lb->addServer("server-5");

    for (int i = 0; i < 10; i++) {
        std::string server = lb->getServer();
        std::cout <<"分发请求至服务器: " << server << std::endl;
    }

    return 0;
}

四、单例模式的两种实现方式

4.1 饿汉式单例模式

class Singleton {
private:
    Singleton() {}//私有构造函数
    Singleton(const Singleton&) = delete;//禁止拷贝构造函数
    Singleton& operator=(const Singleton&) = delete;//禁止赋值操作符
public:
    //公有静态方法,返回唯一的实例
    static Singleton* getInstance() {
        return _instance;
    }
private:
    static Singleton* _instance;
};

Singleton* Singleton::_instance = new Singleton();

当类被加载时,静态变量_instance就会被初始化,此时就会创建单例类的唯一实例。

4.2 懒汉式单例模式

class Singleton {
private:
    Singleton() {}//私有构造函数
    Singleton(const Singleton&) = delete;//禁止拷贝构造函数
    Singleton& operator=(const Singleton&) = delete;//禁止赋值操作符
public:
    //公有静态方法,返回唯一的实例
    static Singleton* getInstance() {
        if (_instance == nullptr) {
            _instance = new Singleton();
        }
        return _instance;
    }
private:
    static Singleton* _instance;
};

Singleton* Singleton::_instance = nullptr;

懒汉式单例模式采用的是延迟加载,首次需要的时候才创建唯一实例。

懒汉模式存在一个很大的问题,就是在多线程并发的场景下,如果按上述的方式编写代码,可能会创建出多个实例。这就违背了单例模式的初衷了。

#include <pthread.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

class Singleton {
private:
    Singleton() {}//私有构造函数
    Singleton(const Singleton&) = delete;//禁止拷贝构造函数
    Singleton& operator=(const Singleton&) = delete;//禁止赋值操作符
public:
    //公有静态方法,返回唯一的实例
    static Singleton* getInstance() {
        pthread_mutex_lock(&mutex);//加锁
        if (_instance == nullptr) {
            //_instance 并不像饿汉式那样,在类加载的时候就创建实例,而是在第一次使用的时候创建实例
            //所以在多线程环境下,_instance就是共享资源,能被多个线程同时判断为nullptr,导致创建多个实例
            //所以要加锁,保证只有一个线程能创建实例
            _instance = new Singleton();
        }
        pthread_mutex_unlock(&mutex);//解锁
        return _instance;
    }
private:
    static Singleton* _instance;
};

Singleton* Singleton::_instance = nullptr;

上面的代码在访问临界区时进行加锁,确保了只会创建一个实例。但是这种做法并不高效,因为在每次调用get方法时都需要锁定判断,所以引入了双重 if 判断。

#include <pthread.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

class Singleton {
private:
    Singleton() {}//私有构造函数
    Singleton(const Singleton&) = delete;//禁止拷贝构造函数
    Singleton& operator=(const Singleton&) = delete;//禁止赋值操作符
public:
    //公有静态方法,返回唯一的实例
    volatile static Singleton* getInstance() {
        if (_instance == nullptr) {
            pthread_mutex_lock(&mutex);//加锁
            if (_instance == nullptr) {
                _instance = new Singleton();
            }
            pthread_mutex_unlock(&mutex);//解锁
        }
        return _instance;
    }
private:
    volatile static Singleton* _instance;
};

volatile Singleton* Singleton::_instance = nullptr;

//关键字:volatile
//volatile 关键字的作用是告诉编译器,不要对该变量进行优化,
//每次都从内存中读取该变量的值,而不是从寄存器中读取

在创建了唯一实例后,多线程再调用get方法时,直接就会因为_instance不为空而直接返回,不用在去竞争锁,提高了效率。同时,在多线程环境下,一个线程可能会修改某个变量的值,而其他线程需要读取该变量的最新值,volatile关键字可以确保每次访问变量时都是从内存中读取最新值。

4.3 饿汉式与懒汉式的对比

饿汉式因为在类加载的时候就创建了唯一的实例,所以不存在线程安全问题,但是,一上来就创建实例,会导致加载速度变慢,另外,就算后续这个唯一的实例用不到也被创建出来了,一定程度上造成了资源的浪费。

懒汉式,存在线程安全问题,即使使用双重 if 判断,在一定程度上确实挽回了一点效率,但是由于在访问_instance变量时,每一次都要去内存中读取,效率是会比较低的。它的优点在于,采用的是延迟加载,需要的时候在创建出实例,后续用不到的话就不会创建,不会造成资源的浪费,同时在类加载的时候,不会拖累效率。

五、单例模式总结

5.1 单例模式的优点

1)全局唯一实例

  • 单例模式确保一个类只有一个实例,并提供一个全局访问点。这在需要严格控制资源或状态时非常有用,例如数据库连接池、线程池等场景。

2)节省系统资源

  • 由于单例模式避免了重复创建实例,减少了内存开销和系统性能消耗,特别适合频繁创建和销毁的对象。

3)避免状态冲突

  • 单一实例可以避免多个实例导致的资源竞争或状态不一致问题,例如配置管理、日志记录等场景。

5.2 单例模式的缺点

1)难以扩展

  • 没有抽象层,扩展有很大的困难。

2)耦合度高

  • 单例类既提供了业务方法,有提供创建对象的方法,将对象的创建和对象本身的功能耦合在了一起。

5.3 单例模式的使用场景

当某个类只需要一个实例对象的时候,就是单例模式出场的时候。

六、结语

在20多种设计模式中,单例模式的结构是最简单的,它只有一个单例类。在软件设计的道路上,我们仍需掌握其他设计模式,“路漫漫其修远兮,吾将上下而求索。”


完~