volatile,final和synchronized之间安全发布的差异

时间:2014-05-30 17:17:07

标签: java concurrency

给定一个带有变量x的A类。变量x在类构造函数中设置:

A() {
x = 77;
}

我们想要将x发布到其他线程。考虑以下3个变量x线程安全(?)发布的案例:

1)x 最终

2)x 易失性

3)x在同步块

中设置
synchronized(someLock) {
A a  = new A();
a.x = 77;
}

Thread2只打印x:

 System.out.println(a.x);

问题是:是否可以观察到Thread2打印的'0'?或者JMM保证在所有3种情况下都会打印'77'或者会抛出NPE?

1 个答案:

答案 0 :(得分:3)

我的回答来自best page on the internet,特别是第17章涉及内存可见性和并发性。

我也假设您没有引用泄漏(即,在对象构造函数完成之前,您没有对该对象的引用)。

  • 最终字段。我将在上面引用上面的章节,第17.5章:

      

    当构造函数完成时,对象被认为是完全初始化的。在该对象完全初始化之后只能看到对象引用的线程可以保证看到该对象的最终字段的正确初始化值。

  • 挥发性。我再次引用JLS:

  

对易失性变量v(第8.3.1.4节)的写入与任何线程对v的所有后续读取同步(其中“后续”根据同步顺序定义)。

因此,假设您的Thread在完成构造函数后可以访问该对象,它将看到正确的值。注意,这意味着你可能也需要制作一个易变的。

  • x在同步块中。这是一个棘手的问题。它可能会也可能不会 是可见的。实际上,同步只会略微增加 难以解释这一点,所以我会放弃并解释是否 这里会看到一个简单的局部变量。然后添加一个 关于synchronized的条款。

根据定义,如果读取和写入之间存在Happens-before关系,则保证可见。否则,可能发生您看到未初始化的值。什么构成Happens-before关系?同样,JLS第17章指定了这一点,特别是:

  • 单个线程中的操作顺序。
  • 同步,锁定和波动。
  • 对象和线程创建。
  • 一切都是传递性的。
  • 更多内容,请阅读JLS

因此可能有两种情况:

A a = new A();
Thread t = new MyThread(a);
t.start();

MyThread保存A的实例并使用它。在这种情况下,线程是在a之后创建的,并且在创建后调用start()。因此,即使x是非易失性的,也可以保证可见性。但是,无法保证x进一步更改的可见性。

情况2:

这有点难以编码,但是:
Main创建两个Threads并立即启动它们,并且有一个类型为A的非易失性字段 ThreadA创建A并将其写入共享字段 ThreadB循环一段时间,直到填充字段然后打印出x。

在这种情况下,即使写入x和写入共享字段之间存在HB,共享字段的读取和写入之间也不存在HB。因此,无法保证写入x的可见性。

正如所承诺的那样 - 如果在这两种情况中的任何一种情况下在x的写入周围放置一个同步块,它将不会影响结果,因为监视器上没有其他任何锁定。锁定和解锁同一监视器会创建同步操作,从而创建HB关系。