细颗粒度Singleton模式实现

2007 年 9 月 27 日

背景讨论

作为一个很典型的设计模式,Singleton 模式常常被用来展示设计模式的技巧,并且随着技术的演进,.NET 语言和 Java 都已经把经典《Design Patterns : Elements of Reusable Object-Oriented Software》中所定义的 Singleton 模式作了完善,例如 C#可以通过这样一个非常精简但又很完美的方式实现了一个进程内部线程安全的 Singleton 模式。

C# 最经典 Singleton 模式的实现(Lazy 构造方式)- - - - - -

public class Singleton
{
private static Singleton instance; // 唯一实例
protected Singleton() { } // 封闭客户程序的直接实例化
public static Singleton Instance
{
get
{
if (instance == null)
instance = new Singleton();
return instance;
}
}
}

C# 通过 Double Check 实现的相对线程安全的 Singleton 模式- - - - - -

public class Singleton
{
protected Singleton() { }
private static volatile Singleton instance = null;
/// Lazy 方式创建唯一实例的过程
public static Singleton Instance()
{
if (instance == null) // 外层 if
lock (typeof(Singleton)) // 多线程中共享资源同步
if (instance == null) // 内层 if
instance = new Singleton();
return instance;
}
}

C#充分依靠语言特性实现的间接版 Singleton 模式 - - - - - - class Singleton
{
private Singleton() { }
public static readonly Singleton Instance = new Singleton();
}

但项目中我们往往需要更粗或者更细颗粒度的 Singleton,比如某个线程是长时间运行的后台任务,它本身存在很多模块和中间处理,但每个线程都希望有自己的线程内单独 Singleton 对象,其他线程也独立操作自己的线程内 Singleton,所谓的线程级 Singleton 其实他的实例总数 = 1(每个线程内部唯一的一个) * N (线程数)= N。

.NET 程序可以通过把静态成员标示为 System. ThreadStaticAttribute 就可以确保它指示静态字段的值对于每个线程都是唯一的。但这对于 Windows Form 程序很有效,对于 Web Form、ASP.NET Web Service 等 Web 类应用不适用,因为他们是在同一个 IIS 线程下分割的执行区域,客户端调用时传递的对象是在 HttpContext 中共享的,也就是说它本身不可以简单地通过 System. ThreadStaticAttribute 实现。不仅如此,使用 System. ThreadStaticAttribute 也不能很潇洒的套用前面的内容写成:

C#- - - - - -

[ThreadStatic]
public static readonly Singleton Instance = new Singleton();

因为按照.NET 的设计要求不要为标记为它的字段指定初始值,因为这样的初始化只会发生一次,因此在类构造函数执行时只会影响一个线程。在不指定初始值的情况下,如果它是值类型,可依赖初始化为其默认值的字段,如果它是引用类型,则可依赖初始化为 null。也就是说多线程情况下,除了第一个实例外,其他线程虽然也期望通过这个方式获得唯一实例,但其实获得就是一个 null,不能用。

解决 Windows Form 下的细颗粒度 Singleton 问题

对于 Windows Forms 下的情况,可以通过 System. ThreadStaticAttribute 比较容易的高速 CLR 其中的静态唯一属性 Instance 仅在本线程内部静态,但麻烦的是怎么构造它,正如上面背景介绍部分所说,不能把它放到整个类的静态构造函数里,也不能直接初始化,那么怎么办?还好,那个很 cool 的实现这里不适用的话,我们就退回到最经典的那个 lazy 方式加载 Singleton 实例的方法。你可能觉得,这线程不安全了吧?那种实现方式确实不是线程安全,但我们这里的 Singleton 构造本身就已经运行在一个线程里面了,用那种不安全的方式在线程内部实现只有自己“一亩三分地”范围内 Singleton 的对象反而安全了。新的实现如下:

C#- - - - - -

public class Singleton
{
private Singleton() { } [ThreadStatic] // 说明每个 Instance 仅在当前线程内静态

private static Singleton instance; public static Singleton Instance

{
get
{
if (instance == null)
instance = new Singleton();
return instance;
}
}
}

Unit Test - - - - - -

/// 每个线程需要执行的目标对象定义
/// 同时在它内部完成线程内部是否 Singleton 的情况
class Work
{
public static IList Log = new List();
/// 每个线程的执行部分定义
public void Procedure()
{
Singleton s1 = Singleton.Instance;
Singleton s2 = Singleton.Instance;
// 证明可以正常构造实例
Assert.IsNotNull(s1);
Assert.IsNotNull(s2);
// 验证当前线程执行体内部两次引用的是否为同一个实例
Assert.AreEqual(s1.GetHashCode(), s2.GetHashCode());
// 登记当前线程所使用的 Singleton 对象标识
Log.Add(s1.GetHashCode());
}
}[TestClass]

public class TestSingleton
{
private const int ThreadCount = 3;
[TestMethod]
public void Test()
{
// 创建一定数量的线程执行体
Thread[] threads = new Thread[ThreadCount];
for (int i = 0; i < ThreadCount; i++)
{
ThreadStart work = new ThreadStart((new Work()).Procedure);
threads[i] = new Thread(work);
}
// 执行线程
foreach (Thread thread in threads) thread.Start(); // 终止线程并作其他清理工作

// … … // 判断是否不同线程内部的 Singleton 实例是不同的

for (int i = 0; i < ThreadCount - 1; i++)
for (int j = i + 1; j < ThreadCount; j++)
Assert.AreNotEqual(Work.Log[i], Work.Log[j]);
}
}

下面我们分析一下单元测试代码说明的问题:

  • 在 Work.Procedure() 方法中,两次调用到了 Singleton 类的 Instance 静态属性,经过验证是同一个 Singleton 类实例。同时由于 Singleton 类的构造函数定义为私有,所以线程(客户程序)无法自己实例化 Singleton 类,因此同时满足该模式的设计意图;
  • 通过对每个线程内部使用的 Singleton 实例登记并检查,确认不同线程内部其实掌握的是不同实例的引用,因此满足我们需要实现的细颗粒度(线程级)的意图;
  • 解决 Web Form 下细颗粒度 Singleton 问题。

上面用 ThreadStatic 虽然解决了 Windows Form 的问题,但对于 Web Form 应用而言并不适用,原因是 Web Form 应用中每个会话的本地全局区域不是线程,而是自己的 HttpContext,因此相应的 Singleton 实例也应该保存在这个位置。实现上我们只需要做少许的修改,就可以完成一个 Web Form 下的细颗粒度 Singleton 设计:

注:这里的 Web Form 应用包括 ASP.NET Application、ASP.NET Web Service、ASP.NET AJAX 等相关应用。但示例并没有在.NET Compact Framework 和.NET Micro Framework 的环境下进行过验证。> C#- - - - - -

public class Singleton
{
/// 足够复杂的一个 key 值,用于和 HttpContext 中的其他内容相区别
private const string Key = “just.complicated…singleton”;
private Singleton() { } public static Singleton Instance

{
get
{
// 基于 HttpContext 的 Lazy 实例化过程
Singleton instance = (Singleton)HttpContext.Current.Items[Key];
if (instance == null)
{
instance = new Singleton();
HttpContext.Current.Items[Key] = instance;
}
return instance;
}
}
}

Unit Test - - - - - -

using System;
using System.Web;
using MarvellousWorks.PracticalPattern.SingletonPattern.WebContext;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace SingletonPattern.Test.Web
{
public partial class _Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
Singleton s1 = Singleton.Instance;
Singleton s2 = Singleton.Instance;
// 确认获得的 Singleton 实例引用确实已经被实例化了
Assert.IsNotNull(s1);
Assert.IsNotNull(s2);
// 确认两个引用调用的是同一个 Singleton 实例
Assert.AreEqual(s1.GetHashCode(), s2.GetHashCode());
// 显示出当前 Singleton 实例的标识,用于比较与其他
// HttpContext 环境下的 Singleton 实例其实是不同的实例
instanceHashCode.Text = s1.GetHashCode().ToString();
}
}
}

浏览器效果- - - - - -

同上,这段单元测试验证了 Web Form 下的细颗粒度 Singleton,通过将唯一实例的存储位置从当前线程迁移到 HttpContext,一样可以实现细颗粒度的 Singleton 设计意图。

更通用的细颗粒度 Singleton

但如果你是一个公共库或者是公共平台的设计者,您很难预料到自己的类库会运行在 Windows Form 还是 Web Form 环境下,但 Singleton 模式作为很多公共机制,最常用的包括技术器、时钟等等又常常会成为其他类库的基础,尤其当涉及到业务领域逻辑的时候,很难在开发过程就约定死运行的模式。怎么办?

这里借助一个工具类,通过它判断当前执行环境是 Web Form 还是 Windows Form,然后作一个 2 in 1 的细颗粒度 Singleton(,听起来有点象早年的任天堂游戏卡),不过就像我们提到的面向对象设计的单一职责原则一样,把两个和在一起会产生一些比较难看的冗余代码,但 Singleton 与其他设计模式有个很显著的区别——他不太希望被外部机制实例化,因为他要保持实例的唯一性,因此一些常用的依赖倒置技巧在这里又显得不太适用。这里实现一个稍有些冗余的 Web Form + Windows Form 2 in 1 的细颗粒度 Singleton 如下:

UML- - - - - -

C# 工具类 GenericContext- - - - - -

/// 判断当前应用是否为 Web 应用的 Helper 方法(非官方方法)
private static bool CheckWhetherIsWeb()
{
bool result = false;
AppDomain domain = AppDomain.CurrentDomain;
try
{
if (domain.ShadowCopyFiles)
result = (HttpContext.Current.GetType() != null);
}
catch (System.Exception){}
return result;
}

C# 2in 1 的细颗粒度 Singleton 模式实现- - - - - -

using System;
using System.Web;
using MarvellousWorks.PracticalPattern.Common;
namespace MarvellousWorks.PracticalPattern.SingletonPattern.Combined
{
public class Singleton
{
private const string Key = “marvellousWorks.practical.singleton”;
private Singleton() { } // 对外封闭构造
[ThreadStatic]
private static Singleton instance; public static Singleton Instance

{
get
{
// 通过之前准备的 GenericContext 中非官方的方法
// 判断当前执行模式是 Web Form 还是非 Web Form
// 本方法没有在 .NET 的 CF 和 MF 上验证过
if (GenericContext.CheckWhetherIsWeb()) // Web Form
{
// 基于 HttpContext 的 Lazy 实例化过程
Singleton instance = (Singleton)HttpContext.Current.Items[Key];
if (instance == null)
{
instance = new Singleton();
HttpContext.Current.Items[Key] = instance;
}
return instance;
}
else // 非 Web Form 方式
{
if (instance == null)
instance = new Singleton();
return instance;
}
}
}
}
}

小结

设计模式中很多意图部分表述的要求其实也都是有语意范围 的,比如说“唯一”、“所有相关”、“一系列相互依赖的”等,但项目中往往有自己定制化的要求,可能的话建议尽量用语言、语言运行环境的特性完成这些工作。

2007 年 9 月 27 日 21:51 1376
用户头像

发布了 61 篇内容, 共 46655 次阅读, 收获喜欢 0 次。

关注

评论

发布
暂无评论
发现更多内容

基于阿里云服务网格(ASM)的GRPC服务部署实践

韩陆

Kubernetes gRPC Service Mesh

近两年流行面试题:Spring循环依赖问题

Java小咖秀

spring 面试题 ioc

2020年6月26日 查询性能优化

瑞克与莫迪

Oracle SQL调优系列之看懂执行计划explain

Nicky.Ma

sql

极客大学架构师训练营 框架开发 模式与重构 JUnit、Spring、Hive核心源码解析 第6课

John(易筋)

spring 极客时间 极客大学 极客大学架构师训练营 JUnit

极客大学架构师训练营 系统架构 第7课 听课总结

John(易筋)

极客时间 系统架构 高并发 极客大学 极客大学架构师训练营

区块链系列教程之:比特币中的挖矿

程序那些事

比特币 区块链 挖矿

​外包公司干了不到3个月,我离职了...(防坑指南)

程序员生活志

程序员 外包 程序员人生 工作经历

创业一定要学投资

Neco.W

创业 投资

Zookeeper集群模式启动

tunsuy

zookeeper 源码分析 socket 分布式集群

Zookeeper通信协议详解

tunsuy

zookeeper TCP/IP 通信协议

Zookeeper的数据剖析

tunsuy

zookeeper 日志分析 事务 快照 数据恢复

架构师训练营第四周

Melo

架构师训练营第三周命题作业

lwy

极客大学架构师训练营

HelloWorld.go

吐核hú

go 学习笔记 TraceLog

第三周-设计模式-学习总结

吴建中

极客大学架构师训练营

Why Spring ???

猴哥一一 cium

spring 源码 Spring Boot Java、 框架设计

架构师训练营第四周-总结

无心水

极客大学架构师训练营

Docker基础修炼2--Docker镜像原理及常用命令

黑马腾云

Docker Linux 容器 运维 镜像

组合设计模式编码&手写单例模式

吴建中

极客大学架构师训练营

第三周手写单例模式(饿汉模式)

吴建中

极客大学架构师训练营

太赞了!一份适合程序员的精选面试题清单。

JackTian

GitHub 编程 程序员 面试题 开源项目

架构师训练营第三周学习总结

lwy

架构训练营第四周 - 作业

无心水

极客大学架构师训练营

抖音、腾讯、阿里、美团春招服务端开发岗位硬核面试(完结)

aoho

面试 后端 阿里

WPF中的Data Binding调试指南

大白技术控

.net 微软 WPF

架构师第4周

上山砍柴

极客大学架构师训练营

区块链的应用为什么这么难?出路在哪?

CECBC区块链专委会

比特币 区块链技术 Token 联盟共识

架构师是怎样炼成的-3-2-设计模式

闷骚程序员

【非原创】微服务设计

Arthur

新手村:Redis基础补充知识

多选参数

数据库 redis 数据库设计 redis6.0.0

细颗粒度Singleton模式实现-InfoQ