在Java中,何时使用volatile关键字?

91

我读过 "Java中何时使用'volatile'?"但仍感到困惑。我如何知道何时应将变量标记为volatile?如果我弄错了,要么省略了某个需要它的volatile,要么将volatile放在不需要的东西上怎么办?在多线程代码中确定哪些变量应该是volatile时有什么经验法则?


请问您在Java中是否曾经使用过volatile关键字? - nos
2
锁提供两个主要功能:互斥和可见性。易失变量共享 synchronized 的可见性特征,但没有原子性特征。阅读更多(作者:Brian Goetz)。 - Daniel
除了8字节的原始类型long和double上的volatile声明可以使单个读取或单个写入具有原子性之外,没有任何原子性特征。这是小规模的原子性。您需要使用同步块来实现大规模的原子性。 - H2ONaCl
7个回答

116

当你想让一个成员变量被多个线程访问,但不需要复合原子性时(不确定这是否是正确的术语),你基本上会使用它。

class BadExample {
    private volatile int counter;

    public void hit(){
        /* This operation is in fact two operations:
         * 1) int tmp = this.counter;
         * 2) this.counter = tmp + 1;
         * and is thus broken (counter becomes fewer
         * than the accurate amount).
         */
        counter++;
    }
}

上面的例子很糟糕,因为你需要复合原子性

 class BadExampleFixed {
    private int counter;

    public synchronized void hit(){
        /*
         * Only one thread performs action (1), (2) at a time
         * "atomically", in the sense that other threads can not 
         * observe the intermediate state between (1) and (2).
         * Therefore, the counter will be accurate.
         */
        counter++;
    }
}

现在来看一个有效的示例:

 class GoodExample {
    private static volatile int temperature;

    //Called by some other thread than main
    public static void todaysTemperature(int temp){
        // This operation is a single operation, so you 
        // do not need compound atomicity
        temperature = temp;
    }

    public static void main(String[] args) throws Exception{
        while(true){
           Thread.sleep(2000);
           System.out.println("Today's temperature is "+temperature);
        }
    }
}
现在,为什么你不能只使用private static int temperature呢?实际上你可以这样写(也就是说你的程序不会崩溃或者出现什么问题),但是其他线程对temperature的更改对于主线程可能是“可见的”也可能是“不可见的”。
基本上这意味着如果你不使用volatile,你的应用程序甚至可以永远写今天的温度为0(实际上该值最终会变得可见)。然而,在必要时不使用volatile可能会导致严重问题(由未完全构建的对象等引起的恶性错误)。
如果你将volatile关键字放在不需要volatile的代码上,它不会影响你的代码正确性(即行为不会改变)。在性能方面,这取决于JVM实现。理论上,由于编译器无法进行重排序优化并且必须使CPU缓存失效等,你可能会获得微小的性能下降,但随后编译器可能会证明你的字段永远不可能被多个线程访问,从而完全删除volatile关键字并将其编译为相同的指令。
编辑:
回应此评论:
“好吧,但为什么我们不能将todaysTemperature同步化并创建一个同步的getter来获取temperature?”
你可以这样做,它会正确地工作。任何你可以用volatile完成的事情都可以用synchronized完成,但反过来则不行。有两个原因你可能更喜欢volatile
1.较少的错误倾向:这取决于上下文,但在许多情况下,使用volatile比使用锁具有更少的并发错误,如在持有锁时阻塞、死锁等。
2.更高的性能:在大多数JVM实现中,volatile的吞吐量和延迟可以显著提高。然而,在大多数应用程序中,差异太小以至于无关紧要。

4
我经常看到这个问题,而这是我看到的第一个让我完全理解的答案。谢谢你。 - uyuyuy99
3
@SemyonDanilov在底部添加了答案。希望有所帮助。 - Enno Shioji
3
对我来说,同步的getter如何在这里有帮助仍然不太清楚?如果不使用volatile,我们仍然不能保证线程本地缓存。 - St.Antario
3
@St.Antario说:synchronized 提供的保证是 volatile 提供保证的超集,因此在使用 synchronized 的情况下同时使用 volatile 是多余的。 - Enno Shioji
是的,我已经在这里提问过 - St.Antario
显示剩余4条评论

15

volatile最常用于无锁算法。当您不使用锁来访问存储共享数据的变量时,将该变量标记为volatile,并且您希望一个线程所做的更改在另一个线程中可见,或者您想创建“happens-after”关系以确保计算不会被重新排序,同样,以确保更改在适当的时间可见。

JMM Cookbook描述了哪些操作可以重新排序,哪些不能。


7

volatile 关键字保证了 volatile 变量的值始终从主内存中读取,而不是从线程的本地缓存中读取。

来自 Java 并发 tutorial

使用 volatile 变量可以减少内存一致性错误的风险,因为对 volatile 变量的任何写操作都会与随后对该变量的读操作建立 happens-before 关系。

这意味着对 volatile 变量的更改始终对其他线程可见。这也意味着当线程读取 volatile 变量时,它不仅看到了 volatile 的最新更改,还看到了导致更改的代码的副作用。

关于您的查询:

我如何知道何时应将变量标记为 volatile?在多线程代码中找出应该是 volatile 的变量的经验法则是什么?

如果您认为所有读取器线程始终获取变量的最新值,则必须将变量标记为 volatile

如果您有一个写入器线程用于修改变量的值和多个读取器线程用于读取变量的值,则 volatile 修饰符保证内存一致性。

如果您有多个线程来读写变量,仅使用volatile修饰符不能保证内存一致性。您必须同步代码或使用高级并发构造,如LocksConcurrent CollectionsAtomic variables等。
相关SE问题/文章: Java文档中的volatile变量解释 Java中volatile和synchronized的区别 javarevisited文章

6
volatile 也可用于在多线程环境中安全地发布不可变对象。 声明像 public volatile ImmutableObject foo 这样的字段,确保所有线程始终看到当前可用的实例引用。 有关该主题的更多信息,请参见Java并发编程实践

3
我认为这里的“不可变”部分有些值得质疑...请参见我在此处的回答:https://dev59.com/c2865IYBdhLWcg3wIrFg以及JSR-133 FAQ,第 http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#volatile 条目。我认为更准确的说法是您可以安全地发布不需要其他同步的对象...我可能没有完全理解,但只要进行了volatile写操作,先前在同一线程中发生的普通写操作将对任何读取的线程可见... - andersoj
1
@andersoj 是正确的,volatile是可传递的。如果读取a.b是volatile的,那么读取a就是volatile的。如果在程序顺序中先于b之前写入a,则对a.b的写入发生在对a的写入之前。因此,可以使用volatile安全地发布可变(否则非线程安全)对象。我认为这不是一个特别好的主意,而且总是宣扬尽可能使用不可变对象,但这仍然是一个重要的观点。 - Jed Wesley-Smith

3
实际上,我不同意最受欢迎的答案中给出的示例,据我所知,它并没有恰当地说明Java内存模型中的volatile语义。Volatile具有更复杂的语义。
在所提供的示例中,即使有另一个运行的线程应该更新温度,如果该其他线程从未被调度,则主线程仍可能永远继续打印“今天的温度为0”。
更好地说明volatile语义的方法是使用两个变量。
为简单起见,我们将假定通过方法“setTemperatures”是更新这两个变量的唯一方法。
为简单起见,我们将假设只有2个线程正在运行:主线程和线程2。
//volatile variable
private static volatile int temperature; 
//any other variable, could be volatile or not volatile doesnt matter.
private static int yesterdaysTemperature
//Called by other thread(s)
public static void setTemperatures(int temp, int yestemp){
    //thread updates yesterday's temperature
    yesterdaysTemperature = yestemp;
    //thread updates today's temperature. 
    //This instruction can NOT be moved above the previous instruction for optimization.
    temperature = temp;
   }

无论是编译器、运行时还是硬件,最后两个任务指令都不能被重新排序以进行优化。

public static void main(String[] args) throws Exception{
    while(true){
       Thread.sleep(2000);
       System.out.println("Today's temperature is "+temperature); 
       System.out.println("Yesterday's temperature was "+yesterdaysTemperature );
 }
}

一旦主线程读取了volatile变量temperature(在打印它的过程中):
1)无论有多少个线程写入它,无论它们使用哪种方法进行更新(同步或非同步),都保证能够看到该volatile变量的最新写入值。
2)如果主线程中的system.out语句在线程2运行语句temperature = temp之后运行,则昨天和今天的温度将保证打印出线程2运行语句temperature=temp时所设置的值。
如果存在以下情况,则此情况会变得更加复杂:a)多个线程正在运行;b)除了setTemperatures方法之外,还有其他可以更新昨天和今天温度变量的方法,这些方法正在被其他线程积极调用。我认为需要一篇相当大的文章来分析基于Java内存模型描述volatile语义的含义。
简而言之,仅尝试使用volatile进行同步是极其危险的,最好坚持同步您的方法。

你的示例也可以用来解释何时使用 synchronized 更好。那些打印语句可能会在另一个线程执行了 "yesterdaysTemperature = yestemp",但尚未执行 "temperature = temp" 时运行,不是吗? - EricS

2

http://mindprod.com/jgloss/volatile.html

“volatile”关键字用于可能被其他线程同时修改的变量。

由于其他线程无法看到局部变量,因此永远不需要将局部变量标记为volatile。您需要使用synchronized来协调来自不同线程的变量更改,但通常仅需要使用volatile来查看它们。”


2

Volatile的意思是保持值的不断变化。这个变量的值永远不会在本地线程中缓存:所有读写操作都将直接进入“主内存”。换句话说,Java编译器和线程不会缓存此变量的值,而总是从主内存中读取它。


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