从单例模式看C#的volatile关键字
目录
-
Singleton示例
-
volatile解决问题1:CPU缓存
-
volatile解决问题2:编译器优化(指令乱序)
一. 标准的单例模式示例
public sealed class Singleton
{
// 静态实例
private static volatile Singleton instance = null;
// Lock对象,线程安全所用
private static object syncRoot = new Object();
private Singleton() { }
public static Singleton Instance
{
get
{
if (instance == null) //一次比较
{
lock (syncRoot)
{
if (instance == null) //二次比较
instance = new Singleton();
}
}
return instance;
}
}
}
注意静态示例自动前的修饰符: volatile,为什么必须指定volatile, 如果不使用该关键字,会有什么后果呢。
volatile【易变的】,查msdn:
volatile 关键字指示一个字段可以由多同时执行的线程修改。 声明为 volatile 的字段不受编译器优化(假定由单个线程访问)的限制。 这样可以确保该字段在任何时间呈现的都是最新的值。
那在底层到底发生了什么,难道不使用volatile,指令执行时,字段的值还不是新的吗,这得从计算机架构及编译器优化两个方面说起。
二. CPU缓存问题
采用volatile关键字,每次读字段值时,都必须从内存中获取,也就是说,对该字段禁用CPU缓存。
由于现代的CPU都存在多个核心,每个核心有独立的内部缓存,而对象字段最初是保存在内存中的,执行指令前,会首先检查缓存中是否存在该字段,如果没有,从内存中读取数据到CPU缓存,进行运算, 如果缓存存在,则无需访问内存,直接使用缓存的数据。
针对单例模式示例,两个线程分别在两个CPU核心运行,两个核心同时运行到上面语句“第一次比较”,instance字段都保存在各个CPU的内部缓存中,通过lock关键字,两个线程会串行执行lock语句块。
比如核心A已经运行完成lock语句块,内存中的instance已经更新,此时核心B继续运行,由于核心B已经缓存了instance示例,在二次比较时,还是认为instance字段为空,这样就导致 new Singleton()执行了两次。
三. 编译器优化问题
采用volatile关键字, 可以避免指令重新排序(instruction reordering), 例如,考虑如下一个循环:
while(true)
{
if(myField)
{
//do something
}
}
如果myField字段没有指定volatile, 在JIT编译时,出于性能优化考虑,编译器会以如下方式重新排序指令:
if(myField)
{
while(true)
{
//do something
}
}
这种情况下,如果你在另一个线程中修改字段myFiled, 运行结果将完成不同。
通常情况下,推荐使用lock语句(Monitor.Enter /Monitor.Exit),但如果你在这个语句块内仅仅修改了一个字段,volatile将有更好的性能表现。