Java 非线程安全对象发布
阅读“实践中的Java并发”,第3.5节中有这一部分: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
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的代码,它基本上可以分解为一系列内存写入和分配:
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对象时