この記事の主な内容は、Javaインタビューの一般的な知識ポイントである揮発性キーワードです。この記事では、揮発性キーワードのすべての側面を詳細に紹介します。この記事を読んだ後、揮発性キーワードの関連する問題を完全に解決できることを願っています。
Java関連の就職のインタビューでは、多くのインタビュアーがJavaの並行性に関するインタビュアーの理解を調べたいと思っています。揮発性キーワードを小さなエントリポイントとして使用すると、Javaメモリモデル(JMM)とJava Concurrentプログラミングのいくつかの機能を尋ねることがよくあります。詳細には、基礎となるJVM実装およびオペレーティングシステム関連の知識を調べることもできます。仮説的なインタビュープロセスを取って、不死身のキーワードの詳細な理解を得ましょう!
私が理解する限り、揮発性によって変更された共有変数には、次の2つの特性があります。
1.変数操作に対する異なるスレッドのメモリの可視性を確保します。
2。コマンドの並べ替えを禁止します
これは多くのことを話す必要があるので、Javaメモリモデルから始めます。 Java仮想マシン仕様は、Javaメモリモデル(JMM)を定義して、さまざまなハードウェアとオペレーティングシステム間のメモリアクセスの違いをブロックしようとするため、Javaプログラムはさまざまなプラットフォームで一貫したメモリアクセス効果を実現できます。簡単に言えば、CPUは命令を非常に迅速に実行するため、メモリアクセス速度ははるかに遅くなり、違いは数という大きさではありません。プロセッサで作業する大物は、CPUにいくつかのキャッシュを追加しました。 Javaメモリモデルでは、上記の最適化が再び抽出されます。 JMMは、上記の通常のメモリと同様に、すべての変数がメインメモリにあることを規定しており、各スレッドには独自の作業メモリが含まれています。理解しやすくするために、CPUのレジスタまたはキャッシュと見なすことができます。したがって、スレッド操作は主にワーキングメモリに基づいています。彼らは独自の作業メモリにのみアクセスすることができ、仕事の前後に値をメインメモリに戻す必要があります。私が言ったことさえ知らないので、紙を取り、描く:
スレッドを実行すると、変数の値が最初にメインメモリから読み取り、次にワーキングメモリのコピーにロードされ、実行のためにプロセッサに渡されます。実行が完了すると、ワーキングメモリのコピーに値が割り当てられ、その後、作業メモリの値がメインメモリに渡され、メインメモリの値が更新されます。ワーキングメモリとメインメモリの使用は高速ですが、いくつかの問題ももたらします。たとえば、次の例をご覧ください。
i = i + 1;
Iの初期値が0であると仮定すると、1つのスレッドのみが実行されると、結果は間違いなく1になります。2つのスレッドが実行されると、結果は2になりますか?これは必ずしもそうではありません。これはそうかもしれません:
スレッド1:メインメモリからiロードI = 0 I + 1 // i = 1スレッド2:メインメモリからIロードIスレッド1がiの値をメインメモリに戻していないため、I + 1 // i = 1スレッド1:メインメモリ2にiです。
2つのスレッドが上記の実行プロセスに従う場合、Iの最後の値は実際には1です。最後の書き込みのバックが遅い場合、Iの値をもう一度読み取ることができます。これはキャッシュの不一致の問題です。以下は、あなたが今尋ねた質問に言及するためです。 JMMは、並行性プロセスの原子性、視界、順序の3つの特性に対処する方法を主に確立しています。これら3つの問題を解決することにより、キャッシュの矛盾の問題を解決できます。揮発性は視界と秩序性に関連しています。
1。原子性:Javaでは、基本データ型の読み取りおよび割り当て操作は原子操作です。いわゆる原子操作は、これらの操作が途切れやすく、一定の期間完了する必要があることを意味します。または、実行されないことを意味します。例えば:
i = 2; j = i; i ++; i = i+1;
上記の4つの操作の中で、i = 2は読み取り操作であり、これは原子操作でなければなりません。 J =原子操作だと思います。実際、それは2つのステップに分かれています。 1つは、iの値を読み取り、jに値を割り当てることです。これは2段階の操作です。原子操作と呼ぶことはできません。 i ++とi = i+ 1は実際に同等です。 iの値を読み、1を追加し、メインメモリに書き戻します。それは3段階の操作です。したがって、上記の例では、原子性を満たすことができないため、最後の値には多くの状況がある場合があります。このようにして、簡単な読書だけがあります。割り当ては原子操作、または数値割り当てのみです。変数を使用する場合、変数値を読み取るための追加操作があります。例外は、仮想マシンの仕様により、64ビットのデータ型(LONGおよびDOUBLE)を2つの操作で処理できることですが、最新のJDK実装によりアトミック操作が依然として実装されています。 JMMは基本的な原子のみを実装します。上記のI ++のような操作は、コード全体の原子性を確保するために同期してロックする必要があります。スレッドがロックを解放する前に、それは必然的に私の値をメインメモリに戻すことになります。 2。可視性:可視性といえば、Javaは揮発性を使用して視界を提供します。変数が揮発性によって変更されると、その変更はすぐにメインメモリに更新されます。他のスレッドが変数を読み取る必要がある場合、新しい値はメモリで読み取られます。これは、通常の変数によって保証されません。実際、同期とロックは視認性を確保することもできます。スレッドがロックを解放する前に、共有されたすべての変数値をメインメモリに戻しますが、同期されたロックはより高価です。 3. JMMの注文により、コンパイラとプロセッサが命令を並べ替えることができますが、AS-if-Serial Semanticsを規定しています。つまり、どのように並べ替えても、プログラムの実行結果を変更できません。たとえば、次のプログラムセグメント:
double pi = 3.14; // adouble r = 1; // bdouble s = pi * r * r; // c
上記のステートメントはa-> b-> cで実行でき、結果は3.14ですが、b-> a-> cの順序で実行することもできます。 AとBは2つの独立したステートメントであるため、CはAとBに依存しますが、AとBは並べ替えることはできませんが、CはAおよびBで最初にランク付けすることはできません。JMMは、並べ替えが単一のスレッドの実行に影響を与えないことを保証しますが、問題はマルチスレッドで発生する傾向があります。たとえば、このようなコード:
int a = 0; bool flag = false; public void write(){a = 2; // 1 flag = true; // 2} public void multiply(){if(flag){// 3 int ret = a * a; // 4}}}2つのスレッドが上記のコードセグメントを実行した場合、スレッド1は最初に書き込みを実行し、次にスレッド2を実行し、次にマルチプリを実行すると、RETの値は4になる必要がありますか?結果は必ずしも次のとおりです。
図に示すように、書き込み方法の1と2が再注文されます。スレッド1は最初にフラグをtrueに割り当て、次にスレッド2に実行し、RETが結果を直接計算し、次にスレッド1になります。この時点で、Aは明らかに1ステップ後です。この時点で、揮発性のキーワードをフラグに追加し、プログラムの秩序を確保できる並べ替えを禁止することもできます。また、ヘビー級の同期とロックを使用して秩序性を確保することもできます。彼らは、その領域のコードが一度に実行されることを保証できます。さらに、JMMには、いくつかの生来の順序、つまり、何らかの手段なしで保証できる秩序性があります。これは通常、実現前の原則と呼ばれます。 << JSR-133:Javaメモリモデルとスレッド仕様>>次のルールを定義します。1。プログラムシーケンスルール:スレッドの各操作について、スレッドでの後続操作に行われます。ドメイン4。トランジニービティ:Aが発生した場合、BとBの前に発生する場合、c前に発生する場合、aはcの前に発生します5.Start()ルール:スレッドaが操作threadb_start()(スレッドB)を実行する場合、スレッドb_start()は、スレッドAの前に発生します。 threadb.join()操作から正常に戻ります。ThreadA。 Thread.interrupded()メソッドを使用して、中断があるかどうかを検出できます。 8。finalize()原則:オブジェクトの初期化完了は、finalize()メソッドが開始されたときに最初に発生します。プログラムシーケンスルールの最初のルールには、スレッドではすべての操作が順番に行われているが、JMMでは、実行結果が同じ限り、並べ替えが許可されていることを示しています。ここで起こることの焦点は、単一スレッド実行結果の正確性でもありますが、マルチスレッドにも同じことが当てはまることを保証することはできません。ルール2モニタールールは実際には理解しやすいです。ロックを追加する前に、ロックを追加し続けることしかできません。ルール3は、問題の揮発性に適用されます。 1つのスレッドが最初に変数を書き、別のスレッドが読み取る場合、書き込み操作は読み取り操作の前になければなりません。 4番目のルールは、前に起こることのトランジテーションです。次の数人については詳しく説明しません。
次に、揮発性変数ルールを再辞任する必要があります。揮発性ドメインを作成し、後でこの揮発性ドメインを読む前に発生します。これをもう一度取り出しましょう。実際、変数が揮発性として宣言されている場合、変数を読むと、いつでも最新の値を読むことができます。ここで、最新の値は、他のスレッドが変数を書き込んでも、すぐにメインメモリに更新されることを意味します。また、メインメモリから新たに書かれた価値を読むこともできます。言い換えれば、揮発性キーワードは、可視性と秩序性を確保することができます。上記のコードを例としてとろうとします。
int a = 0; bool flag = false; public void write(){a = 2; // 1 flag = true; // 2} public void multiply(){if(flag){// 3 int ret = a * a; // 4}}}このコードは、1と2が並べ替えられていなくても、並べ替えによって問題を抱えているだけではありません。 3はそれほどスムーズに実行されません。スレッド1が最初に書き込み操作を実行し、スレッド2が乗算操作を実行するとします。スレッド1はワーキングメモリにフラグを1に割り当てるため、すぐにメインメモリに書き戻さない場合があります。したがって、スレッド2が実行されると、乗算はメインメモリからフラグ値を読み取りますが、これは依然として偽である可能性があるため、ブラケットのステートメントは実行されません。以下に変更した場合:
int a = 0; volatile bool flag = false; public void write(){a = 2; // 1 flag = true; // 2} public void multiply(){if(flag){// 3 int ret = a * a; // 4}}}次に、スレッド1が最初に書き込みを実行し、スレッド2が乗算を実行します。前の原則に従って、このプロセスは、次の3つのタイプのルールを満たします。プログラム順序ルール: 3は4の前に起こります。 (揮発性が命令の並べ替えを制限するため、1は2)揮発性ルールの前に実行されます。揮発性変数を読み取ると、JMMはスレッドに対応するローカルメモリを無効にし、次にメインメモリから共有変数を読み取ります。
まず第一に、私の答えは、原子性を保証できないということです。それが保証されている場合、それは単一の揮発性変数の読み取り/書き込みの原子性のみですが、次の例など、揮発性++のような複合操作とは何の関係もありません。
パブリッククラステスト{public volatile int inc = 0; public void crossion(){inc ++; } public static void main(string [] args){最終テスト= new test(); for(int i = 0; i <10; i ++){new shood(){public void run(){for(int j = 0; j <1000; j ++)test.increase(); }; }。始める(); } while(thread.activecount()> 1)//以前のスレッドがthread.yield()を完了していることを確認してください。 System.out.println(test.inc); }論理的に言えば、結果は10,000ですが、実行中は10,000未満の値になる可能性があります。一部の人々は、揮発性が可視性を保証しないと言うかもしれません。 1つのスレッドはIncによるIncの変更が表示され、もう1つのスレッドはすぐに表示されます!しかし、Operation Inc ++は、Incの価値を読み取り、それ自体で増やし、メインメモリに書き戻すなど、複合操作です。スレッドaがincの値を10に読み取り、変数が変更されず、揮発性ルールをトリガーできないため、この時点でブロックされるとします。スレッドBは、現時点でのIncの価値も読み取っています。メインメモリのINCの値はまだ10であり、自動的に増加し、メインメモリに書き戻されます。これは11です。現時点では、スレッドAの実行ターンです。 10が作業メモリに保存されるため、それ自体が増加し続け、メインメモリに書き戻します。 11は再び書かれています。したがって、2つのスレッドは2回()増加()を実行しましたが、1回しか追加しませんでした。一部の人々は、揮発性がキャッシュラインを無効にしないと言いますか?ただし、スレッドAがスレッドBを読み取って操作を実行する前に、INC値は変更されていないため、スレッドBが読み取られている場合でも10が読み取られます。スレッドBが11をメインメモリに戻した場合、スレッドAのキャッシュラインを無効にしないと言う人もいますか?ただし、スレッドAはすでに読み取り操作を行っています。読み取り操作が完了し、キャッシュラインが無効である場合にのみ、メインメモリ値が読み取られます。したがって、スレッドAは自己障害のみを実行し続けることができます。要約すると、この種の複合操作では、原子関数を維持することはできません。ただし、フラグ値を設定する上記の例では、フラグの読み取り/書き込み操作はシングルステップであるため、原子性を確保できます。原子性を確保するために、同時パケットの下で同期、ロック、アトミックアトミックの動作クラスのみ、つまり自己圧縮(1操作を追加)、自己決定(1操作を削減)、加算操作(数を追加)、および減算操作(1つの数値を削除)して、これらの操作が原子操作であることを確認できます。
揮発性キーワードを使用してアセンブリコードを生成し、揮発性キーワードを使用しないコードを生成すると、揮発性キーワードを持つコードに追加のロックプレフィックス命令があることがわかります。ロックプレフィックス命令は、実際にはメモリバリアに相当します。メモリバリアは次の機能を提供します。1。並べ替えの場合、次の指示をメモリバリア2の前に位置に並べ替えることはできません。このCPUのキャッシュをメモリ** ** 3に作成します。
1。ステータス数量マーク上のフラグと同じように、私はそれについてもう一度言及します:
int a = 0; volatile bool flag = false; public void write(){a = 2; // 1 flag = true; // 2} public void multiply(){if(flag){// 3 int ret = a * a; // 4}}}変数へのこの読み取りおよび書き込み操作は、揮発性としてマークされており、変更がスレッドにすぐに表示されることを保証できます。同期と比較して、ロックは特定の効率改善を持っています。 2。シングルトンモードの実装、典型的なダブルチェックロック(DCL)
クラスSingleton {private volatile static singleton instance = null; private singleton(){} public static singleton getInstance(){if(instance == null){synchronized(singleton.class){if(instance == null)instance = new Singleton(); }} return instance; }}これは怠zyなシングルトンパターンであり、オブジェクトは使用された場合にのみ作成され、初期化操作の指示の並べ替えを避けるために、揮発性がインスタンスに追加されます。
上記は、Javaのインタビュアーが詳細に尋ねるのが大好きな不安定なキーワードの説明に関するこの記事の全体的な内容です。私はそれが誰にでも役立つことを願っています。興味のある友人は、このサイトの他の関連トピックを引き続き参照できます。欠点がある場合は、それを指摘するためにメッセージを残してください。このサイトへのご支援をありがとうございました!