不是线程安全的对象发布

时间:2009-10-25 17:11:36

标签: java concurrency thread-safety

阅读“实践中的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构造函数不会被阻塞,那么在构造函数代码仍在另一个线程中运行时,将为该实例创建一个引用。

这可能吗?

7 个答案:

答案 0 :(得分:14)

这可能是因为Java具有弱内存模型。它不保证读写顺序。

可以使用以下代表两个线程的两个代码片段来重现此特定问题。

主题1:

someStaticVariable = new Holder(42);

主题2:

someStaticVariable.assertSanity(); // can throw

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

  1. Alloc memory to pointer1
  2. 在偏移0
  3. 处将42写入指针1
  4. 将指针1写入someStaticVariable
  5. 因为Java具有弱内存模型,所以从线程2的角度来看,代码实际上可以按以下顺序实际执行:

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

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

      这意味着线程2现在可以在assertSanity获得值42之前调用nn值可能会在{{{}期间被读取两次1}},一次在操作#3完成之前和之后一次,因此看到两个不同的值并抛出异常。

      修改

      According to Jon SkeetassertSanity migh仍然会出现Java 8,除非该字段是最终的。

答案 1 :(得分:10)

使用的Java内存模型,以便在分配给对象中的变量之前,Holder引用的赋值可能会变得可见。

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

  

一个对象被认为是   它完全初始化了   构造函数完成。一个线程   只能看到对象的引用   在那个对象完全之后   初始化保证看到   正确初始化的值   对象的最终字段

所以你的例子仍然可能失败,因为n是非最终的,但是如果你让n成为最终的,那么它应该没问题。

当然是:

if (n != n)

肯定会因非最终变量而失败,假设JIT编译器没有优化它 - 如果操作是:

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

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

答案 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对象时,

  1. Java虚拟机首先将在堆上分配(至少)足够的空间来保存在Holder及其超类中声明的所有实例变量。
  2. 其次,虚拟机会将所有实例变量初始化为其默认初始值。 3.c第三,虚拟机将调用Holder类中的方法。
  3. 请参阅以上内容: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对象   实例