老生常谈的设计模式。
个人对设计模式涉及的不是很多,但是作为一个优秀的程序员我觉得还是有必要对这些充分了解的。
今天从最简单的单体模式学习起:
一,什么是单体模式
Singleton:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
单体模式(Singleton)是经常为了保证应用程序操作某一全局对象,让其保持一致而产生的对象,例如对文件的读写操作的锁定,数据库操作的时候的事务回滚,还有任务管理器操作,都是一单体模式读取的。
对象只要利用自己的属性完成了自己的任务,那该对象就是承担了责任。除了维持了自身的一致性,该对象无需承担其他任何责任。如果该对象还承担着其他责任,而其他对象又依赖于该特定对象所承担的责任,我们就需要得到该特定对象。
将类的责任集中到唯一的单体对象中,确保该类只有一个实例,并且为该类提供一个全局访问点。这就是单体模式的目的。
单体模式的难点不在于单体模式的实现,而在于在系统中任何识别单体和保证单体的唯一性。
二,单体模式的实现
创建一个单体模式类,必须符合三个条件:
1:私有构造函数(防止其他对象创建实例);
2:一个单体类型的私有变量;
3:静态全局获取接口
下面详细谈谈实现的方式:
1,提供唯一的私有构造器,避免多个单体(Singleton)对象被创建,这也意味着该单体类不能有子类,那声明你的单例类为sealed 是一个好主意,这样意图明确,并且让编译器去使用一些性能优化选项。 如果有子类的话使用protected,protected的构造方法可以被其子类以及在同一个包中的其它类调用。 私有构造器可以防止客户程序员通过除由我们提供的方法之外的任意方式来创建一个实例,如果不把构造器声明为private或protected,编译器会自动的创建一个public的构造函数。
2,使用静态域(static field)来维护实例。
将单体对象作为单体类的一个静态域实例化。 使用保存唯一实例的static变量,其类型就是单例类型本身。需要的话使用final,使其不能够被重载。
例如: private static Rutime currentRuntime = new Runtime();
3,使用静态方法(Static Method)来监视实例的创建。
a,加载时实例化
例如:
{
private static Singleton _instance = new Singleton();
private Singleton() { }
public static Singleton getInstance()
{
return _instance;
}
}
b,使用时实例化(惰性初始化):这样做可以在运行时收集需要的信息来实例化单体对象,确保实例只有在需要时才被建立出来。
例如:
{
private object m_mutex = new object();
private bool m_initialized = false;
private SingletonOne m_instance = null;
public SingletonOne()
{
//运行时实例化
}
public SingletonOne Instance
{
get
{
if (!this.m_initialized)
{
lock (this.m_mutex)
{
if (!this.m_initialized)
{
this.m_instance = new SingletonOne();
this.m_initialized = true;
}
}
}
return this.m_instance;
}
}
}
4,单体对象的成员变量(属性):即单体对象的状态 通过单例对象的初始化来实现成员变量的初始化。 通过方法对单体对象的成员变量进行更新操作。
例如:
public class SingletonTwo
{
private static SingletonTwo _instance = null;
private object properties = null;
protected SingletonTwo()
{
//使用运行时收集到的需要的信息,进行属性的初始化等操作。
}
private static void syncInit()
{
if (_instance == null)
{
_instance = new SingletonTwo();
}
}
public static SingletonTwo getInstance()
{
if (_instance == null)
{
syncInit();
}
return _instance;
}
public void updateProperties()
{
// 更新属性的操作。
}
public object getProperties() { return properties; }
}
三,单体对象的同步(单体模式与多线程)
1,单体对象初始化同步
问题: 在多线程模式下,惰性初始化会使多个线程同时初始化该单体,造成一个JVM中多个单例类型的实例, 如果这个单例类型的成员变量在运行过程中发生变化,会造成多个单例类型实例的不一致。
解决的办法:(上面已经给出了例子) 加个同步修饰符: public static synchronized Singleton getInstance(). 这样就保证了线程的安全性. 这种处理方式虽然引入了同步代码,但是因为这段同步代码只会在最开始的时候执行一次或多次,所以对整个系统的性能不会有影响。
2,单体对象属性的同步
问题: 在更新属性的时候,会造成属性的读写不一致。
解决方法:
1,读者/写者的处理方式 设置一个读计数器,每次读取信息前,将计数器加1,读完后将计数器减1。使用notifyAll()解除在该对象上调用wait的线程阻塞状态。只有在读计数器为0时,才能更新数据,同时调用wait()方法要阻塞所有读属性的调用。
2,采用"影子实例"的办法 具体说,就是在更新属性时,直接生成另一个单例对象实例, 这个新生成的单例对象实例将从数据库,文件或程序中读取最新的信息;然后将这些信息直接赋值给旧单例对象的属性。
public class Singleton
{
private int ObjCount = 0;
private Singleton()
{
Console.WriteLine("创建对象");
}
private static Singleton objInstance = null;
public static Singleton getInstance()
{
if (objInstance == null)
objInstance = new Singleton();
return objInstance;
}
public void ShowCount()
{
ObjCount++;
Console.WriteLine("单个对象被调用了{0}次", ObjCount);
}
}
class MulityThreadTest
{
//多线程测试单体模式
public static void RunMoreThread()
{
System.Threading.Thread newThread = new System.Threading.Thread(new System.Threading.ThreadStart(ThreadSingleMethod));
//在线程启动前设置其单元状态。
newThread.SetApartmentState(System.Threading.ApartmentState.MTA);
Console.WriteLine("ThreadState: {0}, ApartmentState: {1},ManagedThreadId:{2}", newThread.ThreadState, newThread.GetApartmentState(), newThread.ManagedThreadId);
newThread.Start();
}
public static void ThreadSingleMethod()
{
Singleton.getInstance().ShowCount();
}
}
四,识别单体模式
1,区别工具类和单体类在于该类是否是有状态的。无状态化,提供工具性质的功能,那就是工具类。
2,是否承担了唯一的责任,并且是否提供了唯一的实例。
五,单体模式的应用
Singleton设计模式可以在应用程序创建一个唯一的全局对象,也就是说,这个对象只能被实例化一次。 应用程序中的窗口管理器或者是数据库连接池等,都是Singleton模式的典型应用。
注明:本文实例有部分来自JAVA,可能没有完全翻译到ASP.NET(C#),请读者自行翻译。
下面再引用一些其它作者的实例:
/// 单键模式的简单实现方式
/// </summary>
public sealed class SampleSingleton1
{
private int m_Counter = 0;
private SampleSingleton1()
{
Console.WriteLine("初始化SampleSingleton1。");
}
public static readonly SampleSingleton1 Singleton = new SampleSingleton1();
/**//// <summary>
/// 调用次数计数器
/// </summary>
public void Counter()
{
m_Counter ++;
}
}
//说明一下,sealed关键字保证了该单键类不会被继承,readonly关键字保证了Singleton实例入口为只读。
/// 单键模式的传统实现方式
/// </summary>
public class SampleSingleton2
{
// 注意:公用变量最好使用volatile关键字,原因参看MSDN
private static volatile SampleSingleton2 m_Instance = null;
private int m_Counter = 0;
private SampleSingleton2()
{
Console.WriteLine("初始化SampleSingleton2。");
}
/**/
/// <summary>
/// 获取单键实例
/// </summary>
public static SampleSingleton2 Singleton
{
get
{
if (m_Instance == null)
{
lock (typeof(SampleSingleton2))
{
if (m_Instance == null)
{
m_Instance = new SampleSingleton2();
}
}
}
return m_Instance;
}
}
/**/
/// <summary>
/// 调用次数计数器
/// </summary>
public void Counter()
{
m_Counter++;
}
}
//注意上面的代码使用了volatile关键字和lock关键字来保证正确创建(即只创建一次)以及正确获取实例。
比较两种实现方式
可以明显看出,方式1的代码少了很多,也没有进行互斥判断以及锁定操作,因此运行速度也有一定的优势。但并不能说方法1就一定比方法2优秀,虽然方法1代码少且运行速度快,但方法1的初始化动作是在整个程序启动之时进行,而方法2的初始化动作是在第一次调用时才进行。所以在具体应用中应根据实际情况的要求来选择实现的方式。
下面是分别对两种实现方式的测试代码:
{
/**//// <summary>
/// 应用程序的主入口点。
/// </summary>
[STAThread]
static void Main(string[] args)
{
DateTime milestone;
int maxCallTimes = 100000000;
// 第一次调用SampleSingleton1的Counter方法
Console.WriteLine("第一次调用SampleSingleton1的Counter方法");
SampleSingleton1.Singleton.Counter();
// 计算10000次调用的耗费时间
milestone = DateTime.Now;
for (int i = 0; i < maxCallTimes; i ++)
{
SampleSingleton1.Singleton.Counter();
}
Console.WriteLine(maxCallTimes.ToString() + "次调用执行时间为:" + ((TimeSpan)(DateTime.Now - milestone)).TotalMilliseconds.ToString());
Console.WriteLine("");
// 第一次调用SampleSingleton2的Counter方法
Console.WriteLine("第一次调用SampleSingleton2的Counter方法");
SampleSingleton2.Singleton.Counter();
// 计算10000次调用的耗费时间
milestone = DateTime.Now;
for (int i = 0; i < maxCallTimes; i ++)
{
SampleSingleton2.Singleton.Counter();
}
Console.WriteLine(maxCallTimes.ToString() + "次调用执行时间为:" + ((TimeSpan)(DateTime.Now - milestone)).TotalMilliseconds.ToString());
string str = Console.ReadLine();
}
}
运行结果如下:
第一次调用SampleSingleton1的Counter方法
100000000次调用执行时间为:1722.4768
第一次调用SampleSingleton2的Counter方法
初始化SampleSingleton2。
100000000次调用执行时间为:3805.472
从运行结果中也可以看出,方法1的初始化动作在应用程序初始化时就进行了;而方法2的初始化动作是在第一次调用时进行。执行时间的单位为毫秒,方法1的效率比方法2的效率要高1倍左右。