博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
设计模式:单例(Singleton)
阅读量:5821 次
发布时间:2019-06-18

本文共 6576 字,大约阅读时间需要 21 分钟。

转载必需注明出处:

 

前言

单例模式是最早了解并经常使用到的设计模式之一,很早就想将其整理成文,但因为对一些细节准备尚未充分,一再延误。本文通过实例与代码,分析了单例模式的需求、原理、实现,以及探讨在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
public sealed class GenericLazyMan { private static readonly Lazy
mInstance = new Lazy
(() => new GenericLazyMan()); private GenericLazyMan() { } public static GenericLazyMan Instance { get { return mInstance.Value; } } }

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

Lazy<T>参考资料:

 

单例模式与性能

 在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

注:本文中并发压力测试使用了JMeter,下载地址:

.Net内存分析使用了CLR Profiler,下载地址:

 

参考文献:

《JAVA与模式》

《Head First设计模式》

你可能感兴趣的文章
Report viewer 水平滚动条
查看>>
关于AbstractCollection
查看>>
Centos 6.5 mkisofs kickstart 制作自动安装iso镜像 光盘
查看>>
Nginx完整配置说明
查看>>
Java核心技术卷1: Java基础知识汇总
查看>>
web服务器的ddos***
查看>>
WebView使用中存在的问题
查看>>
Tomcat服务器SSL报错问题
查看>>
Python实例:毛玻璃效果
查看>>
Android开发中出现的问题及解决(一)
查看>>
6 个重构方法可帮你提升 80% 的代码质量
查看>>
陆上行舟,现在还是理想
查看>>
SaltStack安装与配置
查看>>
惰性载入函数
查看>>
一张图学会数据库迁云最佳路径
查看>>
阿里云MaxCompute被Forrester评为全球云端数据仓库领导者
查看>>
生产场景NFS共享存储优化及实战
查看>>
mysql定时备份脚本
查看>>
MYSQL之InnoDB Monitors
查看>>
oc NSDate、类的扩展 、代理(家庭-保姆)
查看>>