“ volatile”关键字的作用是什么?


130

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


1
volatile的问题之一是它意味着不止一件事。向编译器提供的信息是不进行时髦的优化是C的传统。这意味着在访问时应使用内存屏障。但是在大多数情况下,这只会降低性能并/或使人们感到困惑。:P
AnorZaken

Answers:


93

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


@汤姆-先生,请注意并修正。
将在

11
它仍然比那微妙得多。
汤姆·霍顿

1
错误。不阻止缓存。看我的答案。
doug65536 '16

168

考虑以下示例:

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

编译器可以对此进行优化以仅打印5,如下所示:

System.out.println(5);

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

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


3
我相信i标记为的优化仍然有效volatile。在Java中,一切都是事前发生的关系。
汤姆·霍顿

感谢您的发布,所以volatile具有可变锁定的连接吗?
Mircea 2010年

@Mircea:有人告诉我,将某些内容标记为volatile是关于:将字段标记为volatile将使用某种内部机制来允许线程看到给定变量的一致值,但这在上面的答案中未提及...也许有人可以确认这一点?谢谢
npinti 2010年

5
@Sjoerd:我不确定我是否理解此示例。如果i是局部变量,则其他任何线程都无法更改它。如果是字段,则除非,否则编译器无法优化调用final。我不认为编译器可以基于假设final未明确声明字段“看起来”的情况来进行优化。
polygenelubricants 2010年

1
C#和Java不是C ++。这是不正确的。它不会阻止缓存,也不会阻止优化。它涉及读取-获取和存储释放语义,这是弱顺序内存体系结构所必需的。它与投机执行有关。
doug65536 '16

40

要了解volatile对变量的作用,重要的是要了解变量不是volatile时会发生什么。

  • 变量是非易失性的

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

  • 变量是易失的

当将变量声明为volatile时,这实质上意味着线程不应缓存此类变量,或者换句话说,线程不应信任这些变量的值,除非直接从主内存中读取它们。

那么,何时使变量可变?

当您拥有一个可由多个线程访问的变量,并且您希望每个线程都获得该变量的最新更新值时,即使该值已由程序的任何其他线程/进程/外部更新。


2
错误。它与“防止缓存”无关。它是关于由编译器或通过推测执行执行的CPU硬件重新排序。
doug65536 '16

37

易读字段的读取具有语义。这意味着可以确保从volatile变量读取的内存将在随后的任何内存读取之前发生。它阻止编译器进行重新排序,并且如果硬件需要它(CPU排序不充分),它将使用特殊指令使硬件清除易失性读取之后发生但推测性启动得早的所有读取。通过防止在获取负载的问题和其收回之间发生任何投机性负载,从而防止从一开始就发布它们。

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

考虑以下示例:

something.foo = new Thing();

如果foo是类中的成员变量,并且其他CPU可以访问引用的对象实例something,则它们可能会看到值foo更改,然后Thing构造器中的内存写入才能全局可见!这就是“无序内存”的意思。即使编译器将所有存储都存储在构造函数中,也可以在之前存储到foo。如果foo是,volatile则存储foo将具有释放语义,并且硬件保证foo在允许进行写入之前,写入之前的所有写入对于其他处理器都是可见的foo

如何对写入进行foo如此严重的重新排序?如果高速缓存行保持foo在高速缓存中,并且构造函数中的存储未命中高速缓存,则存储的完成时间可能比对高速缓存未命中的写入要早得多。

英特尔的(糟糕的)Itanium体系结构的内存排序较弱。原始XBox 360中使用的处理器的内存排序较弱。许多ARM处理器(包括非常流行的ARMv7-A)的内存排序都很弱。

开发人员通常看不到这些数据争用,因为诸如锁之类的事情会造成完全的内存障碍,本质上与同时获取和释放语义相同。在获取锁之前,无法推测性地执行锁内部的负载,这些负载会延迟到获取锁为止。没有存储可以在锁释放期间被延迟,释放锁的指令将被延迟,直到在锁内部完成的所有写入都全局可见为止。

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

摘自Wikipedia:

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从不阻止缓存。它所做的是保证其他处理器“看到”的写入顺序。存储释放将延迟存储,直到所有未完成的写操作完成并且发出了总线周期,告诉其他处理器如果碰巧已经缓存了相关的行,则放弃/回写其缓存行。负载获取将刷新所有推测的读取,以确保它们不会成为过去的过时值。


很好的解释。同样是很好的双重检查锁定示例。但是,我仍然不确定何时使用,因为我担心缓存方面。如果我编写了一个队列实现,其中只有1个线程正在写入,而只有1个线程正在读取,我可以不加锁而仅将头部和尾部的“指针”标记为易失性吗?我想确保读者和作家都能看到最新的值。
nickdu

两者head和都tail需要保持易变,以防止生产者假设tail不会改变,并防止消费者假设head不会改变。另外,head必须可变,以确保在全局可见之前存储队列数据写入head是全局可见的。
doug65536 '16

+1,像“最新” /“最新”之类的术语不幸地暗示了单数正确值的概念。实际上,两个竞争者可以在同一时间越过终点线-在CPU上,两个内核可以在同一时间请求写入。毕竟,内核不会轮流做工作-这会使多核毫无意义。良好的多线程思维/设计不应专注于试图强迫低级的“最新性”-本质上是假的,因为锁只会迫使核心一次无公平地任意选择一位发言人-而是要设法消除需要这样一个不自然的概念。
AnorZaken

34

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

爪哇

Java语言规范中

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

C#

volatile关键字的C#参考中:

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


非常感谢您的发布,正如我在Java中所理解的那样,它的作用就像将变量锁定在线程上下文中,在C#中,如果使用了变量,则不仅可以从程序中更改变量的值,而且诸如OS之类的外部因素也可以修改其值(没有暗示的锁定)...请让我知道我是否了解正确的区别...
Mircea 2010年

Java中的@Mircea不涉及锁定,它只是确保将使用volatile变量的最新值。
krock

Java是否承诺某种内存屏障,还是像C ++和C#只是承诺不优化引用?
史蒂文·苏迪特

内存屏障是实现细节。Java实际上承诺的是,所有读取都将看到最新写入所写入的值。
斯蒂芬·C

1
@StevenSudit是的,如果硬件需要屏障或加载/获取或存储/释放,则它将使用这些说明。看我的答案。
doug65536 '16

9

在Java中,“易失性”用于告诉JVM变量可以同时由多个线程使用,因此某些通用优化无法应用。

值得注意的是,访问同一变量的两个线程在同一台计算机上的不同CPU上运行的情况。CPU主动缓存其保存的数据非常普遍,因为内存访问比缓存访问慢得多。这意味着,如果在CPU1中更新了数据,则必须立即通过所有高速缓存并到达主内存,而不是在高速缓存决定清除自身时,才可以使CPU2看到更新后的值(再次忽略该途中的所有高速缓存)。



0

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


1
我认为这不能“解决”问题。这是在某些情况下有帮助的工具。在需要锁定的情况下(例如在竞赛条件下),不要依赖于volatile。
Scratte
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.