细颗粒度Singleton模式实现

转载 2007年09月30日 08:11:00

作者 王翔(Vision Wang) 发布于 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;
                }
            }
        }
    }
}

小结

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

每天一题(48) - C++实现Singleton模式

饿汉模式 代码(1) //.h文件 class Singleton { public: static Singleton& GetInstance(); private: Singleton(){...
  • insistGoGo
  • insistGoGo
  • 2013年07月22日 19:37
  • 3385

C#设计模式——单例模式(Singleton)

一、引言 最近在设计模式的一些内容,主要的参考书籍是《Head First 设计模式》,同时在学习过程中也查看了很多博客园中关于设计模式的一些文章的,在这里记录下我的一些学习笔记,一是为了帮助我更深...
  • ycl295644
  • ycl295644
  • 2015年07月06日 08:53
  • 2021

C++设计模式5--单例模式Singleton--当前对象只有一个实例

很多情况下,我们在开发项目的过程中,都希望自己运行的某个部件只有一个实例, 比如我们天天用QT开发界面,QTCreate里帮助菜单下的关于Qt Create菜单,弹出来的关于对话框,在QTCreate...
  • gatieme
  • gatieme
  • 2014年01月08日 13:25
  • 21431

设计模式-单例模式(Singleton)各种写法和分析比较

介绍单例模式是设计模式中比较简单容易理解的。它的出现主要是: 保证一个类仅有一个实例,并提供一个访问它的全局访问点 其实就在系统运行期间中保证只有这么一个实例,并能够全局访问。应用场景就是当需要一...
  • Card361401376
  • Card361401376
  • 2016年05月07日 23:51
  • 3812

spring 与设计模式(创建型)之单例Singleton

一、前言 Singleton 模式主要作用是保证Java程序中,一个类Class只有一个实例存在,例如在数据中连接,全局计数器。另外Singleton也有能够无状态化,提供工具的性质功能。 二、两种形...
  • ice_grey
  • ice_grey
  • 2015年10月29日 01:14
  • 229

Android设计模式之单例模式 Singleton

一.概述 单例模式是设计模式中最简单的一种,但是它没有设计模式中的那种各种对象之间的抽象关系,所以有人不认为它是一种模式,而是一种实现技巧.单例模式就像字面的意思一样,提供一个只能自己实例化的实例...
  • l2show
  • l2show
  • 2015年07月01日 13:21
  • 12802

设计一个线程安全的单例(Singleton)模式

在设计单例模式的时候,虽然很容易设计出符合单例模式原则的类类型,但是考虑到垃圾回收机制以及线程安全性,需要我们思考的更多。有些设计虽然可以勉强满足项目要求,但是在进行多线程设计的时候。不考虑线程安全性...
  • gggg_ggg
  • gggg_ggg
  • 2015年12月08日 09:43
  • 1835

Linux系统C++中多线程Singleton的实现

我想关于Singleton模式的实现和资料很多很多,这里为什么专门拿出来写一写,还是因为个人觉得要想把单例模式写好还真不是一件容易的事情。 其中涉及到不少编译和底层的知识。这里以Linux平台为例,...
  • acs713
  • acs713
  • 2014年06月04日 16:26
  • 1364

C++中实现singleton(单例模式)的最简单写法

前几天看了 Java中实现singleton的写法,就想在C++中实现一下,找了很多资料,看了各个牛人写的不同版本,但最后在stack overflow上找到了一个最简单的写法,现在贴出来以供参考: ...
  • flybird19870326
  • flybird19870326
  • 2014年03月04日 16:50
  • 1897

单例模式中的懒汉模式和饿汉模式的最优写法

废话不多说,直接上最优写法: 懒汉模式: class Singleton { private volatile static Singleton singleton; pri...
  • liufangbaishi2014
  • liufangbaishi2014
  • 2016年11月07日 22:01
  • 453
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:细颗粒度Singleton模式实现
举报原因:
原因补充:

(最多只允许输入30个字)