C/C++/Java中volatile关键字的用法

5
在学习多线程编程时,经常会涉及volatile关键字。然而,在C/C++和Java(1.4版本及更早)中使用此关键字并不是实现多线程同步的可靠方法。维基百科列出了以下典型用法(没有详细解释):
  1. 允许访问内存映射设备
  2. 在setjmp和longjmp之间使用变量
  3. 在信号处理程序中使用变量
  4. 忙等待
我可以看到volatile关键字在上述用法中的作用,但由于我对每个领域都没有完全的理解,所以我无法确定它在每个用法中的确切行为。能否有人解释一下?

谢谢你的回答。 对于上面列出的用法,你有关于volatile如何工作的任何输入吗? - Ankur
8个回答

12
您的问题在技术上被称为“一罐虫子”!
对于c / c ++(我无法评论java)
您可以粗略地将volatile总结为指示编译器说“请不要优化它”,但是专业人士之间存在很多争议,例如:
a)是否对内核级代码有用 <-根据反馈进行了澄清
b)即使被大多数编译器正确实现。 此外,千万不要在多线程编程中使用它,这里有一个非常好的解释为什么不要使用 =编辑= 有趣的是,Dennis Ritchie反对包括它(以及const),详细信息在这里

1
应该注意的是,“At all useful”文档链接到Linux内核论文。该文档讨论了在Linux内核中使用volatile。在底部,他们仍然提供了volatile的良好用途,这些用途与多个线程无关。在那里使用volatile是有意义且很好的(如果编译器正确地处理volatile),例如在繁忙的循环中,变量在同一CPU的中断处理程序中更新。编译器只是无法知道它是否不应优化读取。您的观点听起来像“volatile”完全是垃圾(即使我知道您可能没有这个意图) :) - Johannes Schaub - litb
@litb 没错,很公正的观点。我会让这个观点更加清晰明了。我不会说它是一无是处的,只是想指出这并不是那么明显。 - zebrabox
正是我想说的。一个澄清是,优化通常将值存储在寄存器中,并从那里重复使用它。Volatile 强制代码在每次读取时重新从内存中读取该值,而不依赖于寄存器中缓存的值。现在的编译器进行了许多优化,包括重写语句及其顺序,因此仅仅从内存中重新读取值是不够的,如果依赖于其他线程中更新的顺序,则需要处理内存屏障。 - iain

7
由于您对这些用例感兴趣,我将解释第一个用例。请注意,这适用于从c/c++的角度来看,不确定它在java中的作用,尽管我怀疑在c/c++和java中,volatile通常用于完全不同的情况。
内存映射设备是处理器与其通信的外围设备,而不是通过特殊总线进行通信。
假设您有一个带有定时器的小灯,该定时器是内存映射的。您通过向其内存地址写入1并且其内部计时器倒计时5秒钟并关闭灯,并将内存位置重置为0来打开灯。现在,您正在开发需要在某些事件之后打开该灯,并有时在计数器到期之前关闭该灯的c程序。如果您使用常规变量(通常是指针或引用)来写入其内存位置,则由于编译器优化可能会出现许多问题。
如果您没有使用那么多变量,并且在没有其他变量使用该值的情况下打开灯并很快关闭它-有时编译器将完全消除第一个赋值,或者在其他情况下,它将仅保留处理器寄存器中的值,永远不会写入内存。在这两种情况下,由于其内存从未更改,因此灯永远不会打开。
现在考虑另一种情况,您检查灯的状态并且它处于打开状态。在这里,该值从设备的内存中提取出来并保存在处理器寄存器中。现在,几秒钟后,灯自动关闭。之后,您尝试再次打开灯,但是由于您读取了该内存地址并且自那以后没有更改过它,编译器会认为该值仍然是1,并且因此永远不会更改它,尽管实际上它现在是0。
通过使用volatile关键字,您可以防止编译器在将代码转换为机器代码时做出任何这些假设,并确保所有这些特定操作都是由程序员严格执行的。这对于内存映射设备非常重要,主要是因为内存位置不是由处理器严格更改的。出于同样的原因,在操作共享内存空间的多处理器系统中通常需要类似的做法。

4
我发现Herb Sutter在DDJ杂志上的这篇文章非常有趣,特别是关于C++、Java和C# .NET中如何处理volatile关键字的部分。 Dr.Dobbs volatile vs. volatile

2
易变变量在Java中非常有用(至少自从Java 5.0以来,它们的行为已经发生了改变),正如Brian Goetz在他的书《Java Concurrency in Practice》(JCIP)中所说-这是该主题的基本书籍(第37页):

确保对变量的更新可预测地传播到其他线程

显式同步也可以实现这一点,但我们通常不希望锁定一个值。双重检查锁定就是其中的经典例子(摘自维基百科)。
// Works with acquire/release semantics for volatile
// Broken under Java 1.4 and earlier semantics for volatile
class Foo {
    private volatile Helper helper = null;
    public Helper getHelper() {
        if (helper == null) {
            synchronized(this) {
                if (null == helper)
                    helper = new Helper();
            }
        }
        return helper;
    }

    // other functions and members...
}

如果辅助程序不是易失性的,这将无法工作。
易失性变量也可以用于实现非锁定并发数据结构,例如java.util.concurrent.ConcurrentHashMap(支持并发更新和访问而无需锁定-请参阅JDK源代码以了解其对易失性的使用)。
JCIP对双重检查锁定、易失性变量和Java并发性进行了很好的讨论。值得一读的是Joshua Bloch的《Effective Java》第二版。
还要注意,在Java中支持原子变量在java.util.concurrent.atomic包中。这些允许对值进行的更改在线程/处理器之间以类似于易失性变量的方式可见,但也允许执行“比较和设置”操作,这意味着可以安全地执行一些其他类型的并发操作而不需要锁定。

2

这里有一个很好的解释:http://en.wikipedia.org/wiki/Volatile_variable,但是简单来说,它告诉编译器不要假设变量不会被其他人访问,并且将其优化为寄存器并仅更新寄存器而不是实际存储是致命的。


在C和C++中,与Java不同,这是毫不奇怪的。例如,在Java中,如果编译器可以确定volatile只被单个线程访问,那么它可以被视为非volatile。 - Tom Hawtin - tackline
正确,我认为他们在维基百科上也提到了这一点。然而,指定符的目的仍然差不多是相同的。 - Fredrik

1

volatile关键字早在C语言中就出现了,它的基本作用是“关闭”一些编译器优化,这些优化假设如果一个变量没有被显式地改变,那么它就没有被改变过。它主要用于声明由中断处理程序更改的变量。例如,我曾经(80年代末)使用它来声明包含鼠标光标位置的全局变量。该位置由中断更改,如果没有使用volatile,主程序有时无法检测到其更改,因为编译器优化了变量访问,认为它不必要。

今天,这些用途通常已经过时了(除非您编写低级操作系统代码),但仍然存在一些罕见情况下需要使用volatile(确实非常罕见-例如,我可能已经7年没有使用它了)。

但对于多线程编程来说,它完全不推荐使用。问题在于它不能保护线程之间的并发访问,它只会消除同一线程中防止其“刷新”的优化。它并不适用于多线程环境。如果您使用Java,请使用synchronized。如果您使用C ++,请使用一些同步库,如pthreads或Boost.Threads(或者最好使用新的C ++ 0X线程库,如果可以的话)。


0

我已经有一段时间没有使用C++了,我真的不记得在那种语言中volatine的定义。但是Java语言规范明确指出volatile的目的是为了促进对变量的多线程访问。引用:“一个字段可以被声明为volatile,在这种情况下,Java内存模型(§17)确保所有线程看到变量的一致值。”他们继续说,对volatile值的引用保证按照它们在代码中指定的顺序满足,即如果您声明i和j为volatile,然后写入“++i; ++j”,那么实际上i将始终在j之前递增。

我唯一记得在Java中使用volatile的时候是当我有一个线程可能设置取消标志,另一个线程循环执行一些大操作,并且每次通过循环检查取消标志。这确实像我预期的那样工作。

我同意“volatile”具有非常有限的用处。大多数多线程需要在某个时刻进行“同步”。但是“有限”和“没有”并不是同一件事。余弦函数在大多数商业应用程序中的用处非常有限。但是当你需要它时,哇,那就省了很多麻烦。


-1

当一个变量可以被多个线程访问,并且您希望在每个指令中,您的代码都能获取该变量的更新值时,应使用Volatile变量。

编译器通常会优化代码并将变量存储在寄存器中,而不是每次从内存中获取变量(如果它们发现没有人更新它)。

但是通过使用volatile,您可以强制编译器每次获取更新的值。


1
这不正确。'volatile'基本上只是告诉编译器,“你不能优化这个变量,因为它可能会被你无法控制的外部因素修改”。你真的无法保证会发生什么。其他答案中的链接解释了为什么按照你所描述的方式使用'volatile'是错误的。 - Richard Corden
考虑CPU缓存。C抽象机不知道CPU缓存。对于它来说,如果一个值只在一个 CPU的缓存中,那么它可能在存储器中。对于多线程应用程序,使用这样的缓存作为“存储器”的意义是徒劳的。 - Johannes Schaub - litb
@Richard:同意。volatile关键字不应该被用作在多个线程之间实现同步的手段。 - Ankur
除了在单个CPU机器上,如果您仔细注意内存访问的原子性和非原子性,它可以用于这种目的。这样的系统远远超过多核桌面计算机,但对于那些不为其开发的人来说,它们大多是不可见的,也不容易被识别为计算机。 - Chris Stratton

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接