【问题标题】:Using volatile keyword with mutable object对可变对象使用 volatile 关键字
【发布时间】:2011-01-06 11:19:52
【问题描述】:

在 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;
}

【问题讨论】:

  • 为了比较,你可能想看看 AtomicReference 这样做是正确的并且是内置的。
  • 如果您只阅读过Test.m,那么根本就没有happens-before关系建立。

标签: java concurrency volatile mutable


【解决方案1】:

这是对 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 之前的世界中,易失性访问和非易失性访问之间没有明确的关系。因此,该程序的输出可能是:

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

在 jdk1.5+ 世界中,volatile 的语义发生了变化,因此 volatile 访问影响非易失性访问的方式与同步完全相同。因此,在 jdk1.5+ 世界中只有某些输出是可能的:

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

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

进一步阅读:

【讨论】:

  • OK,没有volatile 存在以下关系:hb(w_n, w_volN), hb(r_volN,r_n)。如果添加volatile,则可以添加 hb(w_volN,r_volN)。通过传递性,现在的情况是 hb(w_n,r_n)。传递性规则独立于volatile 的语义。
  • 我不相信这些语义在 1.5 之前并非如此。它们可能是隐含的,但仍必须明确定义。
  • @OrangeDog - 它们是谷歌上的热门歌曲,因此很容易找到。我的观点是表明 volatile 绝对会影响可见性。我试图展示它是如何变化的,以证明它会影响可见性。这些链接还表明 volatile 如何影响可见性。因此,您的答案充其量仍然是误导性的,最坏的情况是错误的。 (实际上我并不担心您对同步一词的使用,因为这是您答案中的一个正确陈述)。
  • 嘿@jtahlborn,未来的跟进。情况 3 是不可能的是否意味着编译器或 jvm 不能重新排序自变量的分配,如果其中一个变量是易失的?如果 t1 被重新排序以首先分配 volatile 变量,那么情况 3 将是可能的。
  • @user2259824 - 正如我已经说过的,基于 jdk 1.5+ 内存语义是不可能的。
【解决方案2】:

在您的示例中,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 这样的类会更好。

【讨论】:

  • 这个答案基本正确。但是请注意,当分配 m 时,内部值 正确可见。只有在后续调用不写 m 的 set() 之后才会出现问题。
  • @jtahlborn,你能提供一个参考吗?我在 JLS 中找不到它。它只是说“对 volatile 字段的写入(第 8.3.1.4 节)发生在对该字段的每次后续读取之前”,但这并不意味着该字段所指对象的实际初始化。
  • 通读第 17 节。“发生在之前”的定义和第 17.4.2 节解释了 volatile 现在在内存语义方面等同于同步。
  • @jtahlborn:内部值是指实例的属性。由于 m 只是对对象的引用,所以说对 set() 的调用实际上是写入 m 是否是错误的。
  • @ShirgillFarhanAnsari - 不,打电话给set() 不要写信给m。您只会阅读 m 参考以便调用 ,set()`。
【解决方案3】:

volatile 仅提供对声明为 Object 的引用的保证。该实例的成员不会同步。

根据Wikipedia,你有:

  • (在所有 Java 版本中)读取和 写入 volatile 变量。这 意味着每个线程访问一个 volatile 字段将读取其当前 继续之前的价值,而不是 (可能)使用缓存值。 (但是,不能保证 volatile 的相对排序 通过常规读取进行读取和写入 并写,意思是 通常不是有用的线程 构造。)
  • (在 Java 5 或更高版本中)易失性读取和写入建立发生前 关系,很像获取和 释放互斥体。

所以基本上你所拥有的是通过声明字段volatile,与之交互创建一个“同步点”,之后任何更改都将在其他线程中可见。但在那之后,使用get()set() 是不同步的。 Java Spec有更详尽的解释。

【讨论】:

  • 这是不正确的。您忽略了 wikipedia 中 Java 5 使 volatile 影响非易失性变量的第二点。
  • @jtahlborn 你是对的,我已经添加了第二点并稍微改写了我自己的解释。
  • 您的解释仍然不正确。它似乎仍然暗示 volatile 不会影响非易失性字段。在第一次分配 m 时,值 的可见性得到保证。有关更多详细信息,请参阅我对 @Gugusee 帖子的评论。
【解决方案4】:

使用volatile 而不是完整的synchronized 值本质上是一种优化。与synchronized 访问相比,为volatile 值提供的保证较弱,因此优化。过早的优化是万恶之源;在这种情况下,邪恶可能很难追踪,因为它会以竞争条件等形式出现。所以如果你需要问,你可能不应该使用它。

【讨论】:

    【解决方案5】:

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

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

    【讨论】:

    • 实际上, volatile 可以 影响对象的成员(但不是 OP 如何使用它)。在关系确实提供可见性保证之前发生。
    • @jtahlborn - 你能演示一下吗?关于第二点;虽然效果是写入对读取可见,但术语“为变量提供可见性”太模糊而没有意义。
    • @OrangeDog:假设您创建了一个 Mutable 的本地实例并使用 3 个不同的值调用 set() 方法,然后将您的本地 Mutable 实例分配给 m。所有后续的 m 读取都将保证看到第一个线程在分配 m 之前设置的“最后一个”值。因此,在分配 m 时,可以保证 value 的可见性。但是,正如在别处正确说明的那样,未来对 set() 的任何调用的影响 分配给 m 后都不能保证是可见的。 (其中“可见”意味着保证被另一个线程看到)。
    • @jtahlborn - 这描述了只有对象变量 (m) 受到影响的情况,而不是它的成员。
    • @OrangeDog:对 m 的写入为价值提供了可见性保证。也许这对你来说似乎是合乎逻辑的,所以你不明白我在说什么。但是,如果您查看 jdk1.5 之前的 volatile 语义,您会发现情况不是。 pre-jdk1.5,价值绝对没有保障。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-02-08
    • 2013-08-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多