• 设计模式:单例(Singleton)


    转载必需注明出处:http://www.cnblogs.com/wu-jian

    前言

    单例模式是最早了解并经常使用到的设计模式之一,很早就想将其整理成文,但因为对一些细节准备尚未充分,一再延误。本文通过实例与代码,分析了单例模式的需求、原理、实现,以及探讨在B/S开发中单例模式与性能优化。对自己学习的总结,也希望给学习设计模式的朋友带来帮助。同时个人能力有限,文中如有不足之处请及时指正。

    为什么需要单例模式

    首先看看单例模式的定义:单例模式属于对象创建型模式,其特征可以概括为如下三点:

    1、保证一个类仅有一个实例

    2、必须自已创建自己的唯一实例

    3、提供唯一实例的全局访问点

    单例模式的应用场景很多,打印机程序能比较简单的说明问题:假使有一台打印机,我们写了一个打印程序,如果这个打印程序能被到处new(),甲new出来一个,乙new出来一个,甲还没打印完,乙又开始打印,那就乱了套。所以打印机程序必须使用单例模式,即只有一个对象与打印机通讯,甲乙丙丁同时打印,那就在单例对象中排队吧。

    如果要通过一个最简单的例子来理解单例,我想奥巴马较具代表性:单例模式就好比美国总统,美国总统只能存在一个,如果存在多个?估计中国人民尤其是朝鲜人民会比较高兴。

    单例模式的饿汉实现方式

    单例模式的实现分为饿汉和懒汉两种方式,先看饿汉,代码如下: 

    复制代码
    //饿汉
        public sealed class HungryMan
        {
            //类加载时实例化
            private static HungryMan mInstance = new HungryMan();
    
            //私有构造函数
            private HungryMan() { }
    
            //简单工厂方式提供全局访问点
            public static HungryMan Instance
            {
                get { return mInstance; }
            }
        }
    复制代码

    饿汉为实现单例最为简单的方式,它也是典型的空间换时间,当类被加载即创建实例,而不论这个实例是否需要使用。以后使用实例时,均不再进行判断,节省了运行时间,但占用了空间。一些使用频繁的对象适合使用饿汉方式。

    要求完美的饿汉

    如果你不是完美主义者,可以忽略本节,因为本节的代码在大多数情况下并不能带来性能的提升。

    在使用饿汉方式时,C#并不保证实例的创建时机,如下:

    private static HungryMan mInstance = new HungryMan();

    静态字段可能在类被加载时赋值,也可能在被调用之前赋值,总之我们不能确定到底什么时候创建类的实例。于是有完美主义者提出,这种CLR机制导致的不确定性会带来性能的损耗,能不能做到mInstance在被调用前的瞬间初始化,这样就可以节省了一段时间的内存开销。于是有饿汉的优化版本如下:

    复制代码
    //要求完美的饿汉
        public sealed class PerfectHungryMan
        {
            private static readonly PerfectHungryMan mInstance = new PerfectHungryMan();
    
            private PerfectHungryMan() { }
    
            //通过静态构造函数实现延迟初始化
            static PerfectHungryMan() { }
    
            public static PerfectHungryMan Instance
            {
                get { return mInstance; }
            }
        }
    复制代码

    如代码中的注释,通过静态构造函数实现了类的延迟初始化(即被调用之前初始化)。对比两个类生成的中间代码,可以看到只有一处不同:PerfectHungryMan比HungryMan少了一个特性:beforefieldinit,也就是说静态构造函数抑制了beforefieldinit 特性,而该特性会影响类的初始化,获取IL如下图所示:

    包含beforefieldinit的类会由CLR选择合适的时机来初始化;不包含beforefieldinit的类会被强制在调用前初始化。如本节开头所描述的,大多数情况下废弃beforefieldinit延迟类的初始化并不能带来性能的提升,或提升的性能也是微乎其微。

    所以除非特殊情况,否则我们没必要捡了芝麻丢了西瓜。

    单例模式的懒汉实现方式(非线程安全)

    懒汉即需要时才创建,如下代码所示:

    复制代码
    //懒汉
        public sealed class LazyMan
        {
            private static LazyMan mInstance = null;
    
            private LazyMan() { }
    
            //需要时实例化
            public static LazyMan Instance
            {
                get{
                    if (mInstance == null)
                        mInstance = new LazyMan();
                    return mInstance;
                }
            }
        }
    复制代码

    但代码中存在一个问题,即多线程环境下当两个以上请求同时调用时,会创建出多个对象,这违反了单例的基本原则。

    线程安全的懒汉

    复制代码
    //线程安全的懒汉
        public sealed class MultiLazyMan
        {
            private static MultiLazyMan mInstance = null;
            private static readonly object syncLock = new Object();
    
            private MultiLazyMan() { }
    
            //需要时实例化
            public static MultiLazyMan Instance
            {
                get{
                    //确保单线程访问
                    lock (syncLock)
                    {
                        if (mInstance == null)
                            mInstance = new MultiLazyMan();
                        return mInstance;
                    }
                }
            }
        }
    复制代码

    以上代码的实现是线程安全的,首先创建了一个静态只读的进程辅助对象,lock确保当一个线程位于代码的临界区时,另一个线程不能进入临界区(同步操作)。如果其他线程试图进入锁定的代码,则将一直等待,直到该对象被释放。从而确保在多线程下不会创建多个对象实例。

    这种实现方式确保了多线程环境下实例的唯一性,但从代码中可以发现,每个线程都需占用lock,如果一个WEB程序有100个请求同时到达,就要lock 100次,并且始终有人排队。从性能上来说,这种方式的效率低且性能开销大。

    完美的懒汉

    其实在多线程环境下我们只需在第一次创建实例时使用lock来确保实例唯一。实例创建出来以后,完全可以大家公用,那就加一行小判断,如下代码:

    复制代码
    //完美的懒汉
        public sealed class PerfectLazyMan
        {
            //volatile 关键字指示一个字段可以由多个同时执行的线程修改。
            //声明为 volatile 的字段不受编译器优化(假定由单个线程访问)的限制。
            //这样可以确保该字段在任何时间呈现的都是最新的值。 
            private static volatile PerfectLazyMan mInstance = null;
            private static readonly object syncLock = new Object();
    
            private PerfectLazyMan() { }
    
            public static PerfectLazyMan Instance
            {
                get
                {
                    //判断一下首次创建实例时才进行lock
                    if (mInstance == null)
                    {
                        //确保单线程访问
                        lock (syncLock)
                        {
                            if (mInstance == null)
                                mInstance = new PerfectLazyMan();
                        }
                    }
                    return mInstance;
                }
            }
        }
    复制代码

    OK,通过一行举手之劳的判断得到了完美的懒汉。

    如果查查资料,这个举手之劳的判断居然有个看似高深莫测的专门术语:双重检查成例(Double Check Idiom)

    该术语是由C语言搬到JAVA,然后再从JAVA搬到C#。有幸在阎宏的《JAVA与模式》中读到相关细节,顺便也推荐下这本书,虽然厚了点,写得还是很好,如果作者教条主义再少一点,自由发挥再多一点,这本书就该是中文设计模式类书籍的典范了。

    通过Lazy<T>实现懒汉

    复制代码
    //Lazy<T>
        public sealed class GenericLazyMan
        {
            private static readonly Lazy<GenericLazyMan> mInstance = new Lazy<GenericLazyMan>(() => new GenericLazyMan());
    
            private GenericLazyMan() { }
    
            public static GenericLazyMan Instance
            {
                get
                {
                    return mInstance.Value;
                }
            }
        }
    复制代码

    Lazy<T>是.Net Framework 4.x提供的一个针对大对象延迟加载的封装,它提供了系列便捷功能,同时提供了线程安全,此处不作详述。

    Lazy<T>参考资料:http://msdn.microsoft.com/en-us/library/dd997286(VS.100).aspx

    单例模式与性能

     在B/S开发中我想每个人都写过类似如下的代码:

    protected void Page_Load(object sender, EventArgs e)
            {
                businessObject = new Somewhere.BusinessObject();
                businessObject.DoSomething();
            }

    创建一个业务对象,然后调用对象的方法来完成一些操作。

    因为.Net、Java等高级语言中内置了垃圾回收机制,我们可以把并发的内存开销完全交由GC(Garbage Collection,垃圾回收)来打理,所以如上的代码在大多数情况下不会出现问题,以至于逐渐让我们遗忘了并发、遗忘了内存、遗忘了性能。

    B/S开发是面向多用户处理并发请求的,在Page_Load中new出来的对象针对每一次请求。当1000人同时访问一个页面,我们实际就new出了1000个对象放在内存中,如果不凑巧这个对象有10M以上,那就代表了内存开销将大于10G,这是一个恐怖的数字,只是我们不常碰到1000的并发和10M的对象,当然,我们还有垃圾回收在保驾护航。

    下面我模拟了一个1.5M左右的对象,100的并发,看看如下的CPU和内存曲线图:

    首先CPU飙升,然后内存开销出现规律的峰谷。很明显,每个峰顶代表垃圾回收开始,每个谷底代表垃圾回收结束。垃圾回收虽然给我们带来了便捷,但其性能损耗也是妇孺皆知。附上本次测试的源代码:

    复制代码
    namespace WuJian.DesignModel.Singleton
    {
        public class TestObject
        {
            //加载一个1.5M的图片
            public TestObject()
            {
                this.mData = System.Drawing.Image.FromFile(HttpRuntime.AppDomainAppPath + @"app_data\1500k.jpg");
            }
    
            private System.Drawing.Image mData;
    
            public System.Drawing.Image Data
            {
                get { return this.mData; }
                set { this.mData = value; }
            }
        }
    
        public partial class StaticDemo : System.Web.UI.Page
        {
            protected void Page_Load(object sender, EventArgs e)
            {
                TestObject obj = new TestObject();
                Response.Write("image width is " + obj.Data.Width + "px");
            }
        }
    }
    复制代码

     接下来看看使用单例模式同样1.5M的对象,100并发:

     内存开销是一条直线,没有峰谷,没有垃圾回收,并且几乎不受并发数量影响,100的并发与1000的并发在内存开销上完全相同。贴出代码变动部分:

    复制代码
    public partial class StaticDemo : System.Web.UI.Page
        {
            //单例模式
            private static readonly TestObject obj = new TestObject();
    
            protected void Page_Load(object sender, EventArgs e)
            {
                Response.Write("image width is " + obj.Data.Width + "px");
            }
        }
    复制代码

    示例很简单,其目的也只是为了引发大家的一些思考,你是否关心了可重用对象的内存开销?你是否成为了垃圾回收的俘虏?

    DEMO下载

    DEMO环境:Visual Studio 2012、.Net Framework 4.5

    点击下载DEMO

    注:本文中并发压力测试使用了JMeter,下载地址:http://jmeter.apache.org/ 

    .Net内存分析使用了CLR Profiler,下载地址:http://search.microsoft.com/en-us/DownloadResults.aspx?q=clr%20profiler

    参考文献:

    《JAVA与模式》

    《Head First设计模式》

    TOM大叔:别再让面试官问你单例

  • 相关阅读:
    《设备树 — 引入设备树,内核文件的位置变化(八)》
    《设备树 — 内核中设备树的操作函数(七)》
    《设备树 — 根文件系统中查看设备树(六)》
    《设备树 — platform_device和platform_driver如何让匹配(五)》
    《设备树 — device node到platform device的转换(四)》
    Ex 6_19 至多用k枚硬币兑换价格_第七次作业
    Ex 6_18 硬币有限的兑换问题_第七次作业
    Ex 6_17 数量无限的硬币兑换问题_第七次作业
    Ex 6_16 旧货销售问题_第七次作业
    Ex 6_21 最小点覆盖问题_第八次作业
  • 原文地址:https://www.cnblogs.com/lykbk/p/jgrtgtuig843857839.html
Copyright © 2020-2023  润新知