什么是“易失性”关键字使用?

我读了一些关于 volatile关键字的文章,但是我不知道它的正确用法。你能告诉我它在 C # 和 Java 中应该用来做什么吗?

59420 次浏览

考虑一下这个例子:

int i = 5;
System.out.println(i);

编译器可能会将其优化为只打印5,如下所示:

System.out.println(5);

但是,如果有另一个线程可以改变 i,这是错误的行为。如果另一个线程将 i更改为6,优化后的版本仍将打印5。

volatile关键字可以防止这种优化和缓存,因此在另一个线程可以更改变量时非常有用。

对于 C # 和 Java 来说,“ volatile”告诉编译器,变量的值永远不能被缓存,因为它的值可能在程序本身的范围之外发生变化。然后,编译器将避免任何可能导致问题的优化,如果变量变化“超出其控制范围”。

易失性关键字在 Java 和 C # 中有不同的含义。

爪哇咖啡

来自 Java 语言规范:

一个字段可以声明为可变的,在这种情况下,Java 内存模型确保所有线程都能看到变量的一致值。

C #

来自 C # 参考文献(检索日期: 2021-03-31) :

易失性关键字表示一个字段可能被同时执行的多个线程修改。由于性能原因,编译器、运行时系统甚至硬件都可能重新安排对内存位置的读写。声明为易失性的字段不受这些优化的影响。(...)

在 Java 中,“ volatile”用于告诉 JVM 变量可以被多个线程同时使用,因此不能应用某些常见的优化。

值得注意的是,访问同一个变量的两个线程在同一台机器的不同 CPU 上运行。由于内存访问比缓存访问慢得多,CPU 积极地缓存它所持有的数据是非常普遍的。这意味着,如果数据是 CPU1中的 更新,它必须立即通过所有缓存和主内存,而不是当缓存决定清除自己,以便 CPU2可以看到更新的值(再次忽略所有缓存在途中)。

为了理解易失性对变量的影响,理解当变量不是易失性时会发生什么是很重要的。

  • 变量是非易失性的

当两个线程 A & B 访问一个非易失性变量时,每个线程将在其本地缓存中维护该变量的一个本地副本。线程 A 在其本地缓存中所做的任何更改对于线程 B 都是不可见的。

  • 变量是不稳定的

当变量被声明为可变时,这实际上意味着线程不应该缓存这样的变量,或者换句话说,线程不应该信任这些变量的值,除非它们是直接从主内存读取的。

那么,什么时候使变量易变呢

当你有一个可以被多个线程访问的变量时,你希望每个线程都能得到这个变量的最新更新值,即使这个值是由程序外的任何其他线程/进程/更新的。

易失性字段的读取具有 学习语义学。这意味着可以保证从 Volatile变量读取的内存会在以后的内存读取之前发生。它阻止编译器进行重新排序,如果硬件需要它(弱顺序 CPU) ,它会使用一个特殊的指令,使硬件刷新任何在易失性读取之后发生的读操作,但是这些读操作被推测性地提前启动,或者 CPU 可以通过防止在负载获取和退役之间发生任何推测性负载,从而阻止这些读操作提前发出。

易失性字段的写有 释放语义。这意味着保证任何写入 Volatile变量的内存都会被延迟,直到所有以前的内存写入对其他处理器可见为止。

考虑下面的例子:

something.foo = new Thing();

如果 foo是类中的一个成员变量,并且其他 CPU 可以访问 something引用的对象实例,那么它们可能会看到值 foo改变 something0,Thing构造函数中写入的内存是全局可见的!这就是“弱序记忆”的意思。即使编译器在存储到 foo之前在构造函数中具有所有存储,也可能发生这种情况。如果 foovolatile,那么对 foo的存储将具有发布语义,并且硬件保证在对 foo进行写操作之前所有的写操作对其他处理器都是可见的,然后才允许对 foo进行写操作。

foo的写操作怎么可能重新排序得这么糟糕?如果保存 foo的缓存线路在缓存中,而构造函数中的存储丢失了缓存,那么存储完成的时间可能比对缓存的写入丢失的时间快得多。

来自英特尔的(糟糕的) Itanium 架构的内存有序性很差。原始 XBox360中使用的处理器的内存顺序很弱。许多 ARM 处理器,包括非常流行的 ARMv7-A 都有弱有序的内存。

开发人员通常看不到这些数据竞赛,因为像锁这样的东西会造成完整的内存屏障,本质上与同时获取和释放语义是一样的。在获取锁之前,不能以推测的方式执行锁中的任何负载,这些负载会被延迟,直到获取锁为止。锁释放期间不能延迟任何存储,释放锁的指令将被延迟,直到锁内完成的所有写操作都全局可见。

一个更完整的例子是“双重检查锁定模式”模式。此模式的目的是避免为了延迟初始化对象而必须总是获取锁。

摘自维基百科:

public class MySingleton {
private static object myLock = new object();
private static volatile MySingleton mySingleton = null;


private MySingleton() {
}


public static MySingleton GetInstance() {
if (mySingleton == null) { // 1st check
lock (myLock) {
if (mySingleton == null) { // 2nd (double) check
mySingleton = new MySingleton();
// Write-release semantics are implicitly handled by marking
// mySingleton with 'volatile', which inserts the necessary memory
// barriers between the constructor call and the write to mySingleton.
// The barriers created by the lock are not sufficient because
// the object is made visible before the lock is released.
}
}
}
// The barriers created by the lock are not sufficient because not all threads
// will acquire the lock. A fence for read-acquire semantics is needed between
// the test of mySingleton (above) and the use of its contents. This fence
// is automatically inserted because mySingleton is marked as 'volatile'.
return mySingleton;
}
}

在本例中,在存储到 mySingleton之前,MySingleton构造函数中的存储对其他处理器可能不可见。如果发生这种情况,那么窥视 mySingleton 的其他线程将不会获得锁,它们也不一定会拾取对构造函数的写操作。

volatile从不阻止缓存。它所做的是保证其他处理器“查看”写入的顺序。存储释放将延迟存储,直到所有挂起的写操作完成,并且已经发出总线周期,告诉其他处理器如果恰好有相关的缓存行,则丢弃/写回它们的缓存行。加载获取将刷新所有推测的读取,确保它们不会是过去的过时值。

当读取非易失性数据时,执行线程可能会或可能不会总是获取更新的值。 但是如果对象是可变的,那么线程总是获得最新的值。

易变性是解决并发问题。使该值同步。这个关键字主要用在线程中。当多个线程更新同一个变量时。