单例模式
一、单例模式概述
1.单例模式的定义
单例模式(Singleton Pattern)是一种常用的软件设计模式。在这种模式中,一个类负责创建自己的对象,同时确保只有单个对象被创建了。这个类提供了访问其对象的方式。
2.单例模式的作用
单例模式使得我们在使用一个类及其对象时,不必频繁地进行创建和销毁,提高了程序的性能,节约了系统资源。单例模式常常在以下场景中使用:
-
资源共享: 当应用程序需要共享某个资源(如数据库连接、日志记录器、线程池等)时,可以使用单例模式确保所有对象共享同一个实例,避免资源的重复创建和浪费。
-
配置管理器: 在需要全局访问配置信息的情况下,可以使用单例模式来管理配置信息的加载和访问,确保所有组件都使用相同的配置实例。
-
日志记录器: 单例模式可以用于创建全局的日志记录器,以便在整个应用程序中记录日志并保持日志的一致性。
-
线程池: 在需要管理和控制线程的情况下,可以使用单例模式来创建线程池,确保所有线程共享同一个线程池实例,并且能够动态地调整线程池的大小。
-
缓存管理器: 当需要缓存某些数据以提高应用程序的性能时,可以使用单例模式来创建全局的缓存管理器,以确保所有组件都使用相同的缓存实例。
-
窗口管理器: 在图形用户界面(GUI)应用程序中,可以使用单例模式来创建窗口管理器,以确保所有窗口共享同一个实例,并且能够动态地管理和控制窗口的显示和隐藏。
3.单例模式的优缺点
优点:
- 资源节约:由于系统中只存在一个对象,可以节约系统资源,尤其是对象创建和销毁成本高时,单例模式可以提高系统性能。
- 全局访问点:提供了对唯一实例的受控访问,方便全局调用。
- 共享资源:适用于控制资源访问,如配置管理和日志记录,确保资源的协调与共享。
- 避免重复实例:防止其他对象对自己的实例化,确保所有对象都访问同一个实例。
缺点:
- 扩展困难:由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
- 职责过重:单例类的职责过重,可能违背了“单一职责原则”,一个类既负责业务逻辑又负责生命周期管理。
- 灵活性受限:不利于代码的测试,特别是依赖于单例类的代码。在单元测试中,需要模拟不同的单例对象,单例模式的单一实例特性对此造成了阻碍。
- 多线程问题:在多线程环境下,如果不正确实现,可能会导致多个实例被创建,违反单例模式的原则。同时,在高并发的场景下,由于锁的存在,可能会导致线程阻塞,性能下降。
二、单例模式的实现
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() {
}
}
优点:
写法简单,线程安全,天然防止序列化和反序列化。
三、感受
系统学完一个设计模式,不仅能够增加了软件开发的最佳实践相关经验,也能够考察对某一编程语言学习的深度和广度。