Javaの古典的なインタビューは、揮発性の基礎となる実装原理を質問します

序文

共有変数が揮発性として宣言されている場合、この変数に対する読み取り/書き込み操作は非常に特殊なものになります。

1.揮発性メモリのセマンティクス

1.1揮発性機能

揮発性変数自体には、次の3つの特性があります。

  1. 可視性:つまり、スレッドが揮発性変数として宣言された値を変更すると、新しい値は、変数を読み取りたい他のスレッドにすぐに表示されます。通常の変数ではこれを行うことはできませんが、スレッド間の通常の変数の値の転送は、メインメモリを介して行う必要があります。

  2. 順序付け:いわゆるvolatile変数の順序付けとは、volatileとして宣言された変数のクリティカルセクション内のコードの実行が適切であることを意味します。つまり、命令の順序変更は禁止されています。

  3. 制限付き原子性:揮発性変数の原子性は、同期化の原子性とは異なります。同期化原子性とは、同期化として宣言されたメソッドまたはコードブロックが原子的に実行される限りです。volatileはメソッドやコードブロックを変更しませんが、変数を変更するために使用されます。単一のvolatile変数の読み取り/書き込み操作はアトミックですが、volatile ++と同様の複合操作はアトミックではありません。したがって、揮発性の原子性は限られています。また、マルチスレッド環境では、volatileは原子性を保証しません。

1.2揮発性書き込み/読み取りのメモリセマンティクス

揮発性書き込みのメモリセマンティクス:書き込みスレッドが揮発性変数を書き込むと、JMMはスレッドに対応するローカルメモリの共有変数値をメインメモリに更新します。

揮発性読み取りのメモリセマンティクス:読み取りスレッドが揮発性変数を読み取ると、JMMはスレッドに対応するローカルメモリを無効にし、スレッドはメインメモリから共有変数を読み取ります。

2.揮発性セマンティック実装の原則

揮発性セマンティック実装の原則を紹介する前に、CPUに関連する2つの専門用語を最初に見てみましょう。

  • メモリバリア(メモリバリア):メモリ操作の制限の順序を達成するために使用される一連のプロセッサ命令。

  • キャッシュライン:CPUキャッシュに割り当てることができる最小のストレージユニット。プロセッサがキャッシュラインをいっぱいにすると、キャッシュライン全体がロードされます。

2.1揮発性可視性の原理

揮発性可視性のメモリセマンティクスはどのように実装されますか?コードを見て、コードによって生成されたプロセッサのアセンブリ命令を印刷して(アセンブリ命令の印刷方法については、記事の最後に添付します)、揮発性変数に書き込むときにCPUが何を行うかを見てみましょう。

public class VolatileTest {

    private static volatile VolatileTest instance = null;

    private VolatileTest(){}

    public static VolatileTest getInstance(){
        if(instance == null){
            instance = new VolatileTest();
        }

        return instance;
    }

    public static void main(String[] args) {
        VolatileTest.getInstance();
    }
}

上記のコードは、私たちがよく知っているシングルトンモードコードであり、マルチスレッド環境でのスレッドの安全性を保証できません。このコードの特別な場所は、インスタンス変数インスタンスに揮発性の変更を追加したことです。印刷されたアセンブリ命令を見てみましょう。 :

image.png

 

上のスクリーンショットでは、下線を引いた行の最後にコンパイルコメントがあります。putstaticインスタンス、JVMバイトコード命令を理解しているすべての友人は、putstaticが静的変数の値を設定することを意味することを知っています。これも上記のコードにあります静的変数インスタンスに値を割り当てることです。対応するコード:instance = new VolatileTest();インスタンスは揮発性装飾で追加されるため、インスタンスはgetInstanceメソッドでインスタンス化され、静的変数インスタンスの値を設定すると、揮発性変数も書き込まれます。

上記のアセンブリ命令とバイトコード命令があることを見て、これら2つの命令を混同しますか?ここで、バイトコード命令とアセンブリ命令の違いを示します。

Javaはクロスプラットフォーム言語であることは誰もが知っているので、Javaはどのようにしてこのプラットフォームの独立性を実現していますか?これには、JVMおよびJavaバイトコードファイルを理解する必要があります。ここでは、プログラミング言語をハードウェアで最終的に実行する前にプラットフォーム関連のアセンブリ命令に変換する必要があるという合意が必要です。たとえば、CとC ++はどちらも、ソースコードを直接CPU関連のアセンブリ命令にコンパイルして、 CPUの実行。一連のCPUはシステムアーキテクチャが異なるため、アセンブリ命令も異なります。たとえば、X86アーキテクチャのCPUはX86アセンブリ命令に対応し、armアーキテクチャのCPUはarmアセンブリ命令に対応します。プログラムのソースコードがハードウェア関連の低レベルのアセンブリ命令に直接コンパイルされる場合、プログラムのクロスプラットフォームの性質は大幅に低下しますが、実行パフォーマンスは比較的高くなります。プラットフォームの独立性を実現するために、javaコンパイラjavacは、Javaソースプログラムをプラットフォームに関連するアセンブリ命令に直接コンパイルするのではなく、中間言語、つまりjavaクラスバイトコードファイルにコンパイルします。バイトコードファイルは、その名前が示すように、バイトコード、つまりバイトを1つずつ格納します。Javaバイトコードファイルを開いて調べた友人の中には、バイトコードファイルがバイナリではなく16進数で保存されていることがあります。これは、バイナリが長すぎ、バイトが8ビットで構成されているためです。バイナリ構成。したがって、16進数表記で表現すると、2つの16進数は1バイトを表すことができます。Javaソースコードでコンパイルされたバイトコードファイルは、CPUで直接実行できないので、どのように実行しますか?答えはJVMです。Javaプログラムをさまざまなプラットフォームで実行できるようにするために、Java公式はプラットフォームごとにJava仮想マシンを提供しています。JVMはハードウェアレイヤーで実行され、さまざまなプラットフォームの違いをシールドします。javacによってコンパイルされたバイトコードファイルは、JVMによって均一にロードされ、最終的にハードウェア関連のマシン命令に変換されて、CPUによって実行されます。JVMを介してバイトコードファイルをロードすることはわかっていますが、JVMはバイトコード内の各バイトを、作成したJavaソースコードにどのように関連付けるのか、つまり、JVMが作成したJavaソースをどのように認識するのかについて、まだ疑問があります。コードが対応するクラスファイルの16進セグメント、この16進セグメントの機能、およびそれが実行する機能 16進数の多くは理解できません。そのため、JVMレベルの仕様を定義する必要があります。JVMレベルでは、認識できるいくつかの命令ニーモニックを抽象化します。これらの命令ニーモニックは、Javaバイトコード命令です。

ロック命令は、マルチコアプロセッサで次のイベントをトリガーします。

現在のプロセッサのキャッシュラインのデータをシステムメモリに書き込み、他のCPUのメモリアドレスにキャッシュされているデータを無効にします。

処理速度を向上させるために、プロセッサは通常、メモリと直接通信しませんが、操作を実行する前にまずシステムメモリ内のデータを内部キャッシュに読み取りますが、操作が完了した後、プロセッサがキャッシュされたデータをメモリに書き戻すタイミングはわかりません。ただし、volatileで変更された変数を書き込むと、JVMはロックプレフィックス命令をプロセッサに送信して、この変数のデータをキャッシュラインに書き込んでシステムメモリに戻します。現時点では、システムメモリに書き込まれるだけですが、他のプロセッサのキャッシュラインのデータはまだ古いです。他のプロセッサのキャッシュラインのデータも、新しく書き込まれたシステムメモリのデータである場合、キャッシュ整合性プロトコルを実装する必要があります。つまり、プロセッサが独自のキャッシュラインデータをシステムメモリに書き込んだ後、他の各プロセッサは、バス上に伝搬されたデータをスニッフィングして、キャッシュされたデータが期限切れかどうかを確認します。キャッシュラインに対応するメモリアドレスのデータが変更された後、キャッシュラインキャッシュのデータは無効に設定され、プロセッサがこのデータを変更したい場合、システムメモリから自身にデータを再度読み取ります。キャッシュライン、再キャッシュ。

要約すると、揮発性の可視性の実現は、CPUのロック命令を使用することです。揮発性の機械命令を書き込む前にロックプレフィックスを追加することにより、揮発性の書き込みには次の2つの原則があります。

揮発性を書き込むとき、プロセッサはメインメモリにキャッシュを書き戻します。

1つのプロセッサのキャッシュをメモリに書き戻すと、他のプロセッサのキャッシュが無効になります。

 

2.2揮発性注文の実装原理

揮発性の順序の保証は、命令の順序変更を禁止することによって実現されます。命令の並べ替えにはコンパイラとプロセッサの並べ替えが含まれ、JMMはこれら2つの命令の並べ替えを個別に制限します。

では、命令の並べ替えを禁止するにはどうすればよいですか?答えは、メモリの障壁を追加することです。JMMは、次の4つの状況で揮発性とメモリバリアがあります。

  1. 各揮発性書き込み操作の前にStoreStoreバリアを挿入して、揮発性書き込み操作とその後の書き込み操作の並べ替えを防止します。
  2. 各揮発性書き込み操作の後にStoreLoadバリアを挿入して、揮発性書き込み操作とその後の読み取り操作の並べ替えを防止します。
  3. 各揮発性読み取り操作の後にLoadLoadバリアを挿入して、揮発性読み取り操作とその後の読み取り操作の並べ替えを防止します。
  4. 各揮発性読み取り操作の後にLoadStoreバリアを挿入して、読み取り揮発性操作とその後の書き込み操作の順序変更を防止します。

上記のメモリバリア挿入戦略は非常に保守的です。たとえば、揮発性書き込み操作の後にStoreStoreおよびStoreLoadバリアを追加する必要がありますが、この書き込み揮発性の後に読み取り操作がない場合があるため、理論的にはStoreStoreバリアのみを追加できます。一部のプロセッサはまさにそれを行います。しかし、JMMの保守的なメモリバリア挿入戦略は、揮発性変数がどのプロセッサプラットフォームでも確実に順序付けられるようにします。

3. JSR-133は揮発性メモリのセマンティクスを強化します

JSR-133より前の古いJavaメモリモデルでは、揮発性変数間の操作の順序変更は許可されていませんが、揮発性変数と通常の変数間の順序変更は許可されています。たとえば、メモリバリアの前の操作は揮発性変数を書き込む操作であり、メモリバリアの背後の操作は通常の変数を書き込む操作です。これら2つの書き込み操作は揮発性メモリのセマンティクスを破壊する可能性がありますが、JMMではこれら2つの操作を並べ替えることができます。

JSR-133以降の新しいJavaメモリモデルでは、volatileのメモリセマンティクスが強化されています。揮発性変数と通常の変数間の並べ替えが揮発性メモリのセマンティクスを破壊する可能性がある限り、そのような並べ替えはコンパイラの並べ替え規則とプロセッサメモリバリアアクセスポリシーによって禁止されます。

 

添付ファイル:組み立て手順を印刷するようにアイデアを構成します

ツールキットのダウンロードアドレス:リンク:https
://pan.baidu.com/s/11yRnsOHca5EVRfE9gAuVxA抽出コード:gn8z

図に示すように、ダウンロードしたツールキットを解凍し、jdkインストールディレクトリのjreパスの下のbinディレクトリにコピーします。

image.png

次に、アイデアを構成します:-server -Xcomp -XX:+ UnlockDiagnosticVMOptions -XX:+ PrintAssembly -XX:CompileCommand = compileonly、* class name。メソッド名

JREオプションは、ツールキットに組み込まれたjreパスを選択します。

次の写真は私のアイデアの構成です。

image.png

上記の設定を実行した後、組み立て手順を印刷できます。

公開された81の元の記事 ・の ような29 訪問者20,000以上

おすすめ

転載: blog.csdn.net/breakout_alex/article/details/105465436