繁体   English   中英

不是线程安全的对象发布

[英]Not thread-safe Object publishing

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

这可能吗?

之所以可行,是因为Java的内存模型较弱。 它不保证读写顺序。

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

线程1:

someStaticVariable = new Holder(42);

线程2:

someStaticVariable.assertSanity(); // can throw

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

  1. 分配内存到指针1
  2. 在偏移量0处将42写入指针1
  3. 将指针1写入someStaticVariable

因为Java的内存模型很弱,所以从线程2的角度来看,代码完全有可能按以下顺序实际执行:

  1. 分配内存到指针1
  2. 将指针1写入someStaticVariable
  3. 在偏移量0处将42写入指针1

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

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

编辑

根据Jon Skeet的说法,除非字段为final,否则Java 8仍然会发生AssertionError

使用 Java的内存模型是这样的,分配给Holder引用可能分配到对象中的变量之前变得可见。

但是,从Java 5开始生效的较新的内存模型使这成为不可能,至少对于最终字段而言:构造函数中的所有分配“在新对象对变量的引用的任何分配之前”发生。 有关更多详细信息,请参见Java语言规范第17.4节 ,但这是最相关的代码段:

对象的构造函数完成后,就认为该对象已完全初始化。 保证只有在对象完全初始化之后才能看到对对象的引用的线程才能保证看到该对象的最终字段的正确初始化值

因此您的示例可能仍然会失败,因为n为非最终值,但是如果您使n最终值也可以。

当然:

if (n != n)

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

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

那么该值可能会在两次提取之间发生变化。

好吧,在书中它指出了第一个代码块:

这里的问题不是Holder类本身,而是Holder没有正确发布。 但是,可以通过将n字段声明为final来使Holder免受不适当发布的影响,这将使Holder不变。 参见第3.5.2节

对于第二个代码块:

因为没有使用同步来使Holder对其他线程可见,所以我们说Holder没有正确发布。 不正确发布的对象可能会导致两件事。 其他线程可能会在Holder字段中看到一个过时的值,因此即使将值放置在Holder中,也会看到一个空引用或其他较旧的值。 但更糟糕的是,其他线程可能会看到Holder引用的最新值,而对于Holder的状态却是陈旧的值。[16] 为了使事情更不可预测,线程在第一次读取字段时可能会看到过时的值,而在下次读取时可能会看到最新的值,这就是assertSanity可以引发AssertionError的原因。

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

(注意:此处不查找选票-答案允许提供比评论更详细的信息。)

基本问题是,如果没有适当的同步,则如何在不同的线程中体现对内存的写入。 经典示例:

a = 1;
b = 2;

如果您在一个线程上执行此操作,则第二个线程可能会看到b设置为2,而a被设置为1。此外,第二个线程看到这些变量之一被更新与第二个线程之间可能会有无穷的时间间隔。其他变量正在更新。

从理智的角度来看这件事,如果您假设

if(n != n)

如果是原子的(我认为这是合理的,但我不确定),那么就永远不会抛出断言异常。

此示例位于“对包含final字段的对象的引用未逸出构造方法”下

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

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

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

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

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

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

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

要么

私人国际 如果您将private final 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实例字段的写入和初始化Helper对象的写入(即this.n = n)进行重新排序,以使前者首先出现。 这可以显示一个竞争窗口,在此期间其他线程可以观察到部分初始化的Helper对象实例。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM