与其他领域相关的易变语义

时间:2009-08-29 11:09:18

标签: java concurrency volatile

假设我有以下代码

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 个答案:

答案 0 :(得分:18)

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

原子性不是一个问题,有或没有volatile(对象引用的写入是原子的),所以你可以通过任何方式跨越关注列表 - 唯一的开放问题是变化的可见性和'正确性'的订购。

对volatile变量的任何写入都会设置'before-before'关系(新的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节) - 在每次后续读取该字段之前发生。” (这是澄清文本,解释写入然后读取易失性字段是同步点)
  3. “如果hb(x,y)和hb(y,z),那么hb(x,z)”(发生之前的传递性)
  4. 所以在你的例子中:

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

    意味着保证'service'设置正确,在这种情况下,一旦serviceReady为true。

    您可以使用几乎完全相同的示例看到一些好的文章,一个在IBM DeveloperWorks - 请参阅“新的保证挥发”:

      

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

    the JSR-133 FAQ的一个,由JSR的作者撰写:

      

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

答案 1 :(得分:2)

AFAIK这是正确的代码。

@CPerkins:只使同步的setService方法不起作用,因为你还需要同步读取。

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

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

答案 2 :(得分:1)

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

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

答案 3 :(得分:0)

从理论上讲,它永远不会奏效。您希望确保两个变量的内存一致性,并且您希望在第一个变量上依赖volatile read。 volatile只读保证读取线程看到变量的最新值。所以它肯定不如进入锁定(同步)部分那么强大。

实际上,它可能会起作用,具体取决于您使用的JVM对volatile的实现。如果通过刷新所有CPU缓存实现volatile读取,它应该工作。但我已经准备好打赌它不会发生。 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 Fields

     

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

  

17.4.4同步顺序

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

它没有说明其他变量的可见性效果。