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

Dim*_*ime 11 java concurrency volatile

情况如下:

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

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

关注:

  • 创建对象的新实例并设置其所有值在时间上是分开的.
  • 但是在设置所有值之前,所有其他线程都不知道这个新实例.因此其他线程不应具有未完全初始化对象的缓存.不是吗?

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

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

假设我们有一个foo班级:

class Foo {   
    public int x=0;   
}
Run Code Online (Sandbox Code Playgroud)

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

 // 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 ?
}
Run Code Online (Sandbox Code Playgroud)

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

你有什么意见?

- 问候,德米特里

axt*_*avt 8

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

  • 从静态初始化程序初始化对象引用;
  • 将对它的引用存储到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
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)

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

f.x = 42 happens before foo = f happens before read of foo.x 
Run Code Online (Sandbox Code Playgroud)

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


Mat*_*arz 4

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

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

JLS的相关规则是:

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

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