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

时间:2011-07-12 08:32:37

标签: java concurrency volatile

情况如下:

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

    关注:

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

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

    编辑: 由于我觉得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。

    您有什么看法?

    - 问候, 德米特里

3 个答案:

答案 0 :(得分:8)

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

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

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

但是,如果您不使用它,声明字段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的读取没有按发生前关系链接,因此它们可以按任何顺序执行。

答案 1 :(得分:4)

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

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

JLS的相关规则是:

  • 线程中的每个操作都发生在该程序命令后面的该线程中的每个操作之前
  • 在启动线程中的任何操作之前发生对线程启动的调用。

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

答案 2 :(得分:0)

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

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

为什么保证:

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

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

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

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