不是线程安全的对象发布

Max*_*ler 14 java concurrency thread-safety

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

public Holder holder;
public void initialize() {
     holder = new Holder(42);
}
Run Code Online (Sandbox Code Playgroud)

除了创建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.");
    }
}
Run Code Online (Sandbox Code Playgroud)

可以抛出AssertionError!

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

Jar*_*Par 14

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

线程1:

someStaticVariable = new Holder(42);
Run Code Online (Sandbox Code Playgroud)

线程2:

someStaticVariable.assertSanity(); // can throw
Run Code Online (Sandbox Code Playgroud)

从表面上看,似乎不可能发生这种情况.为了理解为什么会发生这种情况,您必须超越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个不同的值和抛出一个例外.

编辑

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

编辑第二

根据乔恩的说法,除非该领域是最终的,否则他永远不会说第8版Java是不可能的.


Jon*_*eet 11

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

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

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

所以你的例子仍然可能会失败,因为n它是非最终的,但如果你做出n决定,它应该没问题.

当然是:

if (n != n)
Run Code Online (Sandbox Code Playgroud)

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

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

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