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

2022-09-01 12:11:47

我试图简单地测试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 甚至 100 万个线程。仍然没有失败。我总是看到3和4的两个值。我怎样才能让它失败?


答案 1

我写了规范。The TL;这个答案的DR版本是,仅仅因为它可能看到0代表y,这并不意味着它保证看到0代表y。

在这种情况下,正如您所指出的,最终的字段规范保证您将看到 x 的 3。将编写器线程视为具有 4 条指令:

r1 = <create a new TestClass instance>
r1.x = 3;
r1.y = 4;
f = r1;

您可能看不到 x 的 3 的原因是编译器对此代码进行了重新排序:

r1 = <create a new TestClass instance>
f = r1;
r1.x = 3;
r1.y = 4;

在实践中,最终字段的保证通常实现的方式是确保构造函数在任何后续程序操作发生之前完成。想象一下,有人在r1.y = 4和f = r1之间竖起了一个很大的障碍。因此,在实践中,如果某个对象有任何最终字段,则可能会获得所有这些字段的可见性。

现在,从理论上讲,有人可以编写一个不是以这种方式实现的编译器。事实上,许多人经常谈论通过编写最恶意的编译器来测试代码。这在C++人群中尤其常见,他们的语言中有很多很多未定义的角落,可能导致可怕的错误。


答案 2

从 Java 5.0 开始,您可以保证所有线程都将看到构造函数设置的最终状态。

如果你想看到这个失败,你可以尝试一个较旧的JVM,如1.3。

我不会打印出每个测试,我只打印出失败。你可能会在一百万分之一中失败,但错过了它。但是,如果您只打印故障,它们应该很容易被发现。

查看此失败的更简单方法是添加到编写器中。

f.y = 5;

并测试

int y = TestClass.f.y; // could see 0, 4 or 5
if (y != 5)
    System.out.println("y = " + y);