首页 > 代码库 > 从单例模式看C#的volatile关键字

从单例模式看C#的volatile关键字

目录

  1. Singleton示例

  2. volatile解决问题1:CPU缓存

  3. volatile解决问题2:编译器优化(指令乱序)

一. 标准的单例模式示例 ```csharp 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), 例如,考虑如下一个循环: ```csharp while(true) { if(myField) { //do something } }

```

如果myField字段没有指定volatile, 在JIT编译时,出于性能优化考虑,编译器会以如下方式重新排序指令: ```csharp

if(myField) { while(true) { //do something } }

```

这种情况下,如果你在另一个线程中修改字段myFiled, 运行结果将完成不同。 通常情况下,推荐使用lock语句(Monitor.Enter /Monitor.Exit),但如果你在这个语句块内仅仅修改了一个字段,volatile将有更好的性能表现。