前言
单例模式有二种,饿汉模式和懒汉模式。这里将去实现单例模式的这些例子,然后分析它们的优缺点和使用场景。
1.懒汉模式(单线程)
public class SingleThread_Singleton
{
private static SingleThread_Singleton _instance = null;
private SingleThread_Singleton() { }
public static SingleThread_Singleton instance {
get {
if (_instance == null)
_instance = new SingleThread_Singleton();
return _instance;
}
}
public void Show()
{
Console.WriteLine("懒汉单线程的单例");
}
}
使用到类实例时才会去及时创建,如果代码没有使用多线程去访问类实例,以上写法就可以了,不然的话就需要加线程锁,当多线程访问此对象时,其他线程就需要等待挂起,下面就是多线程的懒汉模式。
2.懒汉模式(多线程)
public class MultiThread_Singleton
{
//变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了
private static volatile MultiThread_Singleton _instance = null;
private static readonly object lockHelper = new object();
private MultiThread_Singleton() { }
public static MultiThread_Singleton instance {
get {
if (_instance == null)
{
lock (lockHelper)
{
if (_instance == null)
_instance = new MultiThread_Singleton();
}
}
return _instance;
}
}
public void Show()
{
Console.WriteLine("懒汉多线程的单例");
}
}
大家可能看到这里有几个细节问题。1.为什么需要判断2次实例等于null?2.volatile关键字的作用?3.lock的一些注意事项?
1.这其实叫做双重锁定。外面的_instance == null判断如果去掉,会发生什么事情呢?这个就是所谓的不讲道理,不管什么情况先让多线程进行同步,性能消耗比较大,所以外部的判断是必要的。里面的_instance == null判断如果去掉,会发生什么事情呢?如果多线程都通过了外层的判断进行排队,没有内部的判断,那将会实例化多个对象出来,所以这里还需要进行一次判断,保证线程的安全。
2.volatile 关键字指示一个字段可以由多同时执行的线程修改。 声明为 volatile 的字段不受编译器优化(假定由单个线程访问)的限制。 这样可以确保该字段在任何时间呈现的都是最新的值。也就是说cpu不能进行缓存,每次访问时都要去内存里获取最新的值。
3.lock锁引用类型的对象,string类型除外。lock推荐的做法是使用静态的、只读的、私有的对象。保证lock的对象在外部无法修改才有意义,如果lock的对象在外部改变了,对其他线程就会畅通无阻,失去了lock的意义。lock(this)可能会造成死锁,也就是无效的线程锁,因为不同的类对象会导致this不同。所以最好自己定义一个静态的、只读的、私有的对象去锁定。
3.饿汉模式
public class Static_Singleton
{
public static readonly Static_Singleton instance = new Static_Singleton();
private Static_Singleton() { }
public void Show()
{
Console.WriteLine("饿汉模式的单例");
}
}
这个类的实例启动的时候就进行初始化了,专治各种花里胡哨。我们可以在public static readonly Static_Singleton instance = new Static_Singleton();打个断点观察一下,发现还没有执行Main函数的时候,就先执行赋值了。所以我们得出在类加载时就完成了初始化,所以程序启动比较慢,但获取对象的速度快,线程安全。
总结
饿汉和懒汉的同异:
同:任何时刻只有一个类实例。
异:饿汉程序启动比较慢,但获取对象的速度快,线程安全。饿汉程序启动比较快,要它的时候,才会进行初始化,所以需要注意多线程死锁的问题。
单例模式应用的场景:
(1)资源共享的情况下,避免由于资源操作时导致的性能或损耗等。如上述中的日志文件,应用配置。
(2)控制资源的情况下,方便资源之间的互相通信。如线程池等。