.NET 内存模型,以及与 Java 内存模型的对比

并发编程的本质问题是:

  • 在多线程环境下,一个线程对共享变量的修改,什么时候能够被其他线程看到?
  • 编译器和 CPU 是否允许对指令进行重排序?
  • 哪些操作具备原子性保证?

这些问题由内存模型(Memory Model)来定义。

一、什么是内存模型?

内存模型是一套语言级规范,用于定义在多线程环境下:

  • 线程之间如何通过内存进行交互
  • 变量读写在不同线程之间何时可见
  • 编译器和 CPU 在什么范围内可以进行重排序
  • 哪些同步操作能够建立 happens-before 关系

需要注意的是,内存模型并不是物理内存结构,而是抽象层面的规则系统。

在现代计算机体系结构中:

  • CPU 存在多级缓存
  • 指令允许乱序执行
  • 写操作可能被延迟刷新

如果语言层面不定义清晰规则,多线程程序将无法推理其行为。

内存模型通常围绕三个核心性质展开:

  • 可见性(Visibility)—— 一个线程的修改何时对其他线程可见
  • 有序性(Ordering)—— 指令是否可能被重排序
  • 原子性(Atomicity)—— 操作是否不可分割

理解内存模型,本质是在理解并发程序的行为边界。

二、什么是 happens-before?

happens-before 是内存模型中的一个偏序关系(partial order),用于定义:

  • 如果操作 A happens-before 操作 B,那么 A 的结果对 B 可见,并且 A 在逻辑上先于 B 执行。

它并不表示“时间上的绝对先后”,而是表示一种可见性与顺序保证。

可以理解为:

  • 如果没有 happens-before 关系 → 结果不可预测
  • 如果存在 happens-before 关系 → 可见性得到保证

一个简单示例

1
2
3
4
5
6
7
int x = 0;

// Thread 1
x = 42;

// Thread 2
Console.WriteLine(x);

在没有同步的情况下:

  • Thread 2 可能打印 0
  • 也可能打印 42

因为两个线程之间没有 happens-before 关系。

加上锁之后

1
2
3
4
5
6
7
8
9
10
11
12
13
int x = 0;
object obj = new object();

// Thread1
lock(obj)
{
x = 42;
}
// Thread2
lock(obj)
{
Console.WriteLine(x);
}

这里存在:

  • Thread1 的 unlock happens-before Thread2 的 lock

因此:

  • 写入 x = 42 对 Thread2 可见
  • 不会打印 0

happens-before 的本质

它本质上定义了:

  • 哪些操作会形成内存屏障
  • 哪些写入必须对后续读取可见
  • 哪些重排序是被禁止的

常见建立 happens-before 的方式包括:

  • 释放锁 → 获取同一把锁
  • volatile 写 → volatile 读
  • 线程启动
  • 线程 join
  • 原子操作

无论是 .NET 还是 Java,内存模型的核心都是围绕 happens-before 构建。

三、.NET 是否有内存模型?

有。.NET 的内存模型定义在 CLI(Common Language Infrastructure)规范中(ECMA 规范的一部分)。C# 作为运行在 CLI 之上的语言,遵循这一内存模型。

.NET 内存模型定义了:

  • 在没有同步的情况下,允许重排序
  • 同步原语如何建立 happens-before 关系
  • lock、volatile、Interlocked 等具备的内存屏障语义

四、.NET 内存模型核心机制

1. 重排序

.NET 允许:

  • 编译器重排序(JIT)
  • CPU 重排序

只要不改变单线程语义,重排序是合法的。

问题在于:多线程下如果没有同步,行为是不确定的。

2. 可见性问题示例

1
2
3
4
5
6
7
8
9
10
11
12
bool ready = false;
int number = 0;

// Thread 1
number = 42;
ready = true;

// Thread 2
if (ready)
{
Console.WriteLine(number);
}

理论上可能输出:

1
0

原因可能是:

  • 写入 number 与 ready 被重排序
  • ready 被先刷新到缓存
  • number 仍然停留在线程缓存中

3. volatile

1
volatile bool ready;

作用:

  • 禁止某些重排序
  • 保证对该变量的读写直接与主内存交互

特点:

  • 保证可见性
  • 不保证复合操作的原子性
  • 不能替代锁

.NET 中的 volatile 语义相对克制,更多用于简单状态标记。

4. lock(Monitor)

1
2
3
4
lock (obj)
{
// critical section
}

本质是:

1
2
3
Monitor.Enter(obj);
...
Monitor.Exit(obj);

语义包括:

  • 互斥
  • 完整内存屏障
  • 进入临界区时读取最新值
  • 退出临界区时刷新写入

lock 不仅解决可见性问题,还解决竞态条件。

5. Interlocked

1
Interlocked.Increment(ref count);

特点:

  • 原子操作
  • 无锁实现
  • 自带内存屏障语义

常用于高性能并发场景,类似于 Java 中的原子类。

6. MemoryBarrier

1
Thread.MemoryBarrier();

强制插入内存屏障:

  • 禁止前后指令重排序
  • 强制刷新缓存

实际开发中很少直接使用,通常由 lock 或 Interlocked 隐式提供。

五、Java 内存模型(JMM)

Java 内存模型定义在 Java Language Specification 中。

它采用一个更抽象的结构:

  • 主内存(Main Memory)
  • 每个线程的工作内存(Working Memory)

线程对变量的读写必须:

  • 从主内存加载到工作内存
  • 修改后再写回主内存

Java 明确定义:

  • happens-before 规则
  • volatile 语义
  • synchronized 语义
  • final 字段的安全发布语义

六、.NET 与 Java 内存模型对比

维度 .NET Java
是否有正式规范 CLI 规范 JLS 规范
是否定义 happens-before
内存结构抽象 不强调主/工作内存 明确主内存 + 工作内存
volatile 语义 保证可见性,限制重排序 语义更强,明确屏障类型
锁机制 lock / Monitor synchronized
原子操作 Interlocked AtomicXXX
final 语义 无直接等价机制 final 保证安全发布

七、volatile 对比

Java volatile:

  • 保证可见性
  • 禁止重排序
  • 明确包含 StoreLoad 屏障
  • 经常用于双重检查锁

.NET volatile:

  • 保证读取最新值
  • 限制重排序
  • 语义略弱于 Java
  • 更多建议使用 lock 或 Interlocked

八、双重检查锁(DCL)差异

Java 中:

1
private volatile static Singleton instance;

必须 volatile,否则可能读取到半初始化对象。

.NET 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private static Singleton instance;
private static readonly object obj = new object();

public static Singleton Instance
{
get
{
if (instance == null)
{
lock (obj)
{
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}

.NET 2.0 之后的内存模型已经保证 DCL 安全。

九、两者设计哲学差异

Java:

  • 模型高度形式化
  • 明确抽象主内存与工作内存
  • happens-before 规则非常完整

.NET:

  • 更贴近 CPU 层面
  • 更依赖内存屏障语义
  • 抽象层级更低

本质目标一致,但抽象方式不同。

十、与 GC 的关系

GC 负责:

  • 对象生命周期
  • 内存回收

内存模型负责:

  • 并发可见性
  • 指令重排序
  • happens-before

二者处于完全不同层面。

十一、总结

  • .NET 存在正式内存模型,定义在 CLI 规范中
  • 支持 happens-before 语义
  • lock、Interlocked、volatile 都具有内存屏障语义
  • Java 内存模型更加形式化,抽象更清晰
  • 两者核心目标一致,但实现哲学不同

理解内存模型,本质是在理解并发程序的行为边界,以及同步机制在底层真正做了什么。

Powered by Hexo and Hexo-theme-hiker

Copyright © 2018 - 2026 TEN-Z'S BLOG All Rights Reserved.

访客数 : | 访问量 :