我该如何在Java中正确使用volatile关键字?

14

假设我有两个线程和一个对象。其中一个线程对该对象进行赋值:

public void assign(MyObject o) {
    myObject = o;
}

另一个线程正在使用该对象:

public void use() {
    myObject.use();
}

变量 myObject 必须声明为 volatile 吗?我试图理解什么情况下需要使用 volatile 和何时不需要,这让我感到困惑。第二个线程是否可能在其本地内存缓存中保留对旧对象的引用?如果不是,则原因是什么?

非常感谢。


1
你正在使用哪个版本的Java? - Alerty
我正在移动设备上进行开发。基本上是Java 1.4.x。 - Tiyoal
6个回答

11

我正在尝试理解何时使用volatile关键字

大多数情况下应避免使用它。而是使用AtomicReference(或其他适当的atomic类)。内存效果相同,意图更加清晰。

我强烈建议阅读优秀的Java Concurrency in Practice以获得更好的理解。


2
谢谢你的建议。我同意我不必使用这些“低级别的东西”。然而,我感到有必要了解它在引擎盖下是如何工作的。这本书对此是一个非常好的建议。谢谢。 - Tiyoal
@Tiyoal,如需了解细节,请参阅JLS的第17.4节,其中讨论了内存模型:http://java.sun.com/docs/books/jls/third_edition/html/memory.html#17.4 - Kevin
并发实践中的第二点。Volatile是synchronized的一种较弱形式。如果一个变量被一个线程写入并被多个线程读取,为了确保立即看到写入,可以使用它。 - bwawok
2
@Kevin:引用自此链接的一句话,来自@Alerty:“访问该变量就像它被包含在一个同步块中,以其自身为锁。”这基本上就是我在回答中试图表达的内容。 - BalusC
@bwawolk:这就是我的问题所在。一个线程写入,一个(多个)线程读取。为什么我不需要将共享对象标记为volatile? - Tiyoal
我在一些在线教程中读到了关于volatile关键字的内容,其中提到线程可能会决定将变量的“本地值”存储在其本地线程缓存中。为了克服这个问题,您需要将变量声明为volatile。因此,我想知道如果第二个线程决定对myObject这样做会发生什么。他会继续使用过时的对象吗?这就是为什么我想知道是否必须将其声明为volatile。我理解得对吗? - Tiyoal

6

不用过多关注复杂的技术细节,你可以将volatile视为对变量的一种synchronized修饰符。当你想要同步访问方法或块时,通常会使用以下synchronized修饰符:

public synchronized void doSomething() {}

如果您想“同步”访问变量,那么您需要使用volatile修饰符:
private volatile SomeObject variable;

在幕后,它们做着不同的事情,但是效果是相同的:更改会立即对下一个访问线程可见。
在您的特定情况中,我认为volatile修饰符没有任何价值。volatile不能以任何方式保证分配对象的线程将在使用对象的线程之前运行。可能反过来更好。你可能只想在use()方法中首先执行空值检查。
更新:还请参阅此文章

访问变量就像被封装在同步块中一样,在自身上同步。我们在第二点中说“就像”,因为对于程序员(和可能大多数JVM实现),实际上并没有涉及锁定对象。


7
这是完全不正确的。Volatile完全与变量的内存效果和可见性有关,而不是并发访问。 - Kevin
3
@Kevin:是的,那是技术细节。我说的是效果,这样更容易理解。变量的改变立即对下一个访问线程可见。 - BalusC
@Kevin:我不确定这样做是否会弊大于利。这可能取决于人们如何解释这一切。然而,我完全同意你最好寻找优秀的java.util.concurrent API,我为你的建议点赞。 - BalusC
虽然我认为你对使用各种方法的影响有一个相当好的解释,但是你的建议是完全错误的。在线程之间没有某种形式的内存同步的情况下,读者可能永远看不到更改。此外,你所描述的前后场景在问题中并不存在。我们都知道这两种方法可能会被多次调用,null值可能是可以接受的,也可能不可接受。 - Robin
@Robin:你提出了一个很好的观点。在我们给出实质性建议之前,OP应该首先更详细地澄清功能要求。现在还太模糊了。 - BalusC
显示剩余2条评论

4

声明一个Java的volatile变量意味着:

  • 这个变量的值永远不会被本地线程缓存
  • 访问该变量的行为就像它被封装在同步块中一样

volatile的典型和最常见的用法是

public class StoppableThread extends Thread {
  private volatile boolean stop = false;

  public void run() {
    while (!stop) {
      // do work 
    }
  }

  public void stopWork() {
    stop = true;
  }
}

2

在这种情况下,您可以使用volatile。您需要使用volatile、同步访问变量或类似机制(如AtomicReference)来确保在分配线程上进行的更改实际上可见于读取线程。


但是这如何确保分配对象的线程在使用对象的线程之前运行? - BalusC
我为什么需要在这里进行同步?声明为volatile不足以吗? - Tiyoal
@Tiyoal - 这是一个二选一的情况。你需要使用其中一个选项。 - Robin
@BalusC - 这与线程无关,只需要简单地检查 null。没有规定必须先完成一个操作再完成另一个操作,也没有规定不能多次调用它们两个。因此,如果每次调用都可能返回空值,则读取线程必须检查 null。您引用的是生产者/消费者场景,这将是完全不同的解决方案。 - Robin

1

我花了很多时间来理解volatile关键字。我认为@aleroot给出了世界上最好和最简单的例子。

这是我为小白(像我一样)解释的方式:

场景1:假设stop没有声明为volatile,那么一个给定的线程会这样想和执行:

  1. 调用stopWork():我必须将stop设置为true
  2. 太好了,在我的本地堆栈中完成了,现在我必须更新JVM的主堆。
  3. 糟糕,JVM告诉我要让CPU给另一个线程,我必须停一会儿......
  4. 好的,我回来了。现在我可以使用我的值更新主堆。正在更新...

场景2:现在让stop声明为volatile

  1. 当调用stopWork()时:我必须将stop设置为true
  2. 太好了,我已经在本地堆栈中完成了,现在我需要更新JVM的主堆。
  3. 对不起,伙计们,我现在必须执行(2)-我被告知它是volatile。我必须再占用一点CPU时间...
  4. 正在更新主堆...
  5. 好的,我完成了。现在我可以让步了。

没有同步,只是一个简单的想法...

为什么不把所有变量都声明为volatile呢?因为Scenario2/Step3。这有点低效,但仍然比常规同步要好。


0

这里有一些令人困惑的评论:为了澄清,您的代码在当前状态下是不正确的,假设两个不同的线程调用assign()use()

在没有volatile或其他happens-before关系(例如,在公共锁上同步)的情况下,对assign()中的myObject的任何写入都不能保证被调用use()的线程看到--不会立即,不会及时,事实上可能永远不会看到。

是的,volatile是纠正这种行为的一种方法(假设这是不正确的行为--有可能存在您不关心这一点的情况!)。

您完全正确,'use'线程可以看到myObject的任何“缓存”值,包括它在构造时分配的值和任何中间值(同样在没有其他happens-before点的情况下)。


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