2012-04-20 79 views
10

我想确保根据Java内存模型正确理解“有效不可变对象”行为。有效不可变对象

比方说,我们有我们要发布为有效不可改变一个可变类:

class Outworld { 
    // This MAY be accessed by multiple threads 
    public static volatile MutableLong published; 
} 

// This class is mutable 
class MutableLong { 
    private long value; 

    public MutableLong(long value) { 
    this.value = value; 
    } 

    public void increment() { 
    value++; 
    } 

    public long get() { 
    return value; 
    } 
} 

我们做到以下几点:

// Create a mutable object and modify it 
MutableLong val = new MutableLong(1); 
val.increment(); 
val.increment(); 
// No more modifications 
// UPDATED: Let's say for this example we are completely sure 
//   that no one will ever call increment() since now 

// Publish it safely and consider Effectively Immutable 
Outworld.published = val; 

的问题是: Java内存模型是否保证所有线程都必须有Outworld.published.get() == 3

根据Java Concurrency In Practice这应该是真的,但请纠正我,如果我错了。

3.5.3。安全出版习惯

要安全地发布对象,对象的引用和对象的状态必须同时对其他线程可见。 正确构造的对象可以通过以下方式安全地发布:
- 从静态初始化程序初始化对象引用;
- 将引用存储到易失性字段或AtomicReference中;
- 将引用存储到正确构造的对象的最终字段中;或
- 将对其的引用存储到由锁正确保护的字段中。

3.5.4。有效不可变的对象

安全地发布有效的不可变对象可以安全地使用任何线程,而无需额外的同步。

+0

请显示建立对象状态的[* static initializer *](http://docs.oracle.com/javase/tutorial/java/javaOO/initial.html),该对象的状态是可见的。 – trashgod 2012-04-20 23:07:09

回答

5

是的。在读取之前,MutableLong上的写入操作之后是happens-before关系(在易失性部分)。

(这可能是一个线程读取Outworld.published并将其传递到另一个线程不安全的。从理论上说,可以看到以前的状态。在实践中,我没有看到它的发生。)

+1

您的评论(第2段)似乎与您的答案(第1段)不符,而且与易失性阅读链接之前发生的情况不符。你能详细说明吗? – assylias 2012-04-21 09:07:26

+2

假设线程T1安全地发布到T2。从T1到T2有*发生之前*。但是,如果相同的对象从T2到T3不安全地发布,那么从T2到T3没有* happen-before *,因此也没有*在从T1到T3之前发生*之前发生。 – 2012-04-21 12:32:44

2

的问题是:Java内存模型是否保证所有线程 必须具有Outworld.published.get()== 3?

简短的回答是no。因为其他线程在读取之前可能会访问Outworld.published

在执行了Outworld.published = val;之后,在没有对val进行其他修改的情况下 - 是的 - 它始终是3

但是,如果任何线程执行val.increment那么其值就可能不同于其他线程。

+0

是的,我说的是我们可能认为这个对象是有效不可变的情况;即我们完全确定没有其他线程会对其调用“increment()”。 我更新了一个更具体的例子。 – 2012-04-22 09:20:58

4

有几个条件必须满足Java内存模型,以保证Outworld.published.get() == 3

  • 的你贴的代码创建并递增MutableLong片断,然后设置Outworld.published场,必须发生知名度之间的步骤。实现这一点的一种方式是让所有代码在单个线程中运行 - 保证“as-if-serial semantics”。我认为这就是你的意图,但认为值得指出。
  • Outworld.published的读数必须有发生之后的语义。一个例子可能是执行相同的线程Outworld.published = val;然后启动其他可以读取值的线程。这将保证“好像串行”语义,从而防止在读取之前重新排序读取。

如果您能够提供这些保证,那么JMM将保证所有线程都能看到Outworld.published.get() == 3


但是,如果您对这方面的一般计划设计建议感兴趣,请继续阅读。

为保证没有其他线程看到Outworld.published.get()的不同值,您(开发人员)必须保证您的程序不会以任何方式修改该值。通过随后执行Outworld.published = differentVal;Outworld.published.increment();。虽然这是可以保证的,但如果设计代码以避免可变对象,并使用静态非最终字段作为多线程的全局访问点,可以更容易:

  • 改为发布MutableLong,将相关值复制到不同类的新实例中,其状态不能修改。例如:引入类别ImmutableLong,其将value分配给final施工领域,并且没有increment()方法。
  • 而不是多个线程访问静态非最终字段,将该对象作为参数传递给您的实现。这将防止一个流氓线程重新分配值并干扰其他线程的可能性,并且比静态字段重新分配更容易推理。 (不可否认,如果你正在处理遗留代码,说起来容易做起来难)。
+0

我完全同意你的意见。同步太容易出错。我会不惜一切代价避免它。共享可变性不是设计算法或程序的安全方式。我认为每个程序员都应该投入一些时间来研究函数式编程概念,并将它们应用到oo世界。这绝对是值得的。 – bennidi 2012-04-22 09:33:26