使用volatile关键字与可变对象

32
在Java中,我了解到volatile关键字提供了变量的可见性。问题是,如果一个变量是指向可变对象的引用,volatile是否也可以提供该对象内部成员的可见性?
在下面的示例中,如果多个线程正在访问volatile Mutable m并更改value,那么它能否正常工作?
示例:
class Mutable {
    private int value;
    public int get()
    {
        return a;
    }
    public int set(int value)
    {
        this.value = value;
    }
}

class Test {
    public volatile Mutable m;
}

1
为了比较,您可能想看一下正确执行此操作并内置的AtomicReference。 - Peter Lawrey
如果你只是阅读 Test.m,那么根本没有建立任何先于发生的关系。 - Tom Hawtin - tackline
5个回答

18
这是关于volatile的一些细节解释的旁注说明。我写在这里是因为它对于评论来说太多了。我想给出一些示例,展示volatile如何影响可见性以及在jdk 1.5中发生了什么变化。
给定以下示例代码:
public class MyClass
{
  private int _n;
  private volatile int _volN;

  public void setN(int i) {
    _n = i;
  }
  public void setVolN(int i) {
    _volN = i;
  }
  public int getN() { 
    return _n; 
  }
  public int getVolN() { 
    return _volN; 
  }

  public static void main() {
    final MyClass mc = new MyClass();

    Thread t1 = new Thread() {
      public void run() {
        mc.setN(5);
        mc.setVolN(5);
      }
    };

    Thread t2 = new Thread() {
      public void run() {
        int volN = mc.getVolN();
        int n = mc.getN();
        System.out.println("Read: " + volN + ", " + n);
      }
    };

    t1.start();
    t2.start();
  }
}

这个测试代码的行为在jdk1.5及以上版本中有很好的定义,但在jdk1.5之前的版本中没有定义。

在jdk1.5之前的世界中,volatile访问和非volatile访问之间没有定义的关系。因此,该程序的输出可能是:

  1. 读取:0,0
  2. 读取:0,5
  3. 读取:5,0
  4. 读取:5,5

在jdk1.5+的世界中,volatile的语义被更改,以便volatile访问以与同步相同的方式影响非volatile访问。因此,在jdk1.5+的世界中只有某些输出是可能的:

  1. 读取:0,0
  2. 读取:0,5
  3. 读取:5,0 <- 不可能
  4. 读取:5,5

输出3不可能,因为从volatile _volN中读取“5”会在2个线程之间建立同步点,这意味着t1在分配给_volN之前执行的所有操作必须对t2可见。

进一步阅读:


好的,如果没有使用volatile,则存在以下关系:hb(w_n, w_volN),hb(r_volN,r_n)。如果添加了volatile,则可以添加hb(w_volN,r_volN)。通过传递性,现在有hb(w_n,r_n)。传递性规则与volatile的语义无关。 - OrangeDog
谢谢。为什么你一开始不能提供这些链接呢?然而,对于7年前就已经解决的实现问题的分散讨论并不是很有帮助。我已经修改了我的答案,避免了对“同步”这个词的草率使用。 - OrangeDog
2
@OrangeDog - 它们是谷歌上的热门搜索结果,所以它们很容易找到。我的观点是要表明volatile绝对会影响可见性。我试图通过一个例子来展示它是如何改变的,从而证明它会影响可见性。这些链接也说明了volatile如何影响可见性。因此,你的答案最多只能说是误导,最坏的情况下是错误的。(实际上,我并不关心你使用“同步”这个词,因为那是你回答中唯一正确的陈述)。 - jtahlborn
1
嘿@jtahlborn,来自未来的一个后续问题。如果情况3不可能,是否意味着编译器或jvm无法重新排序独立变量的赋值,如果其中一个是易失性的?如果t1被重新排序为首先分配易失性变量,那么情况3将成为可能。 - user2259824
1
@user2259824 - 就像我已经说过的那样,基于JDK 1.5+的内存语义是不可能实现的。 - jtahlborn
显示剩余2条评论

8
在你的例子中,volatile关键字只保证了任何线程写入到'm'的最后一个引用将对随后读取'm'的任何线程可见。
它并不保证你的get()
因此,使用以下序列:
Thread-1: get()     returns 2
Thread-2: set(3)
Thread-1: get()    

你完全可以得到2而不是3,这是合法的。 volatile 对此没有任何影响。

但如果将你的Mutable类更改为以下内容:

class Mutable {
    private volatile int value;
    public int get()
    {
        return a;
    }
    public int set(int value)
    {
        this.value = value;
    }
}

那么,保证来自Thread-1的第二个get()将返回3。

但请注意,volatile通常不是最好的同步方法。

在您简单的get/set示例中(我知道这只是一个示例),使用适当的同步并提供实用方法的类,如AtomicInteger,会更好。


1
这个答案基本上是正确的。然而需要注意的是,当m被赋值后,内部值将会被正确地显示出来。只有在后续调用set()但没有写入m时,你才会遇到问题。 - jtahlborn
2
@jtahlborn,你能提供一下参考资料吗?我在JLS中找不到相关内容。它只是说“对volatile字段(§8.3.1.4)的写入发生在对该字段后续读取之前”,但这并不意味着有关字段所引用的对象的实际初始化的任何内容。 - Sergei Tachenov
1
阅读第17节。"发生在"的定义和17.4.2解释了volatile现在在内存语义上等同于synchronized。 - jtahlborn
@jtahlborn:通过内部值,您指的是实例的属性。由于m只是对象的引用,因此说调用set()实际上是写入m是否错误? - Farhan stands with Palestine
@ShirgillFarhanAnsari - 不,对set()的调用不会写入m。你只需要读取m引用以调用set() - jtahlborn
显示剩余2条评论

5
volatile 只能保证声明的对象引用的内容,它并不能同步实例的成员变量。
根据维基百科的描述:
  • (在所有版本的Java中) 对于volatile变量的读和写有一个全局的排序。这意味着每个访问volatile字段的线程都会在继续执行之前读取其当前值,而不是(可能)使用缓存的值。(但是,volatile读和写与常规读和写的相对顺序没有保证,因此通常不是一个有用的线程构造)。
  • (在Java 5或更高版本中)volatile的读和写建立了happens-before关系,就像获取和释放互斥锁一样。
基本上,通过声明volatile字段,与之交互将创建一个“同步点”,在此之后,任何更改都将在其他线程中可见。但是,在此之后,使用get()set()是不同步的。Java规范有更详细的说明。

3
这不正确。您忽略了维基百科中的第二点,即Java 5使得volatile关键字影响非volatile变量。 - jtahlborn
@jtahlborn 你是正确的,我已经添加了第二点并稍微改了一下我的解释。 - Mario F
1
你的解释仍然不正确。它似乎仍在暗示volatile不会影响非volatile字段。在第一次给m赋值时,value的可见性是有保证的。请查看我对@Gugusee帖子的评论以获取更多细节。 - jtahlborn

0

volatile并不会“提供可见性”。它的唯一作用是防止处理器缓存变量,从而在并发读写时提供happens-before关系。它不会影响对象的成员,也不会提供任何同步synchronized锁定。

由于您没有告诉我们代码的“正确”行为是什么,因此无法回答这个问题。


2
实际上,volatile 可以影响对象的成员(虽然不会影响 OP 的使用方式)。happens before 关系确实提供可见性保证。 - jtahlborn
1
@OrangeDog:假设您创建了一个Mutable的本地实例,并使用3个不同的值调用了set()方法,然后将本地Mutable实例分配给m。所有后续对m的读取都保证能够看到在它被分配之前第一个线程设置的“最后”值。因此,在分配m时保证了值的可见性。但是,正如其他地方正确指出的那样,分配给m之后任何未来调用set()的影响都不能保证是可见的。(其中“可见”表示另一个线程可以看到)。 - jtahlborn
@OrangeDog:对m的写入为值提供了可见性保证。也许这对你来说很合理,所以你不明白我在说什么。然而,如果你看一下JDK1.5之前的volatile语义,你会发现这并不是这种情况。在JDK1.5之前,value绝对没有任何保证。 - jtahlborn
@John - 嗯,是的,显然。这是由于volatile对引用变量m的影响,而不是它对对象成员的任何影响。 - OrangeDog
@OrangeDog:嘿,那是经典的多线程陷阱之一。我已经看到过这个错误很多次了。除非在两个线程之间有“同步点”,否则跨越两个线程的“发生前关系”是 保证的。volatile提供了这种保证。在发布更多错误信息之前,请先阅读相关资料。 - jtahlborn
显示剩余20条评论

0
使用volatile而不是完全synchronized的值本质上是一种优化。这种优化来自于对比synchronized访问提供的弱保证。过早优化是万恶之源;在这种情况下,恶魔可能很难跟踪,因为它会以竞态条件等形式出现。所以如果你需要问,你可能不应该使用它。

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