UML软件工程组织

 

 

浅析软件项目开发中的单件模式
 
作者: 王翔 出处:IT168
 

前言

单件(Singleton)模式作为设计模式的一个典型且相对简单的构造型模式,已经在很多项目的实际开发中被广泛采用,由于其所有相关操作都是基于同一个实例的引用,因此单件模式的使用主要出于如下几个目的:

逻辑上仅仅只有一个实例才可以保证逻辑的正确性。

通过共享减少内存的使用,作为应用中一些集中的统一数据机制存在。

减少因为频繁的构造过程带来应用上的性能损失。

下面是一个最简单的经典单件模式实现方式:

using System;
namespace VisionLogic.DesignPattern.Practice
...{
public class Singleton
...{
private static Singleton instance;
private Singleton() ...{ }
public static Singleton Instance
...{
get
...{
if (instance == null)
...{instance = new Singleton();
instance.data = 0;
}
return instance;
}
}
/**//// <summary>
/// 共享的实例数据
/// </summary>
private int data;
public int Data
...{
get ...{ return data; }
set ...{ data = value; }
}
}
}

示例1

那么客户端在实际的使用中通过静态的Instance属性就可以获得一个唯一的实例,无论哪一个客户程序对于引用实例的操作都会在其他客户程序中同步更新。

using System;
using System.Diagnostics;
using VisionLogic.DesignPattern.Practice;
namespace VisionLogic.DesignPattern.Practice.Client
...{
class Program
...{
static void Main(string[] args)
...{
Singleton instance1 = Singleton.Instance;
Singleton instance2 = Singleton.Instance;
instance1.Data = 30;
Trace.WriteLine(instance2.Data);
instance2.Data = 15;
Trace.WriteLine(instance1.Data);
}
}
}
结果:
30
15

示例2

单件模式的主要注意事项

上面的例子虽然简单,但实际使用中还需要考虑不要因为面向对象特性和新的开发平台特性,所导致的单件类型性质的变异。

不要实现ICloneable接口或者继承自其相关的子类,否则客户程序可以跳过已经隐蔽起来的类构造函数,导致私有的Singleton() { } 部分的实效。

using System;
namespace VisionLogic.DesignPattern.Practice
...{
public class BaseEntity : ICloneable
...{
public object Clone()
...{
return this.MemberwiseClone();
}
}
public class Singleton : BaseEntity
...{
private static Singleton instance;
private Singleton() ...{ }
public static Singleton Instance
...{
get
...{
if (instance == null)
...{instance = new Singleton();
instance.data = 0;
}
return instance;
}
}
….
}
}

示例3

using System;
using System.Diagnostics;
using VisionLogic.DesignPattern.Practice;
namespace VisionLogic.DesignPattern.Practice.Client
...{
class Program
...{
static void Main(string[] args)
...{
Singleton instance1 = Singleton.Instance;
Singleton instance2 = (Singleton)instance1.Clone();
instance1.Data = 30;
Trace.WriteLine(instance2.Data);
instance2.Data = 15;
Trace.WriteLine(instance1.Data);
}
}
}
结果:
0
30

示例4

上面的示例说明通过ICloneable接口的克隆过程导致私有构造函数的失效,CLR通过内存结构的复制生成了一个新的实例,最终导致不同实例的引用操作结果的各自独立。

上面的单件实现虽然采取了无参数的私有构造函数,但是实际工程中对于第一个且唯一一个实例的构造可能会依赖于具体的参数,不过由于每个客户程序在真正使用时使用的都是既得的实例,因此实际上客户程序不应该把构造参数传递给唯一个实例的构造函数,实际的参数传递过程一般只能通过硬编码在单件类型的静态属性Instance中完成。考虑到代码的维护性问题,笔者建议保留实例的无参数构造函数,而把需要传递的参数通过配置系统传递到唯一实例的构造函数中,例如这里增加了三个配置值:

<configuration>
<configSections>
<section name="sampleSection"
type="System.Configuration.SingleTagSectionHandler" />
</configSections>
<sampleSection setting1="Value1"
setting2="value two"
setting3="third value" />
</configuration>

示例5

作为一个集中访问的单独实例,有时候常常需要对于自身数据进行更新,更新的方式可以通过外部显示调用或者单件实例对象自主更新方式两种方式完成。

如果准备采用外部调用,那么工程上最好通过增加一个接口(例如:名为 IRefershable)为应用中每个Singleton类型统一增加更新的可能。与上一个说明同理,这里刷新过程也最好统一设计为无参数的方法,待刷新所需要的数据源最好来自于一个配置文件或者来自运行环境。

using System;
namespace VisionLogic.DesignPattern.Practice
...{
/**//// <summary>
/// 为Singleton 对象增加可以更新的能力
/// </summary>
interface IRefershable
...{
void Refersh();
}
class Singleton : IRefershable
...{
...
}
}

示例6

内部方式则是可以为其增加一个System.Threading.Timer或者通过配置文件的Watcher的回调代理完成,下面是一个增加了Timer的内部自发更新示例:

using System;
namespace VisionLogic.DesignPattern.Practice
...{
public class Singleto
n...{
private static Singleton instance;
private System.Threading.Timer timer;
private const int Interval = 2000;
private Singleton() ...{ }
public static Singleton Instance
...{
get
...{
if (instance == null)
...{
instance = new Singleton();
instance.timeStamp = System.DateTime.Now;
instance.timer = new System.Threading.Timer(
instance.RefershTimeStamp,
null,
0,
Interval);
}
return instance;
}
}
/**//// <summary>
/// 共享的实例数据
/// </summary>
private DateTime timeStamp;
public DateTime TimeStamp
...{
get ...{ return timeStamp; }
}
/**//// <summary>
/// timer 的回调方法
/// </summary>
/// <param name="state"></param>
private void RefershTimeStamp(object state)
...{
timeStamp = System.DateTime.Now;
}
}
}

示例7

对于较为大型的应用,一般部署上会考虑采用NLB集群,这样如果Singleton其中需要保存一些公共的内存对象(例如:计数器),但是由于在每个服务器节点上各自都有Singleton,所以总体来看Singleton并不“单件”,那么应用对于单一实例的依赖会因为物理节点的非“单件”破坏,从本质上讲采用Singleton模式本身会对于应用的水平扩展(Scale Out)形成障碍。

如果工程上要对NLB集群中的Singleton对象继续保持其“单件”,那么可以采用集中保存其属性信息(或State信息)。不仅如此,对于如果需要通过外界修改这些属性,那么还需要在外部增加一个串行的集中属性信息更新机制。

图:NLB环境下的多节点Singleton实现

对于跨AppDomain的访问,往往需要把复杂的对象通过串行化后进行传递,但是串行化本身会导致单件特性的破坏。因为等于串行化完成了单件对象的“深拷贝”,所以一定不能对于单件对象声明SerializableAttribute属性。下面是一个错用情形示例——为单件类型增加了SerializableAttribute属性。

using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
namespace VisionLogic.DesignPattern.Practice
...{
[Serializable]
public class Singleton
...{
private static Singleton instance;
private static BinaryFormatter formatter = new BinaryFormatter();
private Singleton() ...{ }
public static Singleton Instance
...{
get
...{
if (instance == null)
...{
instance = new Singleton();
instance.data = 0;
}
return instance;
}
}
/**//// <summary>
/// 共享的实例数据
/// </summary>
private int data;
public int Data
...{
get ...{ return data; }
set ...{ data = value; }
}
/**//// <summary>
/// 把Singleton 实例通过二进制串行化为字符串
/// </summary>
/// <param name="graph"></param>
/// <returns></returns>
public static string SerializeToString(Singleton graph)
...{
MemoryStream memoryStream = new MemoryStream();
formatter.Serialize(memoryStream, graph);
Byte[] arrGraph = memoryStream.ToArray();
return Convert.ToBase64String(arrGraph);
}
/**//// <summary>
/// 通过二进制反串行化从字符串回复出Singleton 实例
/// </summary>
/// <param name="serializedGraph"></param>
/// <returns></returns>
public static Singleton DeserializeFromString(string serializedGraph)
...{
Byte[] arrGraph = Convert.FromBase64String(serializedGraph);
MemoryStream memoryStream = new MemoryStream(arrGraph);
return (Singleton)formatter.Deserialize(memoryStream);
}
}
}
using System;
using System.Diagnostics;
using VisionLogic.DesignPattern.Practice;
namespace VisionLogic.DesignPattern.Practice.Client
...{
class Program
...{
static void Main(string[] args)
...{
Singleton instance1 = Singleton.Instance;
string serializedInstance =
Singleton.SerializeToString(instance1);
Singleton instance2 =
Singleton.DeserializeFromString(serializedInstance);
instance1.Data = 30;
Trace.WriteLine(instance2.Data);
instance2.Data = 15;
Trace.WriteLine(instance1.Data);
}
}
}
结果:
0
30

示例8:错用SerializableAttribute导致的单件特性的丧失

示例结果清楚地表明instance1和instance2其实是不同的实例对象。同样的,如果单件实例对象通过Remoting在服务器和客户端传递,将会把这个非单件的效果在不同的计算节点间传递。

多线程环境下的单件模式

其实在上面的讨论中,笔者并没有对多线程应用的单件模式保护进行讨论。可以说即便有了私有的构造函数,即便避免了ICloneable接口和SerializableAttribute属性的错误使用,一样会因为并发的instance == null错误判断导致多个instance = new Singleton()语句的执行,最终形成多个实例的出现,为此需要在访问instance == null之前需要增加一个锁来限制单件构造函数的构造过程。

using System;
namespace VisionLogic.DesignPattern.Practice
...{
public class Singleton
...{
private static volatile Singleton instance;
private static object root = new object();
private Singleton() ...{ }
public static Singleton Instance
...{
get
...{
lock (root)
...{
if (instance == null)
instance = new Singleton();
}return instance;
}
}
}
}

示例9

这里通过简单增加一个lock(root)判断来保证即便运行在多线程环境下,即便并发的调用进入了通过Instance静态属性获得单件引用的情况下,Singleton实例的构造过程唯一性。

但是,这个实现其时存在很大的性能隐患,尤其对于频繁短调用的应用情形而言,一个锁把整个系统的调用归为串行,不管设备节点有多少个N核的CPU。应用其他部分有多大的并发可能执行到这里只能串行,一个锁成了整个应用的瓶颈。为了避免这个情况的出现,需要在lock之前再为其增加一个instance == null的检查,保证instance一旦建立,后面的客户程序调用可以继续以并发方式执行。或者就彻底把这个唯一instance的构造过程通过readonly限制放在一个只会唯一执行一次的某个过程中——Singleton类的静态构造函数中。

using System;
namespace VisionLogic.DesignPattern.Practice
...{
public class Singleton
...{
private static volatile Singleton instance;
private static object root = new object();
private Singleton() ...{ }
public static Singleton Instance
...{
get
...{
if (instance == null)
...{
lock (root)
...{
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
}
}

示例10:通过两次instace == null检查保证高效的单件实例获取

using System;
namespace VisionLogic.DesignPattern.Practice
...{
public class Singleton
...{
public static readonly Singleton Instance = new Singleton();
}
}

示例11:微软MSDN站点上提供的通过静态只读实例方式实现的多线程安全的单件模式

后者虽然在实现代码上仅仅简化为一句,但是通过分析其IL就可以了解他为什么可以保证即便在多线程环境下仍然可以保证单件。

图:通过只读静态构造函数完成的单件模式编译结果

其中,请注意.class public auto ansi beforefieldinit部分,它等于CLR内置的静态构造机制帮助我们完成了加锁过程,确保了只有静态的对象在默认提供的静态构造器创建之前,实例的所有属性不能够被访问。而唯一实例静态的Instance则可以安安稳稳地在默认的静态构造器中创建。下面是编译器生成的静态构造函数。

.method private hidebysig specialname rtspecialname static
void .cctor() cil managed
...{
// Code size 11 (0xb)
. maxstack 8
IL_0000: newobj
instance void VisionLogic.DesignPattern.Practice.Singleton::.ctor()
IL_0005: stsfld
class VisionLogic.DesignPattern.Practice.Singleton
VisionLogic.DesignPattern.Practice.Singleton::Instance
IL_000a: ret
} // end of method Singleton::.cctor
 

组织简介 | 联系我们 |   Copyright 2002 ®  UML软件工程组织 京ICP备10020922号

京公海网安备110108001071号