15

假设我有以下代码

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();
  }
}
4

4 回答 4

20

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

原子性不是问题,无论有无易失性(写入对象引用是原子的),因此您可以将其从关注列表中划掉——唯一悬而未决的问题是更改的可见性和排序的“正确性”。

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

我们可以通过查看Java Language Specification 的第 17.4.5 节来详细解释这一点,特别是以下关键点:

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

所以在你的例子中:

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

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

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

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

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

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

于 2009-08-31T08:12:11.093 回答
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();
  }
}
于 2009-08-29T14:34:47.240 回答
1

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

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

于 2009-08-29T12:48:28.147 回答
0

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

在实践中,它可能会起作用,具体取决于您使用的 JVM 对 volatile 的实现。如果通过刷新所有 CPU 缓存来实现易失性读取,它应该可以工作。但我准备打赌它不会发生。我可以在多核 x86 CPU 上强制缓存一致性吗?是关于这个主题的好读物。

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


Java 语言规范,第三版,对 volatile 有这样的说法:

8.3.1.4 易失性字段

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

17.4.4 同步顺序

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

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

于 2009-08-29T19:07:06.413 回答