线程安全的不可变对象和初始化安全性

时间:2016-09-04 08:32:40

标签: java multithreading

来自 The Java Concurrency In Practice

的说明
  

任何线程都可以安全地使用不可变对象而无需额外的   同步,即使没有使用同步来发布它们

我得到的是线程安全

Jeremy Manson博客的片段 -

class String {
  // Don't do this, either.
  static String lastConstructed;
  private final byte[] bytes;
  public String(byte[] value) {
    bytes = new byte[value.length];
    System.arraycopy(value, 0, bytes, 0, value.length);
    lastConstructed = this;         
  }
}

由于此引用存储在 lastConstructed 中,因此转义构造函数

当然,如果你使lastConstructed volatile(postJDK5 +语义)​​

,它会工作

其中一个问题是 -

  

如果lastConstructed是易变的,那么引用是不安全的   发布到另一个线程,然后String不会是不可变的。   正确?

杰里米的答复是 -

它不是线程安全的,因为它是不可变的,但它是线程安全的,因为lastConstructed是易失性的。

我完全理解它会是线程安全的,因为lastConstructed是易变的,但是我没有得到它不是线程安全的,因为它是不可变的

为什么呢? Concurrency In Practice中的注释表明,任何线程都可以安全地使用不可变对象(即线程安全保证)。如果某些东西是不可变的,那么它就是线程安全的。

请建议。

2 个答案:

答案 0 :(得分:2)

虽然@Peter Lawrey已经解释了设计线程安全和不可变类的细节和问题,并且基于进一步的讨论,我认为问题没有得到直接的答案。所以,我想详细说明一下:

理解短语“不可变对象可以被任何线程安全使用”的主要问题是它本身并不完整。它依赖于这样的前提:任何对象也必须安全地发布为线程安全的。不可变对象也不例外。因此,完整的短语应该是“不可变的,安全发布的对象可以被任何线程安全地使用。”

String示例中的问题是它允许对象的引用从构造函数中转义,从而向其他线程呈现可能无效的对象状态。例如,如果编译器决定优化构造函数并以性能原因重新排列操作:

public String(byte[] value) {
    bytes = new byte[value.length];
    lastConstructed = this;         
    System.arraycopy(value, 0, bytes, 0, value.length);
}

其他一些读取lastConstructed的线程将能够看到未完全构造的字符串。因此,该类不是线程安全的,即使它的实例是不可变的。

这里我们来到这句话的含义“它不是线程安全的,因为它是不可变的”。这意味着单独的不变性并不能保证线程安全,并且该示例证明了这一点。

使lastConstructed volatile会强制编译器发出内存屏障,阻止优化器以上述方式重新排列操作。它将保证在分配lastConstructed = this之前总是发生数组复制。结果,另一个读取lastConstructed的线程永远不会看到一个未构造的字符串。它还可以保证其他线程始终读取lastConstructed的实际值。这就是为什么“它会是线程安全的,因为在这种特殊情况下,lastConstructed是易变的”

答案 1 :(得分:1)

常见的误解是你在Java中有对象字段。您只有引用和原语。这意味着

static String lastConstructed;

字段lastConstructed可变引用。它的可见性不是线程安全的。拥有一个不可变对象不会在对该对象的引用上赋予任何属性。

同样,如果你有final字段,这不会使你的对象不可变。

final Date today = new Date();

today不是一成不变的,因为您对它进行了一次引用final

更精细的是volatile使用你必须要小心你是在读还是写易变的值。即使你这样做

volatile Date now = new Date();

now.setTime(System.currentTimeMillis()); // no thread safe.

这有两个原因,这不是线程安全的。 1)对now的访问是读取而不是写入,其次是在写入之前发生的任何情况。需要的是之后的写屏障。你看到的东西似乎是无稽之谈。

now = now; // this adds a write barrier.

一个相关的误解是,如果您使用线程安全集合,那么您执行的任何系列操作也是线程安全的。它有点像仙尘,你只是洒在它周围,你的许多虫子消失了,但这并不意味着你真的是线程安全的。

简而言之,线程安全就像一连串的依赖关系,如果访问数据的任何方面都不是线程安全的,那都不是。

注意:添加一些线程安全结构可以隐藏线程安全问题,但它不能修复它们,它只是意味着JVM或OS或硬件中的某些更改将在以后破坏您的代码。