线程安全单例最佳实践,C#中的Lazy是如何保证线程安全的
在.NET 4.0之后,.NET Framework中提供了一种安全的延迟加载类型Lazy
Lazy能够在多线程环境下,保证GetValue函数只执行一次,从而实现单例模式
在过去,实现单例模式我们通常使用二次判断锁,或者利用类的静态初始化函数
利用Lazy类型,能够简化这一过程,并且性能上更好
Lazy创建的时候可以指定线程安装模式,目前有两种模式,PublicationOnly,ExcutionAndPublication
PublicationOnly模式
boxed = CreateValue(); //1 if (boxed == null || Interlocked.CompareExchange(ref m_boxed, boxed, null) != null) //2 { boxed = (Boxed)m_boxed; //3 } else { m_valueFactory = ALREADY_INVOKED_SENTINEL; //4 }
1.运行初始化函数,装箱到一个内部Box类型中,解决null值判断的问题,如果已经创建的情况,会返回null,该过程是线程不安全的
2.判断m_boxed是否为空,m_boxed是value保存的字段,如果等于空则设置为boxed,该方法能保证原子性,该过程是线程安全的
3.如果CreateValue返回空,表示其他线程已经创建有实例,则设置为已经创建好的实例
4.将初始化方法标记为已经初始化,一般发生在并发运行情况下,多次运行CreateValue
PublicationOnly模式下使用基于Interlocked.CompareExchange实现的乐观锁,该类包含了原子性方法 CAS(Compare and swap)
CAS是利用CPU提供的原子性指令来实现,不同运行时版本可能有不一样实现
Interlocked具体的实现在Native方法中,有兴趣的朋友可以通过coreclr/jvm代码查看具体实现
这种模式下,单例函数可能多次运行,但是最终能保证获取到的实例只有一个
ExcutionAndPublication模式下使用的是Volatile+Monitor,Monitor就是lock语句的实现,Monitor实现在Native代码中,是重量级的锁
Monitor支持队列和线程睡眠,能够保证一整个方法块处于单线程执行状态
object threadSafeObj = Volatile.Read(ref m_threadSafeObj); //强制从主内存空间同步变量到线程内存空间副本 bool lockTaken = false; try { if (threadSafeObj != (object)ALREADY_INVOKED_SENTINEL) //此时会有多个线程获取到正确值,抢夺开始 Monitor.Enter(threadSafeObj, ref lockTaken); //尝试等待锁,进入成功设置lockTaken为true else Contract.Assert(m_boxed != null); //单线程代码块 Start if (m_boxed == null) //没有设置值的情况 { boxed = CreateValue(); //获取值 m_boxed = boxed; //设置到字段中 Volatile.Write(ref m_threadSafeObj, ALREADY_INVOKED_SENTINEL); //强制将线程内存空间副本写入到主内存空间 } else // got the lock but the value is not null anymore, check if it is created by another thread or faulted and throw if so { boxed = m_boxed as Boxed; if (boxed == null) // it is not Boxed, so it is a LazyInternalExceptionHolder { LazyInternalExceptionHolder exHolder = m_boxed as LazyInternalExceptionHolder; Contract.Assert(exHolder != null); exHolder.m_edi.Throw(); } }
//单线程代码块End } finally { if (lockTaken) //进入成功需要释放,避免死锁 Monitor.Exit(threadSafeObj); }