30

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が表示されます。どうすればこれを失敗させることができますか?

4

9 に答える 9

20

スペックを書きました。TL; この回答のDRバージョンは、yに対して0が表示される可能性があるという理由だけで、yに対して0が表示されることが保証されているわけではありません。

この場合、ご指摘のとおり、最終的なフィールド仕様では、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++の人々の間で特に一般的です。

于 2013-03-20T06:55:26.053 に答える
7

Java 5.0からは、すべてのスレッドがコンストラクターによって設定された最終状態を確認することが保証されます。

これが失敗するのを見たい場合は、1.3のような古いJVMを試すことができます。

すべてのテストを印刷するのではなく、失敗を印刷するだけです。100万回に1回失敗する可能性がありますが、見逃してしまいます。ただし、失敗を印刷するだけの場合は、簡単に見つけることができます。

この失敗を確認する簡単な方法は、ライターに追加することです。

f.y = 5;

とテストする

int y = TestClass.f.y; // could see 0, 4 or 5
if (y != 5)
    System.out.println("y = " + y);
于 2011-02-21T14:33:59.117 に答える
5

失敗するテスト、または現在のJVMでは不可能な理由の説明を確認したいと思います。

マルチスレッドとテスト

いくつかの理由でテストしても、マルチスレッドアプリケーションが壊れている(または壊れていない)ことを証明することはできません。

  • この問題は、実行のx時間ごとに1回だけ発生する可能性があり、xが非常に高いため、短いテストで問題が発生する可能性はほとんどありません。
  • この問題は、JVM/プロセッサアーキテクチャの一部の組み合わせでのみ発生する可能性があります

あなたの場合、テストを中断する(つまり、y == 0を観察する)には、一部のフィールドが適切に構築され、一部が構築されていない部分的に構築されたオブジェクトをプログラムで確認する必要があります。これは通常、x86/ホットスポットでは発生しません。

マルチスレッドコードが壊れているかどうかを判断するにはどうすればよいですか?

コードが有効または破損していることを証明する唯一の方法は、JLSルールをコードに適用し、結果を確認することです。データレースパブリッシング(オブジェクトまたはyのパブリケーションの同期がない)では、JLSはyが4として表示されることを保証しません(デフォルト値の0で表示される可能性があります)。

そのコードは本当に壊れますか?

実際には、一部のJVMは、テストを失敗させるのに優れています。たとえば、一部のコンパイラ(この記事の「機能しないことを示すテストケース」を参照)は、次のようなものに変換できますTestClass.f = new TestClass();(データレースを介して公開されているため)。

(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の間で実行すると、目的の出力が得られると考えられます。

私は手元にsymantecJITを持っていないので、実験的に証明することはできません:-)

于 2013-03-14T08:55:50.727 に答える
3

これは、コンストラクターがデフォルト値を設定し、リークしないにもかかわらず、非最終値のデフォルト値が観察される例ですthis。これは、もう少し複雑な私の他の質問に基づいています。私は人々がそれがx86で起こることができないと言うのを見続けます、しかし私の例はx64 linuxopenjdk6で起こります...

于 2013-04-26T18:35:07.147 に答える
3

これは複雑な答えの良い質問です。読みやすくするために分割しました。

人々はここで十分な回数、厳格なルールの下でJLS-あなたは望ましい行動を見ることができるはずだと言っています。しかし、コンパイラー(つまりC1C2)は、を尊重する必要がありJLSますが、最適化を行うことができます。そして、これについては後で説明します。

最初の簡単なシナリオを考えてみましょう。このシナリオでは、2つのnon-final変数があり、正しくないオブジェクトを公開できるかどうかを確認します。このテストでは、この種のテストに正確に合わせて調整された専用ツールを使用しています。これを使用したテストは次のとおりです。

@Outcome(id = "0, 2", expect = Expect.ACCEPTABLE_INTERESTING, desc = "not correctly published")
@Outcome(id = "1, 0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "not correctly published")
@Outcome(id = "1, 2", expect = Expect.ACCEPTABLE, desc = "published OK")
@Outcome(id = "0, 0", expect = Expect.ACCEPTABLE, desc = "II_Result default values for int, not interesting")
@Outcome(id = "-1, -1", expect = Expect.ACCEPTABLE, desc = "actor2 acted before actor1, this is OK")
@State
@JCStressTest
public class FinalTest {

    int x = 1;
    Holder h;

    @Actor
    public void actor1() {
        h = new Holder(x, x + 1);
    }

    @Actor
    public void actor2(II_Result result) {
        Holder local = h;
        // the other actor did it's job
        if (local != null) {
            // if correctly published, we can only see {1, 2} 
            result.r1 = local.left;
            result.r2 = local.right;
        } else {
            // this is the case to "ignore" default values that are
            // stored in II_Result object
            result.r1 = -1;
            result.r2 = -1;
        }
    }

    public static class Holder {

        // non-final
        int left, right;

        public Holder(int left, int right) {
            this.left = left;
            this.right = right;
        }
    }
}

コードをあまり理解する必要はありません。非常に最小限の説明はこれですが、Actorいくつかの共有データを変更する2つのがあり、それらの結果が登録されます。@Outcome注釈は、それらの登録された結果を制御し、特定の期待を設定します(内部では、物事ははるかに興味深く、冗長です)。念頭に置いておくと、これは非常にシャープで特殊なツールです。2つのスレッドを実行している場合、実際には同じことを行うことはできません。

これを実行すると、次の2つの結果になります。

 @Outcome(id = "0, 2", expect = Expect.ACCEPTABLE_INTERESTING....)
 @Outcome(id = "1, 0", expect = Expect.ACCEPTABLE_INTERESTING....)

観察されます(他のアクター/スレッドが実際に見た、オブジェクトの安全でない公開があったことを意味します)。

具体的には、これらはいわゆるTC2一連のテストで観察され、実際には次のように実行されます。

java... -XX:-TieredCompilation 
        -XX:+UnlockDiagnosticVMOptions 
        -XX:+StressLCM 
        -XX:+StressGCM

これらの機能についてはあまり詳しく説明しませんが、StressLCMとStressGCMの機能、そしてもちろん、TieredCompilationフラグの機能を以下に示します。

テストの全体的なポイントは次のとおりです。

このコードは、コンストラクターで設定された2つの非最終変数が誤って公開され、で実行されていることを証明しますx86


専用のツールが用意されているので、今やるべきことは、1つのフィールドをに変更して、finalそれが壊れていることを確認することです。そのため、これを変更して再度実行すると、障害を監視する必要があります。

public static class Holder {

    // this is the change
    final int right;
    int left;

    public Holder(int left, int right) {
        this.left = left;
        this.right = right;
    }
}

しかし、もう一度実行すると、失敗は発生しません。つまり、@Outcome上記で説明した2つはいずれも出力の一部にはなりません。どうして?

単一の最終変数にさえ書き込むときJVM(具体的C1には)は常に正しいことをすることがわかります。単一のフィールドであっても、これを実証することは不可能です。少なくとも現時点では。


理論的には、これに投げ込むことができShenandoah、それは興味深いフラグです:(ShenandoahOptimizeInstanceFinalsそれに飛び込むつもりはありません)。私は前の例を次のように実行してみました:

 -XX:+UnlockExperimentalVMOptions  
 -XX:+UseShenandoahGC  
 -XX:+ShenandoahOptimizeInstanceFinals  
 -XX:-TieredCompilation  
 -XX:+UnlockDiagnosticVMOptions  
 -XX:+StressLCM  
 -XX:+StressGCM 

しかし、これは私が期待したようには機能しません。これを試してみたという私の主張にとってはるかに悪いのは、これらのフラグがjdk-14で削除されることです。

結論:現時点では、これを破る方法はありません。

于 2019-12-18T04:35:25.693 に答える
-1

これを行うためにコンストラクターを変更したのはどうですか?

public TestClass() {
 Thread.sleep(300);
   x = 3;
   y = 4;
}

私はJLFファイナルとイニシャライザーの専門家ではありませんが、常識的に言えば、ライターが別の値を登録するのに十分な時間xの設定を遅らせる必要がありますか?

于 2013-03-14T07:52:16.093 に答える
-2

シナリオを次のように変更するとどうなりますか

public class TestClass {

    final int x;
    static TestClass f;

    public TestClass() {
        x = 3;
    }

    int y = 4;

    // etc...

}

于 2013-03-12T06:22:19.693 に答える
-3

このテストが失敗しない理由をよりよく理解するには、コンストラクターが呼び出されたときに実際に何が起こるかを理解する必要があります。Javaはスタックベースの言語です。TestClass.f = new TestClass();4つのアクションで構成されます。初めnew命令は、C / C ++のmallocと同様に呼び出され、メモリを割り当てて、スタックの最上位に参照を配置します。次に、コンストラクターを呼び出すための参照が複製されます。実際、コンストラクターは他のインスタンスメソッドと同様であり、複製された参照を使用して呼び出されます。その後、参照がメソッドフレームまたはインスタンスフィールドに保存され、他の場所からアクセスできるようになります。最後のステップの前は、オブジェクトへの参照はスレッドのスタックの作成の最上位にのみ存在し、他のボディはそれを見ることができません。実際、どの種類のフィールドを使用しているかに違いはありません。両方とも、の場合に初期化されますTestClass.f != null。異なるオブジェクトからxフィールドとyフィールドを読み取ることができますが、これは結果にはなりませんy = 0。詳細については、JVM仕様スタック指向プログラミング言語の記事。

UPD:私が言及するのを忘れた重要なことの1つ。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命令が続きます。その目的の1つは、命令の並べ替えを防ぐことです。

なぜこれが起こるのですか?仕様によると、コンストラクターが戻った後にファイナライズが行われます。したがって、GCスレッドは部分的に初期化されたオブジェクトを見ることができません。CPUレベルでは、GCスレッドは他のスレッドと区別されません。そのような保証がGCに提供される場合、他のスレッドに提供されるよりも。これは、そのような制限に対する最も明白な解決策です。

結果:

1)コンストラクターが同期されていません。同期は他の命令によって行われます。

2)コンストラクタが戻る前に、オブジェクトの参照への割り当てを行うことはできません。

于 2013-03-11T15:31:27.807 に答える
-3

このスレッドで何が起こっているのですか?そもそもなぜそのコードは失敗するのでしょうか?

それぞれが次のことを行う数千のスレッドを起動します。

TestClass.f = new TestClass();

それが何をするのか、順番に:

  1. TestClass.fそのメモリ位置を見つけるために評価する
  2. 評価new TestClass():これにより、TestClassの新しいインスタンスが作成され、そのコンストラクターはxy
  3. 右側の値を左側のメモリ位置に割り当てます

割り当ては、右側の値が生成された後に常に実行される不可分操作です。これはJava言語仕様からの引用です(最初の箇条書きを参照)が、実際にはすべての正常な言語に適用されます。

これは、TestClass()コンストラクターがその仕事をするのに時間がかかり、xおそらくyまだゼロである可能性がある間、部分的に初期化されたTestClassオブジェクトへの参照はそのスレッドのスタックまたはCPUレジスターにのみ存在し、に書き込まれていないことを意味しますTestClass.f

したがってTestClass.f、常に次のものが含まれます。

  • nullプログラムの開始時、他に何かが割り当てられる前のいずれか、
  • または完全に初期化されたTestClassインスタンス。
于 2013-03-17T11:14:55.000 に答える