问题 Java:不同线程缓存非易失性变量


情况如下:

  1. 我有一个有很多二传手和吸气剂的物体。
  2. 此对象的实例是在一个特定线程中创建的,其中设置了所有值。最初我使用new语句创建一个“空”对象,然后我才根据一些复杂的遗留逻辑调用一些setter方法。
  3. 只有这个对象才可用于仅使用getter的所有其他线程。

问题是: 我是否必须使此类的所有变量都不变?

关注:

  • 创建对象的新实例并设置其所有值 在时间上分开。
  • 但所有其他线程都不知道这一点 新实例,直到设置所有值。所以其他线程不应该 有一个未完全初始化对象的缓存。不是吗?

注意: 我知道生成器模式,但由于其他几个原因我无法应用它:(

编辑: 由于我觉得Mathias和axtavt的两个答案不匹配,我想补充一个例子:

假设我们有一个 foo 类:

class Foo {   
    public int x=0;   
}

并且两个线程正在使用它,如上所述:

 // Thread 1  init the value:   
 Foo f = new Foo();     
 f.x = 5;     
 values.add(f); // Publication via thread-safe collection like Vector or Collections.synchronizedList(new ArrayList(...)) or ConcurrentHashMap?. 

// Thread 2
if (values.size()>0){        
   System.out.println(values.get(0).x); // always 5 ?
}

据我所知,Mathias可以根据JLS在某些JVM上打印出0。据我所知,它将始终打印5。

你有什么意见?

- 问候, 德米特里


7290
2017-07-12 08:32


起源

我无法确定哪个答案是正确的。想听听其他参与者的意见。 - Dime
这两个答案是兼容的。 axtavt's没有假设线程何时启动,因此它解决了一般情况。 Mathias的问题是,如果你确实知道你的对象是在构造之前被构造并且在线程开始之前设置了它的值,那么会发生什么事情(用axtavt的话来说,这可以看作是一个特例 安全出版物,这要归功于线程启动所强制执行的可见性障碍。它们相互补充(如果您的线程在设置值之前启动,那么您可以放置​​它们 volatileS)。 - GPI


答案:


在这种情况下,您需要使用 安全出版习语 当你的对象可用于其他线程时,即(来自 Java并发实践):

  • 从静态初始化程序初始化对象引用;
  • 将对它的引用存储到volatile字段或AtomicReference中;
  • 将对它的引用存储到正确构造的对象的最终字段中;要么
  • 将对它的引用存储到由锁正确保护的字段中。

如果使用安全发布,则无需声明字段 volatile

但是,如果你不使用它, 声明字段 volatile (理论上)无济于事,因为内存障碍所致 volatile 是单方面的:易失性写入可以在其后使用非易失性动作重新排序。

所以, volatile 在以下情况下确保正确性:

class Foo {
    public int x;
}
volatile Foo foo;

// Thread 1
Foo f = new Foo();
f.x = 42;
foo = f; // Safe publication via volatile reference

// Thread 2
if (foo != null)
     System.out.println(foo.x); // Guaranteed to see 42

但在这种情况下不起作用:

class Foo {
    public volatile int x;
}
Foo foo;

// Thread 1
Foo f = new Foo();
// Volatile doesn't prevent reordering of the following actions!!!
f.x = 42;
foo = f;

// Thread 2
if (foo != null)
     System.out.println(foo.x); // NOT guaranteed to see 42, 
                                // since f.x = 42 can happen after foo = f

从理论的角度来看,在第一个样本中存在一个传递发生在之前的关系

f.x = 42 happens before foo = f happens before read of foo.x 

在第二个例子中 f.x = 42 并阅读 foo.x 没有发生在之前的关系,因此它们可以按任何顺序执行。


8
2017-07-12 08:43



对不起,我没有得到你的声明“声明字段不稳定(理论上)无济于事,因为不稳定的内存障碍是单方面的。”你能解释一下“单方面”是什么意思吗?谢谢。 - Dime
我认为很明显,海报没有做这些事情,并想知道他的代码是否安全。 Safe publication似乎是JLS第17章中Java并发模型的派生实践。 - Mathias Schwarz
@Dime:我的例子更加清晰。 - axtavt
@axatavt:是的。谢谢。现在我懂了。在我的问题中,我的意思是第一个案例。 - Dime
@Dime:是的。我的观点是1)安全发布你不需要 volatile 领域2)没有安全的出版物 volatile 字段无济于事 - axtavt


你不需要声明你的字段volatile之前设置它的值 start 在读取字段的线程上调用方法。

原因是在这种情况下,设置处于先前发生的关系(如Java语言规范中所定义)与另一个线程中的读取。

JLS的相关规则是:

  • 线程中的每个动作都发生在该线程中的每个动作之前,该动作在程序的顺序中稍后出现
  • 在启动线程中的任何操作之前发生对线程启动的调用。

但是,如果在设置字段之前启动其他线程,则必须声明字段volatile。 JLS不允许您假设线程在第一次读取它之前不会缓存该值,即使在特定版本的JVM上可能就是这种情况。


4
2017-07-12 08:40



也可以看看: java.sun.com/docs/books/jls/second_edition/html/memory.doc.html - Mathias Schwarz
不正确。声明字段 volatile 在这种情况下毫无意义,也无济于事,因为对象的发布可以通过其字段的易失性写入来重新排序。 - axtavt
@Dime:因为非易失性写入(对象的不安全发布)可以使用先前的volatile写入(字段初始化)重新排序,请参阅我的回答。 - axtavt
@axtavt:关于JLS中的线程启动的规则有效地为我们提供了另一个安全的发布习惯用法:在开始另一个线程从它读取之前写入对象。然而,这不是一个非常普遍的习惯用法,因为我们很少创建线程。 - Tom Anderson
我的陈述只是没有了 volatile 修饰符我无法识别写入和读取之间发生的关系。如果可以,那么我的陈述是错误的。 - Mathias Schwarz


为了充分了解正在发生的事情,我一直在阅读有关Java内存模型(JMM)的内容。可以在Java Conurrency in Practice中找到对JMM的有用介绍。

我认为问题的答案是:是的,在给出使对象volatile的成员不是必需的示例中。但是,这种实现相当脆弱,因为这种保证取决于完成事情的确切ORDER以及Container的Thread-Safety。构建器模式将是更好的选择。

为什么保证:

  1. 线程1在将值放入线程安全容器之前完成所有赋值。
  2. 线程安全容器的add方法必须使用一些同步构造,如volatile读/写,锁或synchronized()。这保证了两件事:

    1. 在同步之前的线程1中的指令实际上将在之前执行。也就是说,不允许JVM使用同步指令对指令进行重新排序以进行优化。这称为发生前保证。
    2. 在线程1中的同步之前发生的所有写入之后将对所有其他线程可见。
  3. 发布后永远不会修改对象。

但是,如果容器不是线程安全的,或者事物的顺序被不知道该模式的人改变或者在发布后意外地更改了对象,则不再有任何保证。因此,遵循Builder模式,可以通过谷歌AutoValue或Freebuilder生成更安全。

这篇关于这个主题的文章也很不错: http://tutorials.jenkov.com/java-concurrency/volatile.html


0
2017-07-18 09:27