阅读“实践中的Java并发”,第3.5节中有这一部分:
public Holder holder;
public void initialize() {
holder = new Holder(42);
}
除了创建两个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
构造函数不会被阻塞,那么在构造函数代码仍在另一个线程中运行时,将为该实例创建一个引用。
这可能吗?
答案 0 :(得分:14)
这可能是因为Java具有弱内存模型。它不保证读写顺序。
可以使用以下代表两个线程的两个代码片段来重现此特定问题。
主题1:
someStaticVariable = new Holder(42);
主题2:
someStaticVariable.assertSanity(); // can throw
从表面上看,似乎不可能发生这种情况。为了理解为什么会发生这种情况,您必须超越Java语法并进入更低级别。如果查看线程1的代码,它基本上可以分解为一系列内存写入和分配:
因为Java具有弱内存模型,所以从线程2的角度来看,代码实际上可以按以下顺序实际执行:
可怕?是的,但它可能发生。
这意味着线程2现在可以在assertSanity
获得值42之前调用n
。n
值可能会在{{{}期间被读取两次1}},一次在操作#3完成之前和之后一次,因此看到两个不同的值并抛出异常。
修改强>
According to Jon Skeet,assertSanity
migh仍然会出现Java 8,除非该字段是最终的。
答案 1 :(得分:10)
使用的Java内存模型,以便在分配给对象中的变量之前,Holder
引用的赋值可能会变得可见。
然而,从Java 5开始生效的最新内存模型使这变得不可能,至少对于最终字段:构造函数中的所有赋值“在发生之前”将对新对象的引用赋值给变量。有关详细信息,请参阅Java Language Specification section 17.4,但这是最相关的代码段:
一个对象被认为是 它完全初始化了 构造函数完成。一个线程 只能看到对象的引用 在那个对象完全之后 初始化保证看到 正确初始化的值 对象的最终字段
所以你的例子仍然可能失败,因为n
是非最终的,但是如果你让n
成为最终的,那么它应该没问题。
当然是:
if (n != n)
肯定会因非最终变量而失败,假设JIT编译器没有优化它 - 如果操作是:
然后两个提取之间的值可能会发生变化。
答案 2 :(得分:1)
好吧,在书中它说明了第一个代码块:
这里的问题不是持有人 类本身,但持有人是 没有正确发表。然而, 持有人可以免于不当 通过声明n字段来发布 最后,这将使持有人 不可改变的;见第3.5.2节
对于第二个代码块:
因为未使用同步 使持有人对其他人可见 线程,我们说持有人不是 正确发表。有两件事可以做 错误发布不正确 对象。其他线程可以看到一个 持有人字段的陈旧价值,以及 因此看到空引用或其他 即使值有较旧值也是如此 被置于持有人。但更糟糕的是, 其他线程可以看到更新 持有人参考的价值,但是 状态的陈旧值 保持器。[16]使事情变得更少 可预测的,一个线程可能会看到陈旧 值第一次读取字段时的值 然后是一个更新的价值 下一次,这就是assertSanity的原因 可以抛出AssertionError。
我认为JaredPar在他的评论中已经明确表达了这一点。
(注意:不在这里寻找投票 - 答案允许提供比评论更详细的信息。)
答案 3 :(得分:0)
基本问题是如果没有正确的同步,对内存的写入可能会在不同的线程中显示。经典的例子:
a = 1;
b = 2;
如果你在一个线程上执行该操作,第二个线程可能会在a设置为1之前将b设置为2.此外,在第二个线程看到其中一个变量之间可能存在无限的时间量更新,其他变量正在更新。
答案 4 :(得分:-1)
if(n != n)
是原子的(我认为这是合理的,但我不确定),然后断言异常永远不会抛出。
答案 5 :(得分:-1)
此示例位于“对包含最终字段的对象的引用未逃脱构造函数”
使用new运算符实例化新的Holder对象时,
请参阅以上内容:http://www.artima.com/designtechniques/initializationP.html
假设:第一个线程从上午10点开始,它通过调用新的Holer(42)来调用Holder对象的instatied, 1)Java虚拟机首先将在堆上分配(至少)足够的空间来保存在Holder及其超类中声明的所有实例变量。 - 这将是10点01分 2)其次,虚拟机将所有实例变量初始化为其默认初始值 - 它将在10:02时间开始 3)第三,虚拟机将调用Holder类中的方法.--它将在10点04分开始
现在Thread2开始于 - > 10:02:01时间,它将调用assertSanity()10:03,到那时n初始化为默认值零,第二个线程读取过时数据。
//不安全的出版物 公众持有人;
如果您公开最终持有人将解决此问题
或
private int n;如果你做私人决赛int n;将解决这个问题。
请参阅:http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html在新JMM下最终字段如何工作的部分下?
答案 6 :(得分:-1)
我也对这个例子感到非常困惑。 我找到了一个彻底解释该主题的网站,读者可能会觉得有用: 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对象 实例