什么是"volatile"关键字用于?

Mir*_*cea 118 c# java volatile

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

Sjo*_*erd 159

考虑这个例子:

int i = 5;
System.out.println(i);
Run Code Online (Sandbox Code Playgroud)

编译器可以优化它以打印5,如下所示:

System.out.println(5);
Run Code Online (Sandbox Code Playgroud)

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

volatile关键字防止了这种优化和缓存,并且因此当一个变量可通过另一个线程被改变是有用的.

  • @Sjoerd:我不确定我理解这个例子.如果`i`是局部变量,那么其他任何线程都无法改变它.如果它是一个字段,编译器不能优化调用,除非它是`final`.我不认为编译器可以基于假设一个字段"看起来"`final`而没有明确声明它来进行优化. (4认同)
  • 我相信优化仍然有效,`i`标记为`volatile`.在Java中,所有关于*发生在*之前的关系. (3认同)
  • C#和java不是C++。这是不正确的。它不会阻止缓存,也不会阻止优化。它涉及读取-获取和存储-释放语义,这是弱有序内存架构所必需的。这是关于投机执行的。 (2认同)

Wil*_*l A 83

对于C#和Java,"volatile"告诉编译器必须永远不要缓存变量的值,因为它的值可能会在程序本身的范围之外发生变化.然后,如果变量"在其控制范围之外",则编译器将避免可能导致问题的任何优化.

  • 它仍然比那更微妙****. (9认同)

Sau*_*til 38

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

  • 变量是非易失性的

当两个线程A和B访问非易失性变量时,每个线程将在其本地缓存中维护变量的本地副本.线程A在其本地缓存中完成的任何更改都不会对线程B可见.

  • 变量是不稳定的

当变量被声明为volatile时,它实质上意味着线程不应该缓存这样的变量,换句话说,线程不应该信任这些变量的值,除非它们直接从主存储器读取.

那么,何时使变量变为volatile?

如果有一个变量可以被许多线程访问,并且您希望每个线程获得该变量的最新更新值,即使该值由程序中的任何其他线程/进程/更新.

  • 我认为这根本不正确。如果它是正确的,那么多线程代码将始终需要“易失性”。 (3认同)
  • 错误的。它与“防止缓存”无关。它是关于由编译器通过推测执行重新排序或 CPU 硬件。 (2认同)

kro*_*ock 34

volatile关键字在Java和C#不同的含义.

Java的

来自Java语言规范:

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

C#

来自volatile关键字的C#参考:

volatile关键字表示可以通过诸如操作系统,硬件或同时执行的线程之类的东西在程序中修改字段.


dou*_*536 32

易失性字段的读取具有获取语​​义.这意味着可以保证从volatile变量读取的内存将在任何后续内存读取之前发生.它阻止编译器进行重新排序,如果硬件需要它(弱排序的CPU),它将使用特殊指令使硬件刷新在易失性读取之后发生的任何读取但是推测性地提前启动,或者CPU可以通过防止在负荷获得和退休之间发生任何投机负荷,防止它们首先发布.

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

请考虑以下示例:

something.foo = new Thing();
Run Code Online (Sandbox Code Playgroud)

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

如何对写入进行foo如此糟糕的重新排序?如果高速缓存行保持foo在高速缓存中,并且构造函数中的存储错过了高速缓存,那么存储可能比对高速缓存的写入错过更快地完成.

英特尔的(可怕的)Itanium架构的内存很弱.原始XBox 360中使用的处理器具有弱序存储器.许多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;
    }
}
Run Code Online (Sandbox Code Playgroud)

在此示例中,MySingleton构造函数中的商店在存储之前可能对其他处理器不可见mySingleton.如果发生这种情况,查看mySingleton的其他线程将不会获得锁定,并且它们不一定会获取对构造函数的写入.

volatile永远不会阻止缓存.它的作用是保证其他处理器"看到"写入的顺序.商店发布将延迟商店,直到所有待处理的写入完成并且已发出总线周期,告知其他处理器如果碰巧有相关的线路被缓存则丢弃/回写其高速缓存行.加载获取将刷新任何推测的读取,确保它们不会是过去的过时值.


Tho*_*sen 9

在Java中,"volatile"用于告诉JVM该变量可能同时被多个线程使用,因此无法应用某些常见的优化.

值得注意的是,访问同一变量的两个线程在同一台机器上的不同CPU上运行.由于内存访问速度比缓存访问速度慢得多,因此CPU会积极缓存它所拥有的数据是很常见的.这意味着如果在CPU1中更新数据,它必须立即通过所有缓存和主存储器,而不是在缓存决定清除自身时,以便CPU2可以看到更新的值(再次忽略路上的所有缓存).