什么是“挥发性”的关键字使用?

我读了一些关于volatile关键字的文章,但我无法弄清楚它的正确用法。 你能告诉我应该在C#和Java中使用什么?

对于C#和Java,“volatile”都告诉编译器,variables的值不能被caching,因为它的值可能会超出程序本身的范围。 然后,编译器将避免任何可能导致问题的优化,如果variables“超出其控制”。

考虑这个例子:

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

编译器可以优化这个只打印5,就像这样:

 System.out.println(5); 

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

volatile关键字阻止了这样的优化和caching,因此当一个variables可以被另一个线程改变时很有用。

在Java和C#中, volatile关键字具有不同的含义。

Java的

从Java语言规范 :

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

C#

从volatile关键字的C#参考:

volatile关键字表示可以通过诸如操作系统,硬件或并行执行的线程等方式在程序中修改字段。

为了理解volatile对variables的作用,了解variables不易变时会发生什么很重要。

  • variables是非易失性的

当两个线程A和B正在访问非易失性variables时,每个线程将在其本地caching中保留一个本地副本。 线程A在本地caching中所做的任何更改都不会被线程B看到

  • variables是易变的

当variables被声明为volatile时,它基本上意味着线程不应该caching这样的variables,换句话说,线程不应该信任这些variables的值,除非它们是直接从主内存中读取的。

那么,什么时候做一个variablesvolatile呢?

当你有一个可以被许multithreading访问的variables,并且你希望每个线程都能获得该variables的最新更新值,即使这个值被程序之外的任何其他线程/进程所更新。

易失性字段的读取具有语义 。 这意味着保证从volatilevariables中读取的内存将在下一次内存读取之前发生。 它会阻止编译器执行重新sorting,如果硬件要求(弱sorting的CPU),它将使用一个特殊的指令使硬件清除在易失性读取之后发生的任何读取,但是推测性地早启动,或者CPU可以防止他们在第一时间提前发放,防止在收取负担和退休之间发生任何投机负担。

volatile字段的写有释放语义 。 这意味着可以保证任何写入volatilevariables的内存都被保证延迟,直到所有以前的内存写入对其他处理器都可见。

考虑下面的例子:

 something.foo = new Thing(); 

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

怎么可能写入foo被重新sorting如此糟糕? 如果持有foo的高速caching行位于高速caching中,并且构造函数中的存储器未能访问高速caching,则存储可能比写入高速caching未命中更快地完成。

英特尔(糟糕的)Itanium体系结构内存有限。 在原来的XBox 360中使用的处理器有微弱的有序内存。 许多ARM处理器,包括非常受欢迎的ARMv7-A都具有弱有序存储器。

开发人员通常不会看到这些数据竞争,因为像锁这样的事情将会完成一个完整的内存屏障,实质上与同时获取和释放语义相同。 在获取锁之前,锁内没有负载可以被推测性地执行,它们被延迟直到获得锁。 没有存储可以通过锁释放被延迟,释放锁的指令被延迟,直到在锁内完成的所有写入全局可见。

更完整的例子是“双重检查locking”模式。 这种模式的目的是为了避免为了懒惰地初始化对象而不得不总是获取锁。

从维基百科绊了一跤:

 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永远不会阻止caching。 它所做的是保证其他处理器“看到”写入的顺序。 商店版本将延迟商店,直到所有未完成的写入完成,并且已经发出总线周期,告诉其他处理器如果恰好有相关行被高速caching,则丢弃/回写它们的高速caching行。 负载获取将刷新任何推测的读取,确保它们不会是过去的陈旧值。

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

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