【问题标题】:Java: Caching of non-volatile variables by different threadsJava:不同线程缓存非易失性变量
【发布时间】:2016-07-18 17:34:27
【问题描述】:

情况如下:

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

问题:我是否必须让这个类的所有变量都为volatile?

关注点:

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

注意:我知道构建器模式,但由于其他几个原因,我不能在那里应用它:(

已编辑: 由于我觉得 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。据我了解,axtavt 总是会打印 5。

你的意见是什么?

-- 问候, 德米特里

【问题讨论】:

  • 我无法确定哪个答案是正确的。想听听其他参与者的意见。
  • 这两个答案是兼容的。 axtavt 不假设线程何时启动,因此它解决了一般情况。 Mathias 解决了如果您确实知道您的对象已构建并且在线程启动之前设置了它的值会发生什么(用 axtavt 的话说,这可以被视为 安全发布 的一个特例,这要归功于线程开始强制执行的可见性障碍)。它们相互补充(如果您的线程在设置值之前启动,请注意您将volatiles 放在哪里)。

标签: java concurrency volatile


【解决方案1】:

在这种情况下,您需要在使您的对象可用于其他线程时使用安全发布习惯用法,即(来自Java Concurrency in Practice):

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

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

但是,如果你不使用它,声明字段volatile(理论上)将无济于事,因为volatile 产生的内存障碍是一方面:volatile write 可以在它之后使用非易失性操作重新排序。

所以,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

从理论的角度来看,在第一个样本中存在一个可传递的happens-before关系

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

在第二个例子中f.x = 42foo.x 的读取没有通过happens-before 关系链接,因此它们可以按任意顺序执行。

【讨论】:

  • 对不起,我没有得到你的说法“声明字段 volatile(理论上)不会有帮助,因为 volatile 产生的内存障碍是一方面的。”你能解释一下“单边”是什么意思吗?谢谢。
  • 我认为很明显,发布者没有做这些事情,并且想知道他的代码是否安全。 Safe publication 似乎是从 JLS 第 17 章中的 Java 并发模型派生的实践。
  • @axatavt:是的。谢谢你。现在我懂了。在我的问题中,我指的是第一种情况。
  • @axatavt: 抱歉:) 事实上,最初并没有得到它:) 如果我们在您的第一个示例中交换太多行会发生什么? foo = new Foo(); foo.x= 42;
  • @Dime:是的。我的观点是 1) 安全发布您不需要 volatile 字段 2) 没有安全发布 volatile 字段将无济于事
【解决方案2】:

在读取字段的线程上调用start 方法之前,您无需声明字段的 volatile 值已设置。

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

JLS 的相关规则是:

  • 线程中的每个动作都发生在该线程中的每个动作之前
  • 在线程上启动的调用发生在已启动线程中的任何操作之前。

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

【讨论】:

  • 不正确。在这种情况下,声明字段 volatile 没有任何意义,也无济于事,因为对象的发布可以通过对其字段的 volatile 写入来重新排序。
  • 我假设单个线程负责写入字段,并且该对象随后由同一线程发布。我认为从问题中可以清楚地看到这一点。
  • @Dime:因为非易失性写入(对象的不安全发布)可以与之前的易失性写入(字段的初始化)重新排序,请参阅我的回答。
  • @axtavt:JLS 中关于线程启动的规则有效地为我们提供了另一个安全的发布习惯:在启动另一个线程读取对象之前先写入对象。不过,这不是一个非常普遍的习惯用法,因为我们很少创建线程。
  • 我的声明很简单,如果没有 volatile 修饰符,我无法识别写入和读取之间的发生前关系。如果可以,那么我的说法是错误的。
【解决方案3】:

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

我认为问题的答案是:是的,在给出的示例中,没有必要使对象的成员变为 volatile。但是,这种实现相当脆弱,因为这种保证取决于完成事情的确切顺序以及容器的线程安全性。构建器模式会是更好的选择。

为什么保证:

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

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

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

这篇文章的主题也相当不错: http://tutorials.jenkov.com/java-concurrency/volatile.html

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-21
    • 2020-09-11
    • 1970-01-01
    相关资源
    最近更新 更多