Java 非线程安全对象发布

Java 非线程安全对象发布,java,concurrency,thread-safety,Java,Concurrency,Thread Safety,阅读“实践中的Java并发”,第3.5节中有这一部分: public Holder holder; public void initialize() { holder = new Holder(42); } 除了创建两个Holder实例这一明显的线程安全隐患外,该书还声称可能会出现出版问题 此外,对于持有者类,例如 public Holder { int n; public Holder(int n) { this.n = n }; public void a

阅读“实践中的Java并发”,第3.5节中有这一部分:

public Holder holder;
public void initialize() {
     holder = new Holder(42);
}
除了创建两个
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内存模型过去是这样的:对
持有者
引用的赋值可能在对对象中的变量赋值之前变得可见

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

一个对象被认为是 当其 建造师完成。一根线 只能看到对对象的引用 在那个物体被完全摧毁之后 初始化后,保证可以看到 已正确初始化该文件的值 对象的最终字段

因此,您的示例仍然可能失败,因为
n
不是final,但如果您将
n
设为final,则应该可以

当然:

if (n != n)
如果JIT编译器没有对其进行优化,那么非最终变量肯定会失败-如果操作是:

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

然后,值可以在两次获取之间更改。

之所以可能,是因为Java的内存模型很弱。它不保证读取和写入的顺序

这个特殊的问题可以通过以下两个代表两个线程的代码片段重现

线程1:

someStaticVariable = new Holder(42);
线程2:

someStaticVariable.assertSanity(); // can throw
从表面上看,这似乎不可能发生。为了理解为什么会发生这种情况,您必须超越Java语法,深入到一个更低的层次。如果查看线程1的代码,它基本上可以分解为一系列内存写入和分配:

  • 将内存分配到指针1
  • 将42写入偏移量0处的指针1
  • 将指针1写入someStaticVariable
  • 因为Java的内存模型很弱,所以从线程2的角度来看,代码完全有可能按照以下顺序执行:

  • 将内存分配到指针1
  • 将指针1写入someStaticVariable
  • 将42写入偏移量0处的指针1
  • 吓人?是的,但这是可能发生的

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

    编辑


    除非字段是最终字段,否则,
    AssertionError
    可能仍然会出现。

    如果您假设语句

    如果(n!=n)


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

    基本问题是,如果没有适当的同步,写入内存的内容可能会在不同的线程中表现出来。经典的例子是:

    a = 1;
    b = 2;
    

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

    在书中,它为第一个代码块声明:

    这里的问题不在于支架 类本身,但持有者是 没有适当出版。然而, 可以使支架免受不适当的损坏 通过声明n字段进行发布 最后,这将使持有人 不变的见第3.5.2节

    对于第二个代码块:

    因为没有使用同步 使支架对其他人可见 线程,我们说的持有人不是 适当出版。有两件事可以做 错误与不当出版 物体。其他线程可以看到 holder字段的过时值,以及 因此,可以看到空引用或其他引用 旧值,即使某个值具有 被放在支架上。但更糟糕的是, 其他线程可能会看到最新消息 持有人参考值,但 服务器状态的过时值 持有人[16]使事情变得更糟 可以预见,线程可能会看到过时的 第一次读取字段时的值 然后是一个更为最新的值 下一次,这就是为什么assertSanity 可以抛出断言错误

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

    (注意:此处不寻求投票——答案允许提供比注释更详细的信息。)

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

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

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

    假设:第一个线程从上午10:00开始,它通过调用new Holer(42)调用instatied Holder对象, 1) Java虚拟mac