测试最终字段的初始化安全性

我试图简单地测试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。我怎么能让这个失败?     
已邀请:
从Java 5.0开始,您需要保证所有线程都能看到构造函数设置的最终状态。 如果你想看到这个失败,你可以尝试像1.3这样的旧JVM。 我不会打印出每个测试,我只打印出失败。你可能会在一百万人中失败,但却错过了。但如果你只打印失败,它们应该很容易被发现。 查看此失败的更简单方法是添加到编写器。
f.y = 5;
并测试
int y = TestClass.f.y; // could see 0, 4 or 5
if (y != 5)
    System.out.println("y = " + y);
    
我写了规范。 TL;这个答案的DR版本只是因为y可能看到0,这并不意味着它保证y为0。 在这种情况下,最终的字段规范保证您将看到3 for x,正如您所指出的那样。将编写器线程视为具有4条指令:
r1 = <create a new TestClass instance>
r1.x = 3;
r1.y = 4;
f = r1;
您可能看不到3 for x的原因是编译器重新排序此代码:
r1 = <create a new TestClass instance>
f = r1;
r1.x = 3;
r1.y = 4;
最终字段的保证通常在实践中实现的方式是确保构造函数在任何后续程序操作发生之前完成。想象一下,有人在r1.y = 4和f = r1之间竖起了一道大屏障。因此,在实践中,如果您有一个对象的任何最终字段,您可能会获得所有这些字段的可见性。 现在,从理论上讲,有人可以编写一个没有这种方式实现的编译器。事实上,许多人经常谈论通过编写可能最恶意的编译器来测试代码。这在C ++人群中尤为常见,他们有很多很多未定义的语言角落,可能导致可怕的错误。     
  我希望看到一个测试失败或解释为什么当前的JVM无法实现。 多线程和测试 由于以下几个原因,您无法通过测试证明多线程应用程序已损坏(或未损坏): 问题可能只出现在每x小时运行一次,x太高,以至于你不太可能在短时间内看到它 问题可能只出现在JVM /处理器体系结构的某些组合中 在你的情况下,为了使测试中断(即观察y == 0)将要求程序看到部分构造的对象,其中一些字段已经正确构造而一些字段没有。这通常不会发生在x86 / hotspot上。 如何确定多线程代码是否被破坏? 证明代码有效或损坏的唯一方法是将JLS规则应用于它并查看结果是什么。使用数据竞争发布(没有围绕对象或y的发布进行同步),JLS不保证y将被视为4(可以看到它的默认值为0)。 该代码真的可以破解吗? 在实践中,一些JVM会更好地使测试失败。例如,一些编译器(参见本文中的“测试用例表明它不起作用”)可以将ѭ6变换为类似的东西(因为它是通过数据竞争发布的):
(1) allocate memory
(2) write fields default values (x = 0; y = 0) //always first
(3) write final fields final values (x = 3)    //must happen before publication
(4) publish object                             //TestClass.f = new TestClass();
(5) write non final fields (y = 4)             //has been reodered after (4)
JLS要求(2)和(3)在对象出版物(4)之前发生。但是,由于数据竞争,没有给出(5)的保证 - 如果一个线程从未观察到写操作,它实际上是合法的执行。通过适当的线程交错,可以想象如果
reader
在4到5之间运行,您将获得所需的输出。 我手头没有赛门铁克JIT所以无法通过实验证明:-)     
下面是一个非最终值的默认值示例,尽管构造函数设置它们并且不会泄漏
this
。这是基于我的另一个问题,这个问题有点复杂。我一直看到人们说它不会发生在x86上,但我的例子发生在x64 linux openjdk 6 ...     
你怎么修改构造函数来做到这一点:
public TestClass() {
 Thread.sleep(300);
   x = 3;
   y = 4;
}
我不是JLF决赛和初始化者的专家,但常识告诉我这应该延迟设置x足够让作家注册另一个值?     
如果将场景更改为,该怎么办?
public class TestClass {

    final int x;
    static TestClass f;

    public TestClass() {
        x = 3;
    }

    int y = 4;

    // etc...

}
?     
更好地理解为什么这个测试不会失败可以来自对构造函数被调用时实际发生的事情的理解。 Java是一种基于堆栈的语言。
TestClass.f = new TestClass();
由四个动作组成。调用第一个
new
指令,就像C / C ++中的malloc一样,它分配内存并在堆栈顶部放置一个引用。然后重复引用以调用构造函数。实际上,构造函数与任何其他实例方法一样,它使用重复的引用调用。只有在该引用存储在方法框架或实例字段中之后,才能从其他任何地方访问。在最后一步之前,对象的引用仅存在于创建线程堆栈的顶部,而其他任何人都无法看到它。事实上,你正在使用哪种领域没有区别,如果
TestClass.f != null
,两者都将被初始化。您可以从不同的对象中读取x和y字段,但这不会导致
y = 0
。有关更多信息,您应该看到JVM规范和面向堆栈的编程语言文章。 UPD:我忘了提到一件重要的事情。通过java内存,无法查看部分初始化的对象。如果你不在构造函数中做自我发布,当然。 JLS:   当一个对象被认为是完全初始化时   构造函数完成。一个只能看到对一个引用的线程   保证该对象已完全初始化后的对象   查看该对象最终的正确初始化值   领域。 JLS:   从构造函数的结尾有一个发生前的边缘   反对该对象的终结器的开始。 对这一观点的更广泛的解释:   事实证明,对象的构造函数的结束发生在之前   执行其finalize方法。在实践中,这意味着什么   必须完成构造函数中发生的任何写入   对于终结器中相同变量的任何读取都是可见的,就好像   那些变量是不稳定的。 UPD:那是理论,让我们转向练习。 考虑以下代码,使用简单的非最终变量:
public class Test {

    int myVariable1;
    int myVariable2;

    Test() {
        myVariable1 = 32;
        myVariable2 = 64;
    }

    public static void main(String args[]) throws Exception {
        Test t = new Test();
        System.out.println(t.myVariable1 + t.myVariable2);
    }
}
以下命令显示java生成的机器指令,如何使用它可以在wiki中找到:   java.exe -XX:+ UnlockDiagnosticVMOptions -XX:+ PrintAssembly -Xcomp   -XX:PrintAssemblyOptions = hsdis-print-bytes -XX:CompileCommand = print,* Test.main Test 它的输出:
...
0x0263885d: movl   $0x20,0x8(%eax)    ;...c7400820 000000
                                    ;*putfield myVariable1
                                    ; - Test::<init>@7 (line 12)
                                    ; - Test::main@4 (line 17)
0x02638864: movl   $0x40,0xc(%eax)    ;...c7400c40 000000
                                    ;*putfield myVariable2
                                    ; - Test::<init>@13 (line 13)
                                    ; - Test::main@4 (line 17)
0x0263886b: nopl   0x0(%eax,%eax,1)   ;...0f1f4400 00
...
字段分配后面是NOPL指令,其中一个目的是防止指令重新排序。 为什么会这样?根据规范,在构造函数返回后进行终止。因此GC线程无法看到部分初始化的对象。在CPU级别上,GC线程与其他任何线程都不区分。如果向GC提供此类保证,则将其提供给任何其他线程。这是这种限制最明显的解决方案。 结果: 1)构造函数未同步,同步由其他指令完成。 2)对象的引用的赋值不能在构造函数返回之前发生。     
这个帖子里发生了什么?为什么该代码首先会失败? 您将启动1000个线程,每个线程将执行以下操作:
TestClass.f = new TestClass();
那是做什么的,按顺序: 评估
TestClass.f
找出它的内存位置 评估
new TestClass()
:这会创建一个TestClass的新实例,其构造函数将初始化
x
y
将右侧值分配给左侧内存位置 赋值是一种原子操作,它总是在生成右手值之后执行。这是来自Java语言规范的引用(参见第一个项目符号),但它确实适用于任何理智的语言。 这意味着虽然
TestClass()
构造函数正在花时间完成它的工作,并且
x
y
可能仍然为零,但对部分初始化的对象的引用仅存在于该线程的堆栈或CPU寄存器中,并且尚未写入到
TestClass.f
因此
TestClass.f
将始终包含:
null
,在你的程序开始时,在分配任何其他内容之前, 或完全初始化的
TestClass
实例。     

要回复问题请先登录注册