单例模式 Singleton
- 保证一个类只有一个实例的实现方法
- 给其他类提供一个全局的访问点。
- 由自己创建自己的唯一实例
实现
- 实现方法分为饿汉式(线程安全)、懒汉式(线程不安全)、懒汉式(lock+双重验证、线程安全)、延迟加载(Lazy、线程安全)
1.饿汉式
这种方式比较常用,但容易产生垃圾对象.这时候初始化 instance 显然没有达到 lazy loading 的效果。
优点:没有加锁,执行效率会提高。
缺点:类加载时就初始化,浪费内存。
public class EagerSingleton
{
private EagerSingleton() { }
private static readonly EagerSingleton Instance = new EagerSingleton();
public static EagerSingleton GetInstance()
{
return Instance;
}
}
2.最简单的实现:懒汉式(线程不安全)
namespace Singleton
{
public class Singleton
{
private static Singleton _uniqueInstance;
private Singleton()
{
}
public static Singleton GetInstance()
{
if (_uniqueInstance is null)
{
_uniqueInstance = new Singleton();
}
return _uniqueInstance;
}
}
}
定义了一个静态方法,作为全局访问点,在单线程下是正常的,在多线程同时运行GetInstance,得到的_uniqueInstance都是null,此时就会创建多个 定义了一个静态方法,作为全局访问点,在单线程下是正常的,在多线程同时运行GetInstance,得到的_uniqueInstance都是null,此时就会创建多个的实例。
多线程访问得到hash code是不一样的。
static void Main(string[] args)
{
Task.Run(() =>
{
Singleton singleton = Singleton.GetInstance();
Console.WriteLine(singleton.GetHashCode());
});
Task.Run(() =>
{
Singleton singleton = Singleton.GetInstance();
Console.WriteLine(singleton.GetHashCode());
});
Console.WriteLine("over!");
}
over还提前输出。
over!
4032828
6044116
static void Main(string[] args)
{
Singleton singleton1 = Singleton.GetInstance();
Console.WriteLine(singleton1.GetHashCode());
Singleton singleton2 = Singleton.GetInstance();
Console.WriteLine(singleton2.GetHashCode());
Console.WriteLine("over!");
}
输出
58225482
58225482
over!
3.懒汉式(lock+双重验证、线程安全)
lock关键字
MSDN介绍
lock 关键字可确保当一个线程位于代码的临界区时,另一个线程不会进入该临界区。 如果其他线程尝试进入锁定的代码,则它将一直等待(即被阻止),直到该对象被释放。
lock 关键字在块的开始处调用 Enter,而在块的结尾处调用 Exit。 ThreadInterruptedException 引发,如果 Interrupt 中断等待输入 lock 语句的线程。
通常,应避免锁定 public 类型,否则实例将超出代码的控制范围。
常见的结构 lock (this)、lock (typeof (MyType)) 和 lock ("myLock") 违反此准则:
如果实例可以被公共访问,将出现 lock (this) 问题。
如果 MyType 可以被公共访问,将出现 lock (typeof (MyType)) 问题。
由于进程中使用同一字符串的任何其他代码都将共享同一个锁,所以出现 lock("myLock") 问题。
最佳做法是定义 private 对象来锁定, 或 private static 对象变量来保护所有实例所共有的数据。
在 lock 语句的正文不能使用 等待 关键字。
最常使用的锁是如下格式的代码段:
private static object objlock = new object();
lock (objlock )
{
//要执行的代码逻辑
}
使用lock关键字解决多线程问题
public static LockSingleton GetInstance()
{
// 当第一个线程运行到这里时,此时会对locker对象 "加锁",
// 当第二个线程运行该方法时,首先检测到locker对象为"加锁"状态,该线程就会挂起等待第一个线程解锁
// lock语句运行完之后(即线程运行完之后)会对该对象"解锁"
lock (Locker)
{
// 如果类的实例不存在则创建,否则直接返回
if (_uniqueInstance == null)
{
_uniqueInstance = new LockSingleton();
}
}
return _uniqueInstance;
}
在lock之前判断是否实例
上面的代码还可以优化,通过判断对象是否为null,如果不是null,则直接返回,否则先锁,然后再生成实例,保证不同线程访问得到的是一个实例
public static LockSingleton GetInstance()
{
// 双重锁定只需要一句判断就可以了
if (_uniqueInstance == null)
{
lock (Locker)
{
if (_uniqueInstance == null)
{
_uniqueInstance = new LockSingleton();
}
}
}
return _uniqueInstance;
}
3.使用lock
Task.Run(() =>
{
LockSingleton lockSingleton = LockSingleton.GetInstance();
Console.WriteLine(lockSingleton.GetHashCode());
});
Task.Run(() =>
{
LockSingleton lockSingleton = LockSingleton.GetInstance();
Console.WriteLine(lockSingleton.GetHashCode());
});
输出结果
over!
6044116
6044116
延迟加载(Lazy)
public class LazySingleton
{
private static readonly Lazy<LazySingleton> SingletonLazy = new Lazy<LazySingleton>(() => new LazySingleton());
/// <summary>
/// 私有构造函数
/// </summary>
private LazySingleton()
{
Console.WriteLine("我被创建了.Lazy");
}
/// <summary>
/// 获取实例
/// </summary>
/// <returns></returns>
public static LazySingleton GetInstance()
{
return SingletonLazy.Value;
}
}
总结
单例主要分为如下几种方式,在实际使用过程中:建议采用延迟加载(Lazy)
饿汉式 | 懒汉式 | 懒汉式+lock锁+双重判断 | 延迟加载(Lazy) |
---|---|---|---|
线程安全 | 线程不安全 | 线程安全 | 线程安全 |
不是延迟加载(会浪费内存) | 会延迟加载 | 会延迟加载 | 会延迟加载 |
没有加锁 | 没有加锁 | 加锁 | 加锁 |