题记 关于内存模型, 这实在是个被说烂了的话题. 五六年前刚刚接触到.NET的时候, 各路大牛就开始讨论了. 还记得那时候每每带着无比崇敬的心去阅读那些文字和思想. 之后每每回头去重读那些文字,更感觉收获颇多. 可是大牛们往往言语颇为简概, 所以尽管读的次数多, 但是多数成为时间和差记忆力的受害者屡屡忘记. 所以最近下定决心, 写这样一篇博文, 汇总各路豪杰之思想, 聚集近几年之结论, 加上笔者一点微不足道的收获, 务求准确翔实, 希望图文并茂. 程序员帮程序员, 大家互助.是为题记.
-Jeffrey Sun
引出 – Singleton & Volatile 不知道作为程序员的您想过没有, 如果CPU不是按照您的程序写那样的顺序(Programming Order)执行, 结果会是怎样的? 如果您以前没有意识到这一点, 您可能会比较震惊. 但在进入多核心多处理器时代之后,事实上就是这样的, CPU会调整指令执行的顺序, 并以调整后的顺序(Processer Order)来执行指令. 这是提高CPU执行效率的重要措施.
除了极少数人比如.NET框架的设计者们在很早的时间就遇到了这个问题, 撇开C++/JAVA程序员们不谈, 我猜测很多.NET程序员首次意识到这个问题, 应该是当Double-Check遇到了单例模型的实现:
Imperfect Singleton Implement
- 1. public class Singleton
- 2. {
- 3. private static object syncRoot = new object();
- 4. private Singleton instance;
- 5.
- 6. private Singleton() { }
- 7.
- 8. public Singleton Instance
- 9. {
- 10. get
- 11. {
- 12. if (instance == null)
- 13. {
- 14. lock (syncRoot)
- 15. {
- 16. if (instance == null)
- 17. {
- 18. instance = new Singleton();
- 19. }
- 20. }
- 21. }
- 22.
- 23. return instance;
- 24. }
- 25. }
- 26. }
复制代码这个实现有一点点瑕疵. 在多核心CPU上, 存在这样一种可能. 两个CPU核心同时执行Instance这段代码. 当Processer0执行到释放锁的时候, 由于CPU高速缓存(Cache)和写缓存(Store Buffer)的存在, 主内存中可能Singleton对象还没有被创建. 当Processer1执行到instance==null的判断时依然为真, 然后另外一个Singleton对象也被表明需要创建. 这样当所有写操作完成后, 这个单例模型实际上给出了两个完全不相干的Singleton对象!
A Better Singleton
- 1. public class Singleton
- 2. {
- 3. private static object syncRoot = new object();
- 4. private volatile Singleton instance;
- 5.
- 6. private Singleton() { }
- 7.
- 8. public Singleton Instance
- 9. {
- 10. get
- 11. {
- 12. if (instance == null)
- 13. {
- 14. lock (syncRoot)
- 15. {
- 16. if (instance == null)
- 17. {
- 18. instance = new Singleton();
- 19. }
- 20. }
- 21. }
- 22.
- 23. return instance;
- 24. }
- 25. }
- 26. }
复制代码一个改进后的实现, 只是在单例内部维护的Singleton私有实例前面加了"volatile"关键字. Volatile关键字有什么奇妙的作用呢? MSDN给出这样的解释:
"The volatile keyword indicates that a field might be modified by multiple threads that are executing at the same time. Fields that are declared volatile are not subject to compiler optimizations that assume access by a single thread. This ensures that the most up-to-date value is present in the field at all times." Volatile的这段解释最主要的意思是: 标有Volatile的字段将编译器认为是多线程代码, 从而不会执行优化; 这保证内存中字段的值总是最新的.
那么, 真的是这样么? 是不是使用了Volatile关键字之后, 单例模型就完全没有问题了? 有比Volatile更好的实现么? 探究问题的根本, 要从现代CPU的结构上谈起.
名词解释 旧/前 - 对编程顺序而言, 较前的语句/操作
新/后 - 对编程顺序而言, 较后的语句/操作
Load/Read - CPU读操作, 是指将内存数据加载到寄存器的操作过程.
Store/Write - CPU写操作, 是指根据CPU指令, 将修改过的数据回写主存储器的操作过程.
Load.Acquire - 含有Acquire语义的读操作. 相当于一个单向向后的栅障. 普通的读和写操作可以向后越过该读操作, 但是之后的读和写操作不能向前越过该读操作.
Store.Release - 含有Release语义的写操作. 相当于一个单向向前的栅障. 普通的读和写可以向前越过该写操作, 但是之前的读和写操作不能向后越过该写操作.
Full Fence - 全向栅障. 任何读写操作都不能跨越该栅障.
Cache - CPU封装的高速缓存. 在现代CPU当中, 一般会设置多级缓存(比如从L1到L3等), 多级缓存有不同的访问速度. Cache按照封装分有两种, 一种是与每个CPU核心封装在一起并被其单独占有的, 另一种是几个CPU核心共享的. 在不影响本文分析的基础上, 笔者使用Cache一词统称CPU当中每个逻辑CPU核心独享的缓存. 另因Store Combine Buffer亦不影响本文分析, 保留了Store Buffer之后, 同样省去Store Combine Buffer.
Cache Line - 对应于内存中不同的数据边界大小,Cache根据不同的固定尺寸分成一些大小(Boundary)不等的存储空间, 这些存储空间叫做Cache Line.