一、单例模式
单例模式(Singleton Pattern)是最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:
- 1、单例类只能有一个实例。
- 2、单例类必须自己创建自己的唯一实例。
- 3、单例类必须给所有其他对象提供这一实例。
介绍
意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决:一个全局使用的类频繁地创建与销毁。
何时使用:当您想控制实例数目,节省系统资源的时候。
如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
关键代码:构造函数是私有的。
应用实例: 1、一个党只能有一个书记。 2、Windows 是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行。 3、一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。
优点: 1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。 2、避免对资源的多重占用(比如写文件操作)。
缺点:没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
1.1 单例模式——饿汉式
饿汉式,顾名思义,形容一个人饥饿到疯狂,迫不及待的要吃食物。其实饿汉式单例模式也是这样,在访问这个单例类的时候会直接创建一个单例保存在单例类中,不管你是否使用都会创建出来,这可能造成资源浪费。但是它是使用起来最简单的单例模式。
代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace 单例模式_饿汉式_静态内部类_
{
/// <summary>
/// 程序一但类被访问,单例就会被创建出来
/// </summary>
class HungrySingle
{
// 1. 构造器私有化
//防止外部通过new关键字创建对象
private HungrySingle() { }
// 2.创建一个成员变量用来保存这个单例
//readonly:不能修改,只能读取,和const关键字有区别
public static readonly HungrySingle hungrySingle = new HungrySingle();
// 3.给外部提供一个方法,用来获取单例
public static HungrySingle GetSingle()
{
return HungrySingle;
}
}
}
1.2 单例模式——懒汉式
上面提到,饿汉式单例模式不管你是否使用单例,只要你访问该类,这个类就会被加载,单例模式就会创建出来,造成了资源浪费。懒汉式就是为了解决资源浪费的问题产生的,它不会直接在变量中new 一个对象出来,而是在方法中创建对象,你只有通过方法获取单例才会给你创建一个单例。
代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace 单例模式
{
/// <summary>
/// 懒汉式
/// 当你需要对象的时候,才会创建一个对象,不会造成资源的浪费
/// </summary>
class LazyManSingle
{
//声明一个标识,用来记录构造函数是否被调用
private static bool isNew = false;
//1.构造器私有化
//防止外部通过new 关键字创建对象
private LazyManSingle(){}
//2.声明一个静态的私有变量,但不要创建对象
private static LazyManSingle _lazyManSingle = null;
//3.通过方法创建对象,并且返回
public static LazyManSingle GetSingle()
{
//当使用的时候,进行创建对象
if (_lazyManSingle == null)
{
_lazyManSingle = new LazyManSingle();
}
return _lazyManSingle;
}
}
}
好了,这样资源浪费的问题被解决了,那么单例模式是不是就完美了呢?哈哈,当然不是!你还记得多线程吗?在多线程中,可能会有多个线程去通过GetSingle()
来获取实例,方法执行的过程中会执行构造函数,那么你会产生疑问:我已经在GetSingle()
方法中判断了,如果单例不为空就不会再创建对象了,还有啥问题。
是的,这在单线程是完全可以的,但是在多线程中当有一个线程通过if
判断后,刚要创建对象时,线程执行权被其他线程抢过去了,导致这个线程“暂停执行”,这个时候单例仍然为null。此时,第二个线程也可以通过if判断,这导致了这两个线程接下来都会创建一个实例。那么单例模式就被破坏了。
那么怎么解决这个问题呢?我们知道,防止线程争夺资源要使用“锁(lock)”。那么就很简单了,给if的外面套上一层锁就好了。修改后代码如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace 单例模式
{
/// <summary>
/// 懒汉式
/// 当你需要对象的时候,才会创建一个对象,不会造成资源的浪费
/// </summary>
class LazyManSingle
{
//声明一个标识,用来记录构造函数是否被调用
private static bool isNew = false;
//1.构造器私有化
//防止外部通过new 关键字创建对象
private LazyManSingle(){}
//2.声明一个静态的私有变量,但不要创建对象
private static LazyManSingle _lazyManSingle = null;
//创建锁
private static Object o = new object();
//3.通过方法创建对象,并且返回
public static LazyManSingle GetSingle()
{
if (_lazyManSingle == null)
{
lock (o)
{
//当使用的时候,进行创建对象
if (_lazyManSingle == null)
{
_lazyManSingle = new LazyManSingle();
Console.WriteLine("我创建了一个实例");
}
}
}
return _lazyManSingle;
}
}
}
细心的小伙伴们发现了,我在锁的外面又加了一层判断,看起来好像很多余。是的,其实加不加影响不大,但是加这一层判断有一个好处:防止线程等待锁的释放造成资源的浪费。如果单例模式为空才会去创建单例,这个时候会等待这把锁的释放,这要消耗一定的资源。但是如果不为空,我们直接返回单例即可,没有必要等待锁的释放了。这种方法叫做“双层检查锁”
。
哇!头有点痒了,不会要长脑子了吧。别急,还有一个小细节,在通过new关键字创建对象的时候,会分三步走:首先会给要创建的类对象分配一块内存,然后执行构造函数初始化对象,最后将该内存的内存地址返回。这三步我们通常叫做三个指令,这三个指令的执行顺序一般来说都是不变的,但是凡事都有意外,在某些极端情况,指令的执行顺序会发生改变。如果指令按照132走,那么当指令执行到3的时候,单例就不是null了,而是一块没有意义的内存,此时其他线程会误以为单例被创建了,从而直接返回一个单例,得到了错误的结果,这就是指令重排
。为了避免这种情况,我们需要给单例前添加一个关键字“volatile"
。
//用volatile标记的成员,可以避免指令重排
private volatile static LazyManSingle _lazyManSingle = null;
注意:单线程中指令重排没有影响,但是多线程还是要注意一下,可能会发生错误!
那么直到现在,基本上单例模式就大功告成了!所谓道高一尺,魔高一丈,当你在凝视深渊的时候,深渊也在凝视你。有这么一个东西,它可以获取程序集所有类的信息,并且可以根据这些信息创建任何我想要的东西,它就是————反射。既然你的构造函数已经私有化,我无法通过常规手段进行创建对象,但是你做不到的东西反射可以做到,不管你私有不私有,反射都可以获取这个类中的构造函数并执行,达到创建对象的效果。
具体的实现如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Reflection;
namespace 单例模式
{
class Program
{
static void Main(string[] args)
{
//通过反射来破坏单例:通过反射来获取构造函数,进行创建对象
Type type = typeof(LazyManSingle);
Type[] parameterTypes = new Type[0];
//获取私有无参构造函数
ConstructorInfo construct = type.GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, parameterTypes, null);
//执行构造函数创建对象
LazyManSingle lazyInstance = construct.Invoke(null) as LazyManSingle;
LazyManSingle lazyInstance2 = construct.Invoke(null) as LazyManSingle;
Console.WriteLine(lazyInstance?.GetHashCode());
Console.WriteLine(lazyInstance2?.GetHashCode());
}
}
}
我们也有解决方案:既然反射通过构造函数去创建对象,那么不妨在构造函数中加入一些判断,如果是第一次执行构造函数,我们不用管;如果构造函数被执行过仍然试图调用,我们抛出异常警告。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace 单例模式
{
/// <summary>
/// 懒汉式
/// 当你需要对象的时候,才会创建一个对象,不会造成资源的浪费
/// </summary>
class LazyManSingle
{
//声明一个标识,用来记录构造函数是否被调用
private static bool isNew = false;
//1.构造器私有化
private LazyManSingle()
{
lock (o)
{
if (isNew == false)
{
isNew = true;
}
else
{
throw new Exception("不要试图使用反射破坏单例!");
}
}
}
}
//剩余代码省略了。。。。
}
当第一次执行构造函数的时候,isNew被赋值为true,之后如果继续调用构造函数,此时就会抛出异常。这种方法叫做标记位
,经常用来判断一个方法有没有执行过。
其实,即使是这样也仍然存在缺陷,既然构造函数都能拿到,那么私有变量也不在话下。我们拿到私有变量,在每次创建对象前将isNew
改为false
,就逃避了检查。
那么,就没有一种单例模式是完美无瑕的吗?答案是有的,这种单例模式是通过枚举实现的,比如java就可以通过枚举来实现绝对完美的单例模式,不过C#语言中的枚举和java中的枚举是非常不同的,java中的枚举是可以当作类来用的,它可以有变量和方法等,但是C#的枚举中只能有枚举值。所以C#是无法实现终极单例模式的,尽管java和C#有95%的相似,可是有时往往有那么一些偏差。
1.3 单例模式——饿汉式加强版(静态内部类)
这里我们先回顾一下前面的两种单例模式:
饿汉式:
- 优点:使用方便,代码简单,没有线程安全问题
- 缺点:资源浪费问题,不能实现懒加载,也不能传递参数
懒汉式:
- 优点:能够实现懒加载,避免了资源浪费,使用灵活
- 缺点:有线程安全问题
可见,饿汉式的主要问题就是无法懒加载,那么我们可以通过一种方法来解决这个问题,就是静态内部类:即一个类中存在的静态类。静态内部类的特点就是不会随着外部类的加载而加载,我们把这个单例放到静态内部类中即可。
代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace 单例模式_饿汉式_静态内部类_
{
/// <summary>
/// 通过静态内部类实现懒加载
/// 静态内部类不会因为它外面的类加载而加载
/// </summary>
class HungrySingle
{
// 1. 构造器私有化
private HungrySingle() { }
// 2.静态内部类声明一个实例
private static class InnerClass
{
public static readonly HungrySingle hungrySingle = new HungrySingle();
}
// 3.给外部提供一个方法,用来获取单例
public static HungrySingle GetSingle()
{
return InnerClass.hungrySingle;
}
}
}
至此单例模式完美结束,具体使用还需要结合场景哦。