设计模式之单例模式

发布于:2024-05-10 ⋅ 阅读:(29) ⋅ 点赞:(0)

一、单例模式概述

在这里插入图片描述

1.单例模式的定义

单例模式(Singleton Pattern)是一种常用的软件设计模式。在这种模式中,一个类负责创建自己的对象,同时确保只有单个对象被创建了。这个类提供了访问其对象的方式。

2.单例模式的作用

单例模式使得我们在使用一个类及其对象时,不必频繁地进行创建和销毁,提高了程序的性能,节约了系统资源。单例模式常常在以下场景中使用:

  1. 资源共享: 当应用程序需要共享某个资源(如数据库连接、日志记录器、线程池等)时,可以使用单例模式确保所有对象共享同一个实例,避免资源的重复创建和浪费。

  2. 配置管理器: 在需要全局访问配置信息的情况下,可以使用单例模式来管理配置信息的加载和访问,确保所有组件都使用相同的配置实例。

  3. 日志记录器: 单例模式可以用于创建全局的日志记录器,以便在整个应用程序中记录日志并保持日志的一致性。

  4. 线程池: 在需要管理和控制线程的情况下,可以使用单例模式来创建线程池,确保所有线程共享同一个线程池实例,并且能够动态地调整线程池的大小。

  5. 缓存管理器: 当需要缓存某些数据以提高应用程序的性能时,可以使用单例模式来创建全局的缓存管理器,以确保所有组件都使用相同的缓存实例。

  6. 窗口管理器: 在图形用户界面(GUI)应用程序中,可以使用单例模式来创建窗口管理器,以确保所有窗口共享同一个实例,并且能够动态地管理和控制窗口的显示和隐藏。

3.单例模式的优缺点

优点:

  1. 资源节约:由于系统中只存在一个对象,可以节约系统资源,尤其是对象创建和销毁成本高时,单例模式可以提高系统性能。
  2. 全局访问点:提供了对唯一实例的受控访问,方便全局调用。
  3. 共享资源:适用于控制资源访问,如配置管理和日志记录,确保资源的协调与共享
  4. 避免重复实例:防止其他对象对自己的实例化,确保所有对象都访问同一个实例

缺点:

  1. 扩展困难:由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
  2. 职责过重:单例类的职责过重,可能违背了“单一职责原则”,一个类既负责业务逻辑又负责生命周期管理。
  3. 灵活性受限:不利于代码的测试,特别是依赖于单例类的代码。在单元测试中,需要模拟不同的单例对象,单例模式的单一实例特性对此造成了阻碍。
  4. 多线程问题:在多线程环境下,如果不正确实现,可能会导致多个实例被创建,违反单例模式的原则。同时,在高并发的场景下,由于锁的存在,可能会导致线程阻塞,性能下降。

二、单例模式的实现

1.懒汉式

说明:延迟实例化的单例模式,即先不创建实例,当第一次被调用时,再创建实例。

代码:

C++版本:
class Singleton {
private:
    // 私有静态指针变量,用于保存类的唯一实例
    static Singleton* instance;

    // 构造函数私有化
    Singleton() {}

	// 防止拷贝构造和赋值操作
	Singleton(const Singleton&) = delete;
	Singleton& operator=(const Singleton&) = delete;

public:
    // 提供全局访问点
    static Singleton* GetInstance() {
        if (instance == nullptr) {
            instance = new Singleton();
        }
        return instance;
    }
};

// 初始化静态成员变量
Singleton* Singleton::instance = nullptr;

C#版本:
    public class Singleton
    {
        // 定义一个静态变量来保存类的实例
        private static Singleton instance;

        //构造函数私有化
        private Singleton()
        {
        }

        //一个公有的静态函数,用于提供全局唯一的访问点
        public static Singleton GetInstance()
        {
            if (instance == null)
            {
                instance = new Singleton();
            }
            return instance;
        }
    }

优点:

延迟了实例化,如果不需要使用类,则不进行实例化,在使用时才进行实例化,避免了资源浪费

缺点:

线程不安全,在多线程环境夏,如果有多个线程同时获取实例,都发现了类还没有进行实例化,即instance == nullptr,那么会有多个线程进行 instance = new Singleton(),就会产生多个实例。

2.饿汉式

说明:程序在加载时直接实例化好这个对象,后续需要使用的时候直接调用。

代码:

C++版本:
//实现一个饿汉式单例模式
class Singleton {
private:
	//私有静态成员变量,用于存储唯一实例
	static Singleton* instance;

	//私有构造方法,避免类在外部被实例化
	Singleton() {

	}

	//删除拷贝函数和赋值操作
	Singleton(const Singleton&) = delete;
	Singleton& operator=(const Singleton&) = delete;

public:
	//公有方法,提供全局唯一的访问点
	static Singleton* GetInstance() {
		return instance;
	}
};

// 初始化静态成员变量
Singleton* Singleton::instance = new Singleton();

C#版本:
    //实现一个饿汉式单例模式的类
    public class Singleton
    {
        //类加载时就创建实例
        private static readonly Singleton instance = new Singleton();

        //构造函数私有化
        private Singleton()
        {
        }

        public static Singleton GetInstance()
        {
            return instance;
        }

优点:

线程安全。

缺点:

如果被实例化的对象在本程序的声明周期中并未被使用,则会造成资源浪费。

3.线程安全的懒汉式

说明:通过在GetInstance()加锁来解决多线程情况下的线程不安全问题

代码:

C++版本:
//线程安全的懒汉式单例模式
#include <mutex>

class Singleton
{
private:
	//私有静态成员变量
	static Singleton* instance;
	static std::mutex mtx;

	//私有构造函数
	Singleton() {}

	//删除拷贝函数和赋值函数
	Singleton(const Singleton&) = delete;
	Singleton& operator=(const Singleton&) = delete;

public:
	static Singleton* GetInstance()
	{
		//线程安全的懒汉式单例模式,不使用双重锁机制
		//这行代码保证了只有一个线程可以进入临界区
		//临界区是指一个代码片段,一次只允许一个线程进入执行
		//在这里,临界区是整个GetInstance函数
		std::lock_guard<std::mutex> lock(mtx);
		if (instance == nullptr)
		{
			instance = new Singleton();
		}
		return instance;
	}
};

//静态成员变量初始化
Singleton* Singleton::instance = nullptr;
C#版本:
namespace 线程安全的懒汉式单例模式c_
{
    线程安全的懒汉式单例模式
    
    public class Singleton
    {
        //定义一个静态变量来保存类的实例
        private static Singleton uniqueInstance;
        //定义一个标识确保线程同步
        private static readonly object locker = new object();
        //定义私有构造函数,使外界不能创建该类实例
        private Singleton()
        {
        }

       
        public static Singleton GetInstance()
        {
            //当第一个线程运行到这里时,此时会对locker对象 "加锁",
            //当第二个线程运行到这里时,首先检测locker对象为加锁状态,该线程就会挂起等待第一个线程解锁。
            //lock语句运行完之后(即线程运行完之后)会对该对象 "解锁"
            lock (locker)
            {
                //如果类的实例不存在则创建,否则直接返回
                if (uniqueInstance == null)
                {
                    uniqueInstance = new Singleton();
                }
            }
            return uniqueInstance;
        }
    }
}

优点:

延迟实例化,避免了资源浪费,同时通锁避免了多线程环境下的线程安全问题

缺点:

虽然解决了线程安全问题,在程序的生命周期内确保了只有一个实例,但每次获取实例时,只有拿到锁的线程才可以直接获取,造成了时间上的浪费,程序性能下降。

4.双重检查锁实现

说明:基于“线程安全的懒汉式”进行优化,通过改变锁的位置使得每次调用GetInstance()视情况加锁,优化了程序性能。

代码:

C++实现:
//使用双重检查锁机制实现的单例模式
#include <mutex>

class Singleton
{
	//私有静态成员变量
	static Singleton* instance;
	static std::mutex mtx;

	//私有构造函数
	Singleton() {}
	
	//删除拷贝函数和赋值函数
	Singleton(const Singleton&) = delete;
	Singleton& operator=(const Singleton&) = delete;

	//静态成员函数,提供全局唯一访问点
public:
	static Singleton* GetInstance()
	{
		//双重检查锁机制
		if (instance == nullptr)
		{
			std::lock_guard<std::mutex> lock(mtx);
			if (instance == nullptr)
			{
				instance = new Singleton();
			}
		}
		return instance;
	}
};

//静态成员变量初始化
Singleton* Singleton::instance = nullptr;

C#实现:
namespace 线程安全的懒汉式单例模式
{
    线程安全的懒汉式单例模式
    public class Singleton
    {
        //定义一个静态变量来保存类的实例
        private static Singleton uniqueInstance;
        //定义一个标识确保线程同步
        private static readonly object locker = new object();
        //定义私有构造函数,使外界不能创建该类实例
        private Singleton()
        {
        }

        //定义公有方法提供一个全局访问点,同时你也可以定义公有属性来提供全局访问点
        public static Singleton GetInstance()
        {
            //当第一个线程运行到这里时,此时会对locker对象 "加锁",
            //当第二个线程运行到这里时,首先检测locker对象为加锁状态,该线程就会挂起等待第一个线程解锁。
            //lock语句运行完之后(即线程运行完之后)会对该对象 "解锁"
            if (uniqueInstance == null)
            {
                lock (locker)
                {
                    //如果类的实例不存在则创建,否则直接返回
                    if (uniqueInstance == null)
                    {
                        uniqueInstance = new Singleton();
                    }
                }
            }
            return uniqueInstance;
        }

    }
}

使用.NET4.0引入是lazy<T>实现等效效果,且性能更优。

namespace 线程安全的懒汉式单例模式
{
    线程安全的懒汉式单例模式

    public class Singleton
    {
        //使用lazy<T>类型的字段,确保线程安全
        //Lazy<T>是一个线程安全的延迟初始化类,它允许推迟对象的创建,直到第一次访问该对象时
        //对于创建十分耗时的对象,Lazy<T>类可以提高性能
        //此外,Lazy<T>类还可以保证线程安全
        private static readonly Lazy<Singleton> _instance = new Lazy<Singleton>(() => new Singleton());

        //私有构造函数,确保外部无法实例化
        private Singleton()
        {
        }

        //提供一个公共的静态方法,返回一个单例
        public static Singleton GetInstance()
        {
            return _instance.Value;
        }
    }
}


优点:

  • 延迟实例化,节约了资源
  • 线程安全,并且相对于直接加锁的懒汉式程序的整体性能更优

缺点:

可能会因为操作系统指令重排发生异常: uniqueInstance = new Singleton();这段代码的执行分为3步

  • 为uniqueInstance分配内存空间
  • 初始化uniqueInstance
  • 将uniqueInstance指向分配的内存空间
    由于指令重排,第一步和第三步可能在第二步之前,这样会出现这种情况:
    线程A正在进行第二步(一三步已经完成),在这时线程B也来访问这个类,发现这个类已经存在实例了,直接取用,但其实uniqueInstance还没有被初始化。

5.静态内部类实现

说明:静态内部类(Static Nested Class)是Java中的一种特殊类,它位于另一个类的内部并且使用static关键字进行修饰。

代码:

Java:
public class Singleton {  
    // 定义一个静态内部类SingletonHolder,用于实现Singleton实例的延迟加载
    private static class SingletonHolder {  
        // 在SingletonHolder类中声明一个静态final变量INSTANCE,用于保存Singleton的唯一实例
        private static final Singleton INSTANCE = new Singleton();  
    }
    
    // 将构造方法设为私有,防止外部直接通过new创建Singleton对象
    private Singleton (){}
    
    // 提供一个公共的静态方法getInstance,用于返回Singleton的唯一实例
    // 由于SingletonHolder是静态内部类,且INSTANCE是静态final变量,因此这里的加载是线程安全的
    public static final Singleton getInstance() {  
        // 直接返回SingletonHolder中的INSTANCE实例,实现懒汉式单例模式
        return SingletonHolder.INSTANCE;  
    }  
}
优点:延迟实例化,节约了资源,且线程安全,性能也被提高了。

枚举类实现

说明:枚举(Enum)是一种特殊的类,用于定义固定的常量集。在Java中,可以通过将枚举类型与实例化代码结合,天然地实现线程安全的单例模式,因为Java中的枚举实例创建是线程安全的,并且枚举类型的实例也是唯一的。

代码:

在C#中,lazy<T>可以实现类似效果。

Java:

public enum Singleton {  
    INSTANCE;  
    public void whateverMethod() {  
    }  
}

优点:

写法简单,线程安全,天然防止序列化和反序列化。

三、感受

系统学完一个设计模式,不仅能够增加了软件开发的最佳实践相关经验,也能够考察对某一编程语言学习的深度和广度。


网站公告

今日签到

点亮在社区的每一天
去签到