问题描述
阅读Java并发实践",3.5节中有这部分:
Reading "Java Concurrency In Practice", there's this part in section 3.5:
public Holder holder;
public void initialize() {
holder = new Holder(42);
}
除了创建两个 Holder
实例的明显线程安全隐患外,该书还声称可能会出现发布问题.
Besides the obvious thread safety hazard of creating two instances of Holder
, the book claims a possible publishing issue can occur.
此外,对于 Holder
类,例如
Furthermore, for a Holder
class such as
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
构造函数不会被阻塞,那么当构造函数代码仍在不同的线程中运行时,将创建对实例的引用.
How is this possible? The only way I can think of that can allow such ridiculous behavior is if the Holder
constructor would not be blocking, so a reference would be created to the instance while the constructor code still runs in a different thread.
这可能吗?
推荐答案
之所以能够做到这一点,是因为 Java 具有弱内存模型.它不保证读取和写入的顺序.
The reason why this is possible is that Java has a weak memory model. It does not guarantee ordering of read and writes.
这个特殊的问题可以用下面两个代表两个线程的代码片段来重现.
This particular problem can be reproduced with the following two code snippets representing two threads.
主题 1:
someStaticVariable = new Holder(42);
主题 2:
someStaticVariable.assertSanity(); // can throw
从表面上看,这似乎永远不可能发生.为了理解为什么会发生这种情况,您必须跳过 Java 语法并深入到低得多的级别.如果查看线程 1 的代码,它本质上可以分解为一系列内存写入和分配:
On the surface it seems impossible that this could ever occur. In order to understand why this can happen, you have to get past the Java syntax and get down to a much lower level. If you look at the code for thread 1, it can essentially be broken down into a series of memory writes and allocations:
- 为指针 1 分配内存
- 将 42 写入指针 1 的偏移量 0
- 将指针 1 写入一些静态变量
因为 Java 的内存模型很弱,所以从线程 2 的角度来看,代码完全有可能按照以下顺序实际执行:
Because Java has a weak memory model, it is perfectly possible for the code to actually execute in the following order from the perspective of thread 2:
- 为指针 1 分配内存
- 将指针 1 写入一些静态变量
- 将 42 写入指针 1 的偏移量 0
可怕吗?是的,但它可能发生.
Scary? Yes but it can happen.
这意味着线程 2 现在可以在 n
获得值 42 之前调用 assertSanity
.值 n
在 assertSanity
期间被读取两次,一次在操作 #3 完成之前和一次之后,因此看到两个不同的值并抛出异常.
What this means though is that thread 2 can now call into assertSanity
before n
has gotten the value 42. It is possible for the value n
to be read twice during assertSanity
, once before operation #3 completes and once after and hence see two different values and throw an exception.
编辑
根据 Jon Skeet 的说法,AssertionError
Java 8 除非该字段是最终的.
According to Jon Skeet, the AssertionError
migh still occur with Java 8 unless the field is final.
这篇关于非线程安全的对象发布的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!