测试最终字段的初始化安全性
我试图简单地测试JLS保证的最终字段的初始化安全性。这是我写的一篇论文。但是,根据我当前的代码,我无法让它“失败”。有人可以告诉我我做错了什么,或者这只是我必须反复运行然后看到一个不幸的时机失败?
这是我的代码:
public class TestClass {
final int x;
int y;
static TestClass f;
public TestClass() {
x = 3;
y = 4;
}
static void writer() {
TestClass.f = new TestClass();
}
static void reader() {
if (TestClass.f != null) {
int i = TestClass.f.x; // guaranteed to see 3
int j = TestClass.f.y; // could see 0
System.out.println("i = " + i);
System.out.println("j = " + j);
}
}
}
我的线程正在调用它:
public class TestClient {
public static void main(String[] args) {
for (int i = 0; i < 10000; i++) {
Thread writer = new Thread(new Runnable() {
@Override
public void run() {
TestClass.writer();
}
});
writer.start();
}
for (int i = 0; i < 10000; i++) {
Thread reader = new Thread(new Runnable() {
@Override
public void run() {
TestClass.reader();
}
});
reader.start();
}
}
}
我已经多次运行这种情况了。我目前的循环产生10,000个线程,但我已经完成了1000,100000甚至一百万个。仍然没有失败。我总是看到两个值都是3和4。我怎么能让这个失败?
没有找到相关结果
已邀请:
8 个回复
翁茄口霉氖
并测试
莽缓逢
您可能看不到3 for x的原因是编译器重新排序此代码:
最终字段的保证通常在实践中实现的方式是确保构造函数在任何后续程序操作发生之前完成。想象一下,有人在r1.y = 4和f = r1之间竖起了一道大屏障。因此,在实践中,如果您有一个对象的任何最终字段,您可能会获得所有这些字段的可见性。 现在,从理论上讲,有人可以编写一个没有这种方式实现的编译器。事实上,许多人经常谈论通过编写可能最恶意的编译器来测试代码。这在C ++人群中尤为常见,他们有很多很多未定义的语言角落,可能导致可怕的错误。
峨躬坎抬焚
JLS要求(2)和(3)在对象出版物(4)之前发生。但是,由于数据竞争,没有给出(5)的保证 - 如果一个线程从未观察到写操作,它实际上是合法的执行。通过适当的线程交错,可以想象如果
在4到5之间运行,您将获得所需的输出。 我手头没有赛门铁克JIT所以无法通过实验证明:-)
视蕉梁拌客
。这是基于我的另一个问题,这个问题有点复杂。我一直看到人们说它不会发生在x86上,但我的例子发生在x64 linux openjdk 6 ...
填盖
我不是JLF决赛和初始化者的专家,但常识告诉我这应该延迟设置x足够让作家注册另一个值?
外镶受继
?
镰茧钩
由四个动作组成。调用第一个
指令,就像C / C ++中的malloc一样,它分配内存并在堆栈顶部放置一个引用。然后重复引用以调用构造函数。实际上,构造函数与任何其他实例方法一样,它使用重复的引用调用。只有在该引用存储在方法框架或实例字段中之后,才能从其他任何地方访问。在最后一步之前,对象的引用仅存在于创建线程堆栈的顶部,而其他任何人都无法看到它。事实上,你正在使用哪种领域没有区别,如果
,两者都将被初始化。您可以从不同的对象中读取x和y字段,但这不会导致
。有关更多信息,您应该看到JVM规范和面向堆栈的编程语言文章。 UPD:我忘了提到一件重要的事情。通过java内存,无法查看部分初始化的对象。如果你不在构造函数中做自我发布,当然。 JLS: 当一个对象被认为是完全初始化时 构造函数完成。一个只能看到对一个引用的线程 保证该对象已完全初始化后的对象 查看该对象最终的正确初始化值 领域。 JLS: 从构造函数的结尾有一个发生前的边缘 反对该对象的终结器的开始。 对这一观点的更广泛的解释: 事实证明,对象的构造函数的结束发生在之前 执行其finalize方法。在实践中,这意味着什么 必须完成构造函数中发生的任何写入 对于终结器中相同变量的任何读取都是可见的,就好像 那些变量是不稳定的。 UPD:那是理论,让我们转向练习。 考虑以下代码,使用简单的非最终变量:
以下命令显示java生成的机器指令,如何使用它可以在wiki中找到: java.exe -XX:+ UnlockDiagnosticVMOptions -XX:+ PrintAssembly -Xcomp -XX:PrintAssemblyOptions = hsdis-print-bytes -XX:CompileCommand = print,* Test.main Test 它的输出:
字段分配后面是NOPL指令,其中一个目的是防止指令重新排序。 为什么会这样?根据规范,在构造函数返回后进行终止。因此GC线程无法看到部分初始化的对象。在CPU级别上,GC线程与其他任何线程都不区分。如果向GC提供此类保证,则将其提供给任何其他线程。这是这种限制最明显的解决方案。 结果: 1)构造函数未同步,同步由其他指令完成。 2)对象的引用的赋值不能在构造函数返回之前发生。
苦诫
那是做什么的,按顺序: 评估
找出它的内存位置 评估
:这会创建一个TestClass的新实例,其构造函数将初始化
和
将右侧值分配给左侧内存位置 赋值是一种原子操作,它总是在生成右手值之后执行。这是来自Java语言规范的引用(参见第一个项目符号),但它确实适用于任何理智的语言。 这意味着虽然
构造函数正在花时间完成它的工作,并且
和
可能仍然为零,但对部分初始化的对象的引用仅存在于该线程的堆栈或CPU寄存器中,并且尚未写入到
因此
将始终包含:
,在你的程序开始时,在分配任何其他内容之前, 或完全初始化的
实例。