不是线程安全的对象发布

在实践中阅读Java并发,第3.5节:提出索赔

public Holder holder; public void initialize() { holder = new Holder(42); } 

除了创建2个Holder实例的明显线程安全危险之外,该书声称可能会出现一个可能的发布问题,对于Holder类来说更是如此

 public Holder { int n; public Holder(int n) { this.n = n }; public void assertSanity() { if(n != n) throw new AssertionError("This statement is false."); } } 

可以抛出AssertionError!

这怎么可能 ? 我能想到的唯一可以允许这种荒谬的行为是,如果Holder构造函数不会被阻塞,那么当构造函数代码仍在不同的线程中运行时,将为该实例创建一个引用。 这可能吗 ?

这可能的原因是Java具有弱内存模型。 它不保证读/写的顺序。 这个特殊问题可以使用以下代表2个线程的2个代码片段来重现

线程1:

 someStaticVariable = new Holder(42); 

线程2:

 someStaticVariable.assertSanity(); // can throw 

从表面上看,似乎不可能发生这种情况。 为了理解为什么会发生这种情况,您必须超越Java语法并达到更低的水平。 如果查看线程1的代码,它基本上可以分解为一系列内存写入和分配

  1. Alloc Memory to pointer1
  2. 在偏移0处将42写入指针1
  3. 将pointer1写入someStaticVariable

因为Java具有弱内存模型,所以从thread2的角度来看,代码实际上可以按以下顺序实际执行。

  1. Alloc Memory to pointer1
  2. 将pointer1写入someStaticVariable
  3. 在偏移0处将42写入指针1

害怕? 是的,但它可能发生。

这意味着Thread2现在可以在n获得值42之前调用assertSanity。在assertSanity期间可以读取值n两次,在操作#3完成之前读取一次,之后一次读取2个不同的值抛出exception。

编辑

根据Jon的说法,由于内存模型的更新,使用较新版本的Java是不可能的(谢天谢地)。

编辑第二

根据Jon的说法,除非该领域是最终的,否则他永远不会说使用第8版Java是不可能的。

Java存储器模型曾经是这样,在分配给对象内的变量之前,对Holder引用的赋值可能变得可见。

但是,从Java 5开始生效的最新内存模型使这变得不可能,至少对于最终字段:构造函数中的所有赋值“在发生之前”将对新对象的引用赋值给变量。 有关更多详细信息,请参阅Java语言规范部分17.4 ,但这里是最相关的代码段:

当构造函数完成时,对象被认为是完全初始化的。 在该对象完全初始化之后只能看到对象引用的线程可以保证看到该对象的最终字段的正确初始化值

所以你的例子仍然可能失败,因为n是非最终的,但如果你做出最后的结果应该没问题。

当然是:

 if (n != n) 

假设JIT编译器没有对其进行优化,那么非最终变量肯定会失败 – 如果操作是:

  • 获取LHS:n
  • 获取RHS:n
  • 比较LHS和RHS

然后,两个提取之间的值可能会发生变化。

好吧,在书中它说明了第一个代码块:

这里的问题不是Holder类本身,而是Holder没有正确发布。 但是,通过宣布n字段为最终字段,Holder可以免于不正当的出版物,这将使Holder不可改变; 见3.5.2节

对于第二个代码块:

因为同步不用于使Holder对其他线程可见,所以我们说Holder没有正确发布。 不正确发布的对象可能会出现两件事。 其他线程可以看到holder字段的陈旧值,因此即使已将值放入holder中,也会看到null引用或其他旧值。 但更糟糕的是,其他线程可以看到持有人参考的最高价值,但持有人状态的陈旧价值。[16] 为了使事情更容易预测,线程可能会在第一次读取字段时看到陈旧的值,然后在下次读取更新的值,这就是assertSanity可以抛出AssertionError的原因。

我认为JaredPar在他的评论中已经明确表达了这一点。

(注意:不在这里寻找投票 – 答案允许比评论更详细的信息。)

基本问题是没有适当的同步,对内存的写入可能会在不同的线程中显示。 经典的例子:

 a = 1; b = 2; 

如果你在一个线程上执行此操作,第二个线程可能会在a设置为1之前将b设置为2.此外,在第二个线程看到其中一个变量得到更新和第二个线程之间可能存在无限长的时间。其他变量正在更新。

从理智的角度来看这个,如果你假设这个陈述

if(n != n)

是primefaces的(我认为这是合理的,但我不确定),然后断言exception永远不会抛出。

此示例位于“对包含最终字段的对象的引用未逃脱构造函数”

使用new运算符实例化新的Holder对象时,

  1. Java虚拟机首先将在堆上分配(至少)足够的空间来保存在Holder及其超类中声明的所有实例变量。
  2. 其次,虚拟机会将所有实例变量初始化为其默认初始值。 3.c第三,虚拟机将调用Holder类中的方法。

请参考以上内容: http : //www.artima.com/designtechniques/initializationP.html

假设:第一个线程从上午10点开始,它通过调用新的Holer(42)来调用Holder对象,1)Java虚拟机首先将在堆上分配(至少)足够的空间来保存所有实例在Holder及其超类中声明的变量。 – 它将是10:01时间2)其次,虚拟机将所有实例变量初始化为其默认初始值 – 它将在10点02分开始时间3)第三,虚拟机将调用Holder类中的方法.–它将在10点04分开始

现在Thread2在 – > 10:02:01开始,它将调用assertSanity()10:03,到那时n初始化为默认值零,第二个线程读取过时数据。

//不安全的出版物公众持有人;

如果您公开最终持有人将解决此问题

要么

私人网络 如果你做私人决赛int n; 将解决这个问题。

请参阅: http : //www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html ,如何在新JMM下的最终字段如何工作?

我也对这个例子感到非常困惑。 我发现了一个彻底解释该主题的网站,读者可能会觉得有用: https : //www.securecoding.cert.org/confluence/display/java/TSM03-J.+Do+not+publish+partially+initialized+objects

编辑:链接中的相关文字说:

JMM允许编译器为新的Helper对象分配内存,并在初始化新的Helper对象之前将该内存的引用分配给helper字段。 换句话说,编译器可以重新排序对辅助实例字段的写入以及初始化Helper对象的写入(即this.n = n),以便前者首先出现。 这可以暴露一个竞赛窗口,在此期间其他线程可以观察到部分初始化的Helper对象实例。