【问题标题】:Volatile semantic with respect to other fields相对于其他领域的易变语义
【发布时间】:2009-08-29 11:09:17
【问题描述】:

假设我有以下代码

private volatile Service service;

public void setService(Service service) {
  this.service = service;
}

public void doWork() {
  service.doWork();
}

标记为 volatile 的已修改字段,其值不依赖于先前的状态。所以,这是正确的多线程代码(暂时不要为 Service 的实现而烦恼)。

据我所知,从内存可见性的角度来看,读取 volatile 变量就像进入锁一样。这是因为普通变量的读取不能与读取 volatile 变量重新排序。

这是否意味着下面的代码是正确的?

private volatile boolean serviceReady = false;
private Service service;

public void setService(Service service) {
  this.service = service;
  this.serviceReady = true;
}

public void doWork() {
  if ( serviceReady ) {
    service.doWork();
  }
}

【问题讨论】:

    标签: java concurrency volatile


    【解决方案1】:

    是的,从 Java 1.5 开始,这段代码是“正确的”。

    原子性不是问题,不管有没有 volatile(写入对象引用是原子的),所以你可以用任何一种方式从关注列表中划掉它——唯一悬而未决的问题是变化的可见性和“正确性”排序。

    对 volatile 变量的任何写入都会与对同一变量的任何后续读取建立“先发生”关系(新 Java 内存模型的关键概念,如 JSR-133 中所述)。这意味着读取线程必须对写入线程可见的所有内容都具有可见性:也就是说,它必须在写入时看到所有具有至少“当前”值的变量。

    我们可以通过查看section 17.4.5 of the Java Language Specification来详细解释这一点,具体如下几个关键点:

    1. “如果 x 和 y 是同一个线程的动作,并且 x 在程序顺序中位于 y 之前,则 hb(x, y)”(即同一个线程上的动作不能以不一致的方式重新排序)节目顺序)
    2. “对易失性字段 (§8.3.1.4) 的写入发生在对该字段的每次后续读取之前。” (这是澄清文本,说明 volatile 字段的先写后读是同步点)
    3. "如果 hb(x, y) 和 hb(y, z),则 hb(x, z)"(happens-before 的传递性)

    所以在你的例子中:

    • 由于规则 1,写入“service”(a) 发生在写入“serviceReady”(b) 之前
    • 由于规则 2,写入 'serviceReady' (b) 发生在读取相同 (c) 之前
    • 因此,(a) 发生在 (c) 之前(第三条规则)

    意味着您可以保证“服务”设置正确,在这种情况下,一旦 serviceReady 为真。

    您可以使用几乎完全相同相同的示例看到一些不错的文章,其中一个位于 IBM DeveloperWorks —— 请参阅“Volatile 的新保证”:

    在写入 V 时对 A 可见的值现在保证对 B 可见。

    还有一个the JSR-133 FAQ,由该 JSR 的作者编写:

    因此,如果读者看到 v 的值为真,也可以保证看到之前发生的对 42 的写入。在旧的内存模型下,情况并非如此。如果 v 不是 volatile,那么编译器可以重新排序 writer 中的写入,而 reader 对 x 的读取可能会看到 0。

    【讨论】:

    • 这就是为什么我的目光总是与 JLS 擦肩而过:在我看来,对于非易失性,跨线程的发生之前的关系并不能保证。感谢您的 JSR-133 参考,我的答案已经消失了。
    • 如果 serviceReady 不是易失性的,那么由于规则 1,似乎“写入 'service' (a) 发生在写入 'serviceReady' (b)”的结论。但实际上,它不是。所以我觉得这3条规则中缺少一些东西。你怎么看?
    【解决方案2】:

    AFAIK 这是正确的代码。

    @CPerkins:使唯一的 setService 方法同步是行不通的,因为您还必须在读取时进行同步。

    但是,在这种情况下,一个变量就足够了。为什么需要额外的布尔字段。 例如

    private volatile Service service;
    
    public void setService(Service service) {
      this.service = service;
    }
    
    public void doWork() {
      if ( service != null ) {
        service.doWork();
      }
    }
    

    鉴于没有人将 setService 调用到null。所以你可能应该做一个空检查:

    private volatile Service service;
    
    public void setService(Service service) {
      if (service == null) throw NullPointerException();
      this.service = service;
    }
    
    public void doWork() {
      if ( service != null ) {
        service.doWork();
      }
    }
    

    【讨论】:

    • 是的,没错,库兹。我写的很匆忙,主要是回答关于 volatile 的问题……但我仍然问:为什么不简单地同步?
    • 是的,我同意你关于设计的看法。但是代码是发明的,不是真实的。问题是关于可变语义的。
    • @dotsid:如果您没有实际需要额外 volatile 字段的示例,则很难看出您想要实现什么
    • CPerkins:使用 volatile 而非同步的原因之一:它通常更快
    【解决方案3】:

    您对volatile 的影响是正确的,所以这应该是正确的,但我对您的设计感到困惑。我不明白你为什么不同步 setService - 它可能不会经常被调用。如果它被多次调用,“if (serviceReady)”部分没有实际意义,因为它仍然是正确的,但没关系,因为替换是原子的,如果我理解正确的话。

    我认为service.doWork() 是线程安全的,是吗?

    【讨论】:

    • 这不是真正的代码。在实际系统中,我当然不会那样做 :) 我发明这段代码只是作为教学示例。
    【解决方案4】:

    理论上,它永远不会起作用。您想确保两个变量的内存一致性,并且希望在第一个变量上依赖 volatile read。 volatile read only 保证读取线程看到变量的最新值。所以它绝对没有进入锁定(同步)部分那么强大。

    实际上,它可能会起作用,具体取决于您使用的 JVM 对 volatile 的实现。如果通过刷新所有 CPU 缓存来实现易失性读取,它应该可以工作。但我准备打赌它不会发生。 Can I force cache coherency on a multicore x86 CPU? 是关于这个主题的好书。

    我想说简单地为这两个变量设置一个公共锁(java.util.concurrent.Lock 或 synchronized)并用它来完成。


    Java Language Specification, Third Edition,对 volatile 有这样的看法:

    8.3.1.4 可变字段

    可以将字段声明为 volatile,在这种情况下,Java 内存模型(第 17 节)可确保所有线程看到变量的一致值。

    17.4.4 同步顺序

    • 对 volatile 变量(第 8.3.1.4 节)v 的写入与任何线程对 v 的所有后续读取同步(其中后续根据同步顺序定义)。
    • 对 volatile 字段(第 8.3.1.4 节)的写入发生在对该字段的每次后续读取之前。

    它没有说明其他变量的可见性影响。

    【讨论】:

    • 对不起,这个评论根本不是真的,从 Java 1.5 开始。 “易失性只读保证读取线程看到变量的最新值”是不正确的。从 1.5 开始,在该变量之后的任何内容都“发生在之前”的 volatile 写入,这意味着写入线程可见的 所有内容 现在对读取线程可见。参见例如ibm.com/developerworks/library/j-jtp03304 在“New Guarantees for Volatile”下——清单 1 基本上就是这个例子。 '在写入 V 时对 A 可见的值现在保证对 B 可见。'
    • @Cowan:我检查了 JLS 第 3 版,但没有发现任何相关内容。您能否为您的声明提供一个权威来源?
    • 罗伯特,这是happens-before关系传递性的结果。当 serviceReady 为 true 时,它​​必须已被 setService() 修改。这在易失性写入和读取之间建立了之前发生的事情。因此,写前的动作和读后的动作也处于“发生前”的关系。换句话说,一旦你通过某种同步方式建立了happens-before关系,就保证所有的变化都是可见的。 (17.4.5 发生在订单之前)
    • 对于另一篇文章(同样,使用与提供的代码基本相同的示例)请参阅cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#volatile——作者 Jeremy Manson 和 Brian Goetz 是 JSR for new Memory Model 的作者,所以我认为我们可以认为它们相当权威。
    • 为了扩展 Peter 的解释,取 17.4.5 中的 3 点: (1) “如果 x 和 y 是同一个线程的动作,并且 x 在程序顺序中位于 y 之前,则 hb(x , y)”, (2) “对易失性字段 (§8.3.1.4) 的写入发生在对该字段的每次后续读取之前。”和 (3) “如果 hb(x, y) 和 hb(y, z), 然后是 hb(x, z)”。所以写入“service”发生在写入“serviceReady”之前(第一条规则),写入serviceReady发生在读取相同(第二条规则)之前,我们可以推断写入服务然后发生之前serviceReady 的读取(第三条规则)。
    猜你喜欢
    • 2013-06-29
    • 2014-08-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-06-06
    • 1970-01-01
    • 1970-01-01
    • 2021-05-29
    相关资源
    最近更新 更多