Java仮想マシン(効率的な並行性)に関する深い理解

Java仮想マシン(効率的な並行性)に関する深い理解

「効率的な並行性」は、JVM シリーズの最後の記事です。この記事では主に、仮想マシンがマルチスレッドを実装する方法、複数のスレッドがデータを共有して競合する方法、およびデータの共有と競合によって発生する問題と解決策について紹介します。

[[271680]]

1. Java メモリ モデルとスレッド

コンピュータが複数のタスクを同時に実行できるようになった理由は、プロセッサのパフォーマンスが向上したからだけではなく、より重要なのは、コンピュータの計算速度とストレージおよび通信サブシステムの速度の差が大きすぎて、ディスク I/O、ネットワーク通信、データベース アクセスに多くの時間が費やされるためです。プロセッサがリソースを浪費したり、他のリソースを待つ時間を無駄にしたりしないようにするには、コンピュータが複数のタスクを同時に実行してプロセッサのパフォーマンスを最大限に活用できるようにする必要があります。これは、サーバーの高い同時実行要件を満たすためでもあります。 Java メモリ モデルの設計とスレッドの存在は、まさにより優れた、より効率的なマルチタスクを実現するためのものです。

1. ハードウェアの一貫性と効率性

コンピュータタスクの大部分はプロセッサの計算だけでは完了できません。プロセッサは、データの読み取り、結果の保存など、少なくともメモリと対話する必要があります。この I/O 操作を排除することは困難です。コンピュータのストレージ デバイスの計算速度とプロセッサの計算速度の間には数桁の差があるため、コンピュータは、読み取りおよび書き込み速度がプロセッサの計算速度にできるだけ近いキャッシュ メモリの層を追加して、メモリとプロセッサ間のバッファとして機能する必要があります。計算に必要なデータは、計算を迅速に実行できるようにキャッシュ メモリにコピーされ、計算が完了するとキャッシュ メモリからメモリに同期されるため、プロセッサは低速のメモリの読み取りおよび書き込みを待つ必要がありません。

キャッシュベースのストレージ相互作用は、プロセッサとメモリ間の速度の競合を解決しますが、キャッシュの一貫性という新しい問題が発生するため、コンピュータ システムの複雑さが増します。マルチプロセッサでは、各プロセッサに独自のキャッシュがありますが、すべてのプロセッサが同じメインメモリを共有します。複数のプロセッサの計算タスクが同じメインメモリ領域に関係する場合、それらのキャッシュ データが不整合になる可能性があります。一貫性の問題を解決するために、各プロセッサはキャッシュにアクセスするときに特定のプロトコルに従い、読み取りおよび書き込み時にプロトコルに従って操作を実行する必要があります。

キャッシュを増やすことに加えて、プロセッサの内部計算ユニットを可能な限り完全に活用するために、プロセッサは入力コードをアウトオブオーダー実行用に最適化する場合があります。プロセッサは、計算後にアウトオブオーダー実行の結果を再編成して、結果が順次実行の結果と一致するようにしますが、プログラム内の各ステートメントの計算順序が入力コード内の順序と一致することを保証するものではありません。したがって、別の計算タスクの中間結果に依存する計算タスクがある場合、その順序はコードの順序では保証されません。プロセッサのカオス実行最適化と同様に、JIT コンパイラにも同様の命令並べ替え最適化があります。

2. Javaメモリモデル

Java 仮想マシン仕様では、Java メモリ モデルが定義されています。このモデルは、さまざまなハードウェアとオペレーティング システム間のメモリ アクセスの違いをマスクするために使用されるため、Java プログラムはさまざまなプラットフォームで一貫したメモリ アクセス効果を実現できます。 C/C++ などの言語は、物理ハードウェアとオペレーティング システムのメモリ モデルを直接使用します。したがって、異なるプラットフォーム上のメモリ モデルが異なるため、異なるプラットフォーム用にコードを記述する必要があります。

メインメモリとワーキングメモリ

Java メモリ モデルの主な目的は、プログラム内の各変数のアクセス ルール、つまり仮想マシン内のメモリへの変数の格納とメモリからの変数の読み取りに関する低レベルの詳細を定義することです。ここで言及されている変数は、Java コード内の変数とは異なります。これらには、インスタンス フィールド、静的フィールド、および配列オブジェクトを構成する要素が含まれますが、変数とメソッド パラメーターは含まれません。後者はスレッド プライベートであり、共有されないためです。より優れた実行パフォーマンスを実現するために、Java メモリ モデルでは、実行エンジンがプロセッサの特定のレジスタまたはキャッシュを使用してメイン メモリと対話することを制限せず、JIT コンパイラがコード実行順序などの最適化手段を実行することも制限しません。

Java メモリ モデルでは、すべての変数がメイン メモリに格納されることが規定されています。各スレッドには独自の作業メモリがあります。スレッドの作業メモリには、スレッドが使用する変数のメインメモリのコピーが格納されます。変数に対するスレッドのすべての操作は作業メモリ内で実行する必要があり、メインメモリを直接読み書きすることはできません。スレッド間の変数値の転送は、メインメモリを介して完了する必要があります。

メモリ間操作

メインメモリとワーキングメモリ間の具体的な相互作用プロトコル、つまり変数がメインメモリからワーキングメモリにコピーされる方法や、ワーキングメモリからメインメモリに同期される方法などの詳細については、Java メモリモデルでは次の 8 つの操作が定義されています。仮想マシンを実装する場合、次の各操作がアトミックかつ不可分であることを保証する必要があります。

これらの 8 つの操作は、ロック、ロック解除、読み取り、ロード、使用、割り当て、保存、書き込みです。

揮発性変数の特別なルール

Volatile は、Java 仮想マシンによって提供される最も軽量な同期メカニズムです。変数が揮発性として定義されると、次の 2 つの特性を持ちます。

1 つ目は、この変数がすべてのスレッドで表示されるようにすることです。ここでの「可視性」とは、スレッドがこの変数の値を変更すると、新しい値が他のスレッドにすぐに認識されることを意味します。通常の変数ではこれを行うことができず、メインメモリを介してスレッド間でデータを渡す必要があります。たとえば、スレッド A は共通変数の値を変更し、それをメインメモリに書き戻します。スレッド A が書き戻しを完了すると、別のスレッド B がメイン メモリから読み取りと書き込みを行います。そうして初めて、新しい変数値がスレッド B に表示されるようになります。

2 つ目は、命令の並べ替えの最適化を禁止することです。通常の変数は、メソッドの実行中に代入結果に依存するすべての場所で正しい結果が得られることを保証するだけで、変数代入操作の順序がプログラム コード内の実行順序と一致することを保証することはできません。これはスレッドのメソッドの実行中には認識できないため、Java メモリ モデルで説明されている「スレッド内のシリアル セマンティクス」と呼ばれるものです。

long および double 変数の特別なルール

Java メモリ モデルでは、ロック、ロック解除、読み取り、ロード、割り当て、使用、保存、書き込みの 8 つの操作がすべてアトミックである必要があります。ただし、64 ビット データ型 (long および double) の場合、モデルでは比較的緩いルールが具体的に定義されています。つまり、仮想マシンは、volatile によって変更されない 64 ビット データの読み取り操作と書き込み操作を 2 つの 32 ビット操作に分割できます。つまり、仮想マシン実装では、64 ビット データ型のロード、ストア、読み取り、書き込みの 4 つの操作のアトミック性を保証しないことを選択できます。これは、long と double の間のいわゆる非原子的合意です。

複数のスレッドが volatile として宣言されていない long 型または double 型の変数を共有し、同時に読み取りと変更を行うと、一部のスレッドが誤った値を読み取る可能性があります。幸いなことに、このような状況は非常に稀であり、主流の商用仮想マシンでも long と double の操作はアトミックであるとみなされるため、実際の開発では volatile を使用して変数を変更する必要はありません。

原子性、可視性、秩序

Java メモリ モデルは、同時プロセスにおける原子性、可視性、順序という 3 つの特性を処理する方法を中心に構築されています。

原子性

Java メモリ モデルは、読み取り、ロード、割り当て、使用、保存、書き込みなどのアトミック変数操作を直接保証します。基本データ型のアクセスと読み取りはアトミックであると大まかに想定できます。アプリケーション シナリオでより広範囲の原子性保証が必要な場合、Java メモリ モデルでは、この要件を満たすロックおよびロック解除操作も提供されます。仮想マシンはロックおよびロック解除操作をユーザーに直接公開しませんが、これら 2 つの操作を暗黙的に使用するための高レベルのバイトコード命令 monitorenter および monitorexit を提供します。これら 2 つのバイトコード命令は、Java コードでは synchronized キーワードとして反映されます。したがって、 synchronized によって変更されたメソッドまたはコード ブロック間の操作はアトミックです。

可視性

可視性とは、あるスレッドが共有変数の値を変更すると、他のスレッドがその変更をすぐに知ることができることを意味します。 Java メモリ モデルは、変数が変更された後に新しい値をメイン メモリに同期し、変数が読み取られる前にメイン メモリから変数値を更新することで可視性を実現します。この方法では、伝送媒体としてメインメモリを使用します。これは通常の変数と揮発性変数の両方に当てはまります。通常の変数と揮発性変数の違いは、揮発性変数のルールにより、新しい値がメイン メモリにすぐに同期され、使用の直前にメイン メモリから更新されることが保証される点です。したがって、 volatile はマルチスレッド変数の可視性を保証しますが、通常の変数はこれを保証できないと言えます。 volatile に加えて、Java には synchronized と final という 2 つのキーワードがあります。同期ブロックの可視性は、「変数のロック解除操作を実行する前に、変数をメイン メモリに同期させる必要がある (保存および書き込み操作を実行する)」という規則によって実現されます。 final の可視性とは、「final によって変更されたフィールドがコンストラクターで初期化され、コンストラクターが「this」への参照を渡さない場合、final フィールドの値が他のスレッドで参照できる」ことを意味します。

注文

Java プログラムの自然な秩序性は、次のように要約できます。このスレッド内では、すべての操作が順序どおりに実行されます。あるスレッドが別のスレッドを監視する場合、すべての操作は順序どおりに行われません。文の前半は「スレッド内のシリアルセマンティクス」について言及しており、後半は「命令の並べ替え」現象と「作業メモリとメインメモリ間の同期の遅延」現象について言及しています。 Java 言語には、スレッド間の操作の秩序性を保証するために、 volatile と synchronized という 2 つのキーワードが用意されています。 volatile キーワード自体には命令の並べ替えを禁止するセマンティクスが含まれていますが、 synchronized は「同時に 1 つのスレッドだけが変数をロックできる」というルールによって実現されます。このルールは、同じロックを保持する 2 つの同期ブロックが連続してのみ入力できることを決定します。

優先権の原則

Java メモリ モデル内のすべての順序が volatile と synchronized のみによって保証される場合、一部の操作が面倒になります。しかし、Java の並行コードを書くときには、このことは感じません。これは、Java 言語に「事前発生」原則があるためです。この原則は非常に重要です。これは、データの競合があるかどうか、およびスレッドが安全かどうかを判断するための主な基準です。この原則に依拠することで、並行環境における 2 つの操作間で競合が発生する可能性があるかどうかという問題をすべて、いくつかのルールを通じて解決できます。

Happens before は、Java メモリ モデルで定義された 2 つの操作間の部分的な順序関係です。操作 A が操作 B の前に発生する場合、実際には、操作 B が発生する前に、操作 A の影響を操作 B で観察できることを意味します。「影響」には、メモリ内の共有変数の値の変更、メッセージの送信、メソッドの呼び出しなどが含まれます。

Java メモリ モデルでは、いくつかの自然な事前発生関係が存在します。これらの happen-before 関係は、同期化機能の支援なしに存在し、コード内で直接使用できます。 2 つの操作間の関係がこのリストになく、次の規則から推測できない場合、順序の保証はなく、仮想マシンは自由に順序を変更できます。

  • プログラム順序ルール: スレッド内では、プログラム コードの順序に従って、前に記述されたコードが後ろに記述されたコードよりも先に実行されます。正確に言うと、分岐やループなどの構造を考慮する必要があるため、プログラム コードの順序ではなく制御フローの順序になります。
  • ロック ルールを監視します。ロック解除操作は、同じロックに対する後続のロック操作の前に発生します。
  • 揮発性変数のルール: 揮発性変数への書き込み操作は、その変数への後続の読み取り操作の前に発生します。この原則を理解すれば、DCL シングルトン パターンでインスタンス オブジェクトを識別するために volatile が使用される理由を理解できます。
  • スレッドの起動ルール: スレッドの start() メソッドは、このスレッドの他のすべてのアクションの前に実行されます。
  • スレッド終了ルール: スレッド内のすべての操作は、このスレッドの終了が検出される前に実行されます。
  • プログラム中断ルール: 中断されたスレッドのコードが中断時間の発生を検出する前に、スレッドの interrupt() の呼び出しが発生します。
  • オブジェクト終了ルール: オブジェクトの初期化は finalize() の開始時に完了します。
  • 推移性: 操作 A が B より前に発生し、B が C より前に発生する場合、A は C より前に発生します。

3. Javaとスレッド

Java での並行性について話すとき、それは通常、マルチスレッドに関連しています。このセクションでは、仮想マシンでの Java スレッドの実装について説明します。

スレッド実装

主流のオペレーティング システムはすべてスレッド実装を提供しており、Java 言語はさまざまなハードウェアおよびオペレーティング システム プラットフォーム上でスレッド操作の統一された処理を提供します。 start() を実行し、まだ終了していない Thread クラスの各インスタンスは、スレッドを表します。 Thread クラスのすべての主要メソッドはネイティブです。 Java API では、ネイティブ メソッドは、多くの場合、このメソッドがプラットフォームに依存しない手段を使用していないか、またはプラットフォームに依存しない手段を使用して実装できないことを意味します (もちろん、ネイティブ メソッドは実行効率のためにも使用されることがありますが、通常、最も効率的な手段はプラットフォーム関連の手段です)。

スレッドを実装するには、カーネル スレッドを使用する方法、ユーザー スレッドを使用する方法、ユーザー スレッドと軽量プロセスを組み合わせて使用​​する方法の 3 つの主な方法があります。

Javaスレッド実装

JDK 1.2 より前では、Java スレッドは「グリーン スレッド」と呼ばれるユーザー スレッドに基づいて実装されていました。 JDK 1.2 では、スレッド モデルはオペレーティング システムのネイティブ スレッド モデルに基づくモデルに置き換えられます。したがって、現在の JDK バージョンでは、オペレーティング システムでサポートされているスレッド モデルによって、Java 仮想マシンのスレッドのマッピング方法が大きく決まります。この点については、異なるプラットフォーム間で合意に達する方法はなく、仮想マシン仕様では、Java スレッドが実装するためにどのスレッド モデルを使用する必要があるかは指定されていません。スレッド モデルは、スレッドの同時実行スケールと操作コストにのみ影響します。これらの違いは、Java プログラムのコーディングおよび実行プロセスには影響しません。

4. Java スレッドのスケジューリング

スレッド スケジューリングとは、システムがスレッドにプロセッサ使用権を割り当てるプロセスを指します。主なスケジューリング方法には、協調スレッド スケジューリングとプリエンプティブ スレッド スケジューリングの 2 つがあります。

協調スレッドスケジューリング

協調スケジューリングを使用するマルチスレッド システムの場合、スレッドの実行時間はスレッド自体によって制御されます。スレッドが作業を完了したら、別のスレッドに切り替えるようにシステムに積極的に通知する必要があります。協調型マルチスレッドの最大の利点は、実装が簡単なことです。さらに、スレッドは自身の作業が完了するまでスレッドを切り替えないため、切り替え操作はスレッド自体に認識され、スレッド同期の問題は発生しません。しかし、その欠点も明らかです。スレッドの実行時間は制御できず、スレッドが誤ってプログラムされ、オペレーティング システムにスレッドを切り替えるように指示されない場合でも、プログラムは永久にそこでブロックされます。昔、Windows 3.x システムでは、プロセスを実装するために協調型マルチタスクが使用されていましたが、これは非常に不安定でした。プロセスが CPU 実行時間を放棄しないことを主張した場合、システム全体がクラッシュする可能性があります。

プリエンプティブスレッドスケジューリング

プリエンプティブ スケジューリングを使用するマルチスレッド システムの場合、各スレッドにはシステムによって実行時間が割り当てられ、スレッドの切り替えはスレッド自体によって決定されません。このスレッド スケジューリングの実装方法では、スレッドの実行はシステムによって制御可能であり、1 つのスレッドがプロセス全体をブロックするという問題は発生しません。 Java で使用されるスレッド スケジューリング メソッドはプリエンプティブです。上記の Windows 3.x の例と比較すると、Windows 9x/NT カーネルはプリエンプティブ モードを使用してマルチプロセスを実装します。プロセスに問題がある場合は、タスク マネージャーを使用して、システム クラッシュを引き起こすことなくプロセスを「強制終了」することもできます。

5. 状態遷移

Java 言語では 5 つのスレッド状態が定義されています。どの時点でも、スレッドは次のいずれかの状態のみを持ちます。

  • 新規: 作成後まだ開始されていないスレッドはこの状態です。
  • 実行可能: 実行可能には、オペレーティング システムのスレッド状態の実行中と準備完了が含まれます。つまり、この状態のスレッドは実行中であるか、CPU が実行時間を割り当てるのを待機している可能性があります。
  • 無期限待機 (待機中): この状態のスレッドには CPU 実行時間が割り当てられず、他のスレッドによって明示的に起動されるまで待機する必要があります。次の 3 つの方法は、スレッドを無期限の待機状態にします。
    • TimeOut パラメータを設定せずに Object.wait() を実行します。
    • TimeOut パラメータを設定せずに Thread.join() を実行します。
    • LockSupport.park() 。
  • 時間指定待機: この状態のスレッドには CPU 実行時間は割り当てられませんが、他のスレッドによって明示的に起動されるまで待機する必要はありません。一定時間が経過すると、システムによって自動的に起動されます。次のメソッドは、スレッドを時間制限付きの待機状態にします。
    • スレッドをスリープ状態にします。
    • TimeOut パラメータが設定された Object.wait();
    • TimeOut パラメータを設定した Thread.join()。
    • LockSupport.parkNanos();
    • LockSupport.parkUntil()。
  • ブロック済み: スレッドはブロックされています。 「ブロック状態」と「待機状態」の違いは、「ブロック状態」は排他ロックの取得を待機していることです。このイベントは、別のスレッドがロックを放棄したときに発生します。 「待機状態」は、一定時間待機中、またはウェイクアップ アクションの送信中です。プログラムが同期領域に入るのを待機しているときに、スレッドはこの状態になります。
  • 終了: スレッドの実行が終了しました。

上記の 5 つの状態は、特定のイベントが発生すると、以下に示すように切り替わります。

2. スレッドの安全性とロックの最適化

この記事のテーマは効率的な並行性ですが、効率性の前提はまず並行性の正確性と安全性を確保することなので、このセクションではスレッドの並行性の安全性を確保する方法から始めます。

1. Java スレッドセーフティ

では、スレッドセーフティとは何でしょうか?簡単に言えば、複数のスレッドが同じメモリ領域に対して操作を行う場合、メモリ値の変化は予測可能であり、同じメモリ領域に対する複数のスレッドの操作やアクセスによってメモリに格納された値に制御不能な問題が発生することはない、ということになります。

Java言語におけるスレッドセーフティ

スレッド セーフティを二者択一の概念 (スレッドが絶対に安全であるか、スレッドが絶対に安全でないか) として定義しない場合は、スレッド セーフティを強いレベルから弱いレベルまで次の 5 つのレベルに分けることができます。

  • 不変;
  • 完全にスレッドセーフです。
  • 比較的スレッドセーフです。
  • スレッド互換。
  • スレッド反対。

スレッドセーフな実装方法

スレッドの安全性はコーディングの実装と大きく関係していますが、仮想マシンによって提供される同期とロックのメカニズムも非常に重要な役割を果たします。次に、仮想マシンレベルでスレッドの安全性がどのように確保されるかを見てみましょう。

同期ミューテックス

相互排他同期は、同時実行の正確性を保証する一般的な手段です。同期とは、複数のスレッドが同時に共有データにアクセスする場合に、共有データが一度に 1 つのスレッドによってのみ使用されるようにすることを意味します。相互排除は同期を実現するための手段です。 Java における最も基本的な相互排他同期方法は synchronized キーワードです。コンパイル後、 synchronized キーワードは、 synchronized ブロックの前後にそれぞれ monitorenter と monitorexit の 2 つのバイトコード命令を形成します。どちらのバイトコードも、ロックおよびロック解除するオブジェクトを示す参照型パラメータを必要とします。 Java プログラム内の synchronized がオブジェクト パラメータを明示的に指定する場合、それはこのオブジェクトへの参照です。そうでない場合は、 synchronized がインスタンス メソッドを変更するかクラス メソッドを変更するかに応じて、対応するオブジェクト インスタンスまたはクラス オブジェクトがロック オブジェクトとして取得されます。

仮想マシン仕様の要件に従って、monitorenter 命令を実行すると、まずオブジェクトのロック取得が試行されます。オブジェクトがロックされていない場合、または現在のスレッドがすでにそのオブジェクトのロックを持っている場合、ロック カウンターは 1 増加します。同様に、monitorexit 命令が実行されるとロック カウンタが 1 減算され、ロック カウンタが 0 になるとロックが解除されます。ロック オブジェクトの取得に失敗した場合、現在のスレッドはブロックされ、別のスレッドによってオブジェクト ロックが解放されるまで待機します。

もう一つの注意点は、同期ブロックは、入力したスレッドが実行される前に、他のスレッドの入力をブロックすることです。 Java スレッドはオペレーティング システムのネイティブ スレッドにマップされるため、スレッドをブロックまたは起動する場合は、それを完了するためにオペレーティング システムの支援が必要であり、そのためにはユーザー状態からカーネル状態への移行が必要になります。スレッドの状態遷移には大量のプロセッサ時間が消費されます。単純な同期ブロック (synchronized によって変更された getter() メソッドや setter() メソッドなど) の場合、状態遷移によって消費される時間は、ユーザー コードによって消費される時間よりも長くなる可能性があります。したがって、synchronized は Java では重い操作なので、必要な場合にのみ使用する必要があります。もちろん、仮想マシン自体も、ユーザー状態からカーネル状態への頻繁な変換を回避するために、オペレーティング システムがスレッドをブロックする前にスピン待機プロセスを追加するなど、対応する最適化を行います。これについては、ロックの最適化を紹介するときに詳しく説明します。

非ブロッキング同期

排他同期の最大の問題は、スレッドのブロックと起動によって引き起こされるパフォーマンスの問題であるため、このタイプの同期はブロッキング同期とも呼ばれます。問題への対処方法の観点から見ると、排他同期は悲観的な並行性戦略であり、正しい同期対策(ロックなど)を講じない限り、必ず問題が発生すると考えています。共有データの競合が発生するかどうかに関係なく、共有データはロックする必要があります (もちろん、仮想マシンは不要なロックも最適化します)。ハードウェア命令セットの開発により、競合チェックに基づく楽観的同時実行戦略という別の選択肢が生まれました。簡単に言えば、まず操作が実行されます。他のスレッドとの競合がない場合、操作は成功します。共有データに関して他のスレッドとの競合があり、競合が発生した場合は、他の是正措置が講じられます。この楽観的同時実行戦略の多くの実装ではスレッドを中断する必要がないため、この同期操作は非ブロッキング同期と呼ばれます。

先ほどハードウェア命令セットの開発が必要であると述べたのは、操作と競合検出の 2 つのステップをアトミックにする必要があるためです。

この原子性はどのように保証されるのでしょうか?原子性を保証するために相互排他同期を使用すると、その意味が失われます。したがって、このタスクを完了するにはハードウェアに頼るしかなく、意味的に複数の操作を必要とする動作が 1 つのプロセッサ命令だけで完了できることが保証されます。このタイプのよく使用される指示は次のとおりです。

  • テスト&セット
  • フェッチとインクリメント
  • スワップ
  • 比較スワップ (CAS)
  • ロードリンク/ストア条件付き (LL/SC)

最初の 3 つは以前のプロセッサ命令セットに既に存在し、最後の 2 つは新しいものです。

CAS 命令には、メモリ位置 (Java では変数のメモリ アドレスとして簡単に理解でき、V で表されます)、以前の期待値 (A で表されます)、および新しい値 (B で表されます) という 3 つのオペランドが必要です。 CAS が命令を実行すると、プロセッサは、V が古い予想値 A を満たす場合にのみ、V の値を新しい値 B に更新します。それ以外の場合は更新を実行しません。ただし、V の値が更新されたかどうかに関係なく、V の古い値が返されます。上記の処理プロセスはアトミック操作です。

CAS 操作は、JDK 1.5 以降の Java プログラムで使用できます。この操作は、sun.misc.Unsafe クラスの compareAndSwapInt() や compareAndSwapLong() などのいくつかのメソッドによって提供されます。仮想マシンは、内部的にこれらのメソッドに対して特別な処理を行います。ジャストインタイムコンパイルの結果は、プラットフォーム関連のプロセッサ CAS 命令です。メソッド呼び出し処理が存在しない、または無条件にインライン化されているとみなすことができます。

Unsafe クラスはユーザー プログラムが呼び出すために提供されているクラスではないため、リフレクションを使用しない場合は、compareAndSet() や getAndIncrement() などのメソッドが Unsafe クラスの CAS 操作を使用する JUC パッケージの整数アトミック クラスなど、他の Java API を介して間接的にのみ使用できます。

CAS は見た目は美しいものの、この操作では相互排他同期のすべてのシナリオをカバーできず、セマンティクスの面では CAS は完璧ではありません。変数 V が最初に読み取られたときに A であり、割り当てられる準備ができたときにまだ A である場合、その値は他のスレッドによって変更されていないと言えますか?この期間中に B に変更され、その後 A に戻された場合、CAS 操作では変更されていないと認識されます。この脆弱性は、CAS 操作の「ABA」問題と呼ばれます。

「ABA」問題を解決するために、JUC パッケージは、変数値のバージョンを制御することで CAS の正確性を確保できる、マークされたアトミック参照クラス AtomicStamoedReference を提供します。しかし、このクラスは比較的役に立たない。ほとんどの場合、ABA 問題はプログラムの並行性の正確性に影響を与えません。 ABA 問題を解決する必要がある場合、従来の相互排他同期を使用する方がアトミック クラスよりも効率的である可能性があります。

同期ソリューションなし

スレッドの安全性を確保するために同期は必要ありません。メソッドに共有データが含まれない場合は、当然、同期対策は必要ありません。したがって、以下で説明する再入可能コードやスレッドローカル ストレージなど、一部のコードは本質的にスレッドセーフです。

再入可能コード: 純粋コードとも呼ばれ、コードの実行中にいつでも中断して、代わりにコードの反対側で実行することができます (それ自体への再帰呼び出しを含む)。制御を取り戻すと、元のプログラムにはエラーがなくなります。再入可能コードには、ヒープに格納されたデータや共通のシステム リソースに依存しない、使用されるすべての状態変数がパラメーターによって渡される、非再入可能メソッドが呼び出されないなどの共通の特性があります。メソッドの戻り結果が予測可能で、同じ入力に対して同じ出力を返す場合、それは再入可能コードであり、もちろんスレッドセーフです。

スレッド ローカル ストレージ: これは、データがスレッドに固有であり、スレッド ローカル ストレージを実装するために ThreadLocal が使用されることを意味します。

2. ロックの最適化

HotSpot 仮想マシン開発チームは、スピン ロックやアダプティブ スピン ロック、ロックの除去、ロックの粗大化、軽量ロック、バイアス ロックなど、さまざまなロックの最適化を実装するために多大な労力を費やしてきました。

スピンロックとアダプティブスピン

スピン ロック 相互排他同期について説明した際に、相互排他同期がパフォーマンスに与える最も大きな影響はブロッキングの実装であると述べました。スレッドの一時停止と再開の両方には、ユーザー状態からカーネル状態への移行が伴います。この状態遷移はシステムの同時パフォーマンスに大きな負担をかけます。ただし、ほとんどのシナリオでは、共有データのロック状態は短時間しか続かないため、この短時間にスレッドを一時停止して再開することはコスト効率がよくありません。物理マシンに複数のプロセッサがあり、2 つ以上のスレッドを同時に並行して処理できる場合は、後でロックを要求するスレッドを「しばらく待機」させることができますが、プロセッサの実行時間は放棄せず、ロックを保持しているスレッドがすぐにロックを解除するかどうかを確認します。スレッドを待機させるには、スピン ロックと呼ばれるアイドリング ループ (スピン) を実行するだけで済みます。

スピン待機はスレッド切り替えのオーバーヘッドを回避しますが、プロセッサ時間を消費します。ロックが短時間占有されている場合、スピン待機の効果は当然ながら非常に良好です。逆に、ロックが長時間占有されている場合、スピンしているスレッドはプロセッサ リソースを無駄に消費し、最適化の効果が低下します。したがって、スピン待機には制限が必要ですが、この制限に固定値を設定することは最善の選択ではありません。そのため、仮想マシン開発チームは、スピン待機時間が固定ではなく、同じロックの前回のスピン時間とロック所有者の状態によって決定されるように、適応型スピン ロックを設計しました。同じロック オブジェクトで、スピン ウェイトがロックを正常に取得し、ロックを保持しているスレッドが実行中の場合、仮想マシンはこのスピンも成功する可能性があると判断し、スピン ウェイト時間を延長します。ロックのスピン待機がほとんど成功しない場合は、将来ロックが取得されたときにスピン待機は中止されます。アダプティブ スピニングでは、プログラムが実行され、パフォーマンス監視情報が継続的に改善されるにつれて、仮想マシンによるプログラム ロック状態の予測がより正確になります。

ロックの除去

ジャストインタイムコンパイラが実行されている場合、一部のコードで同期する必要がありますが、共有データ競合の可能性がないことが検出された場合、ロックは排除されます。排除の主な根拠は、エスケープ分析のデータサポートからのものです。コードの一部では、ヒープ上のすべてのデータが他のスレッドで逃げ出さずにアクセスすることはないと判断された場合、それらはスタック上のデータとして扱われ、スレッドプリブと見なされ、同期ロックは当然不要です。

粗大化をロックします

コーディングの場合、同期ブロックの範囲を最小限に制限することを常に推奨し、共有データの実際の範囲内でのみ同期することをお勧めします。これは、必要な同期操作の数を最小限に抑えるためであり、競合がある場合、ロックを待つスレッドはできるだけ早くロックを取得できます。通常、これは正しいですが、一連の連続した操作が同じオブジェクトを繰り返しロックしてロック解除する場合、またはロック操作がループで発生した場合でも、スレッドの競合がなくても、頻繁な相互排除同期は不必要なパフォーマンス損失を引き起こします。たとえば、ループ本体でロックが発生した場合、仮想マシンがこの状況に遭遇すると、ループ本体の外側にロック同期の範囲が拡張され、1回だけロックする必要があります。これはロックコアンスです。

ここでは、軽量のロックとバイアスロックを導入しません。興味がある場合は、メッセージを残してください。別の記事で紹介します。

<<:  詳細説明: Linuxネットワーク仮想化技術

>>:  5G時代のクラウドコンピューティング開発における5つの新たなトレンド

推薦する

クラウド市場の7つのトレンドとITへの影響

クラウドコンピューティング市場は成熟しました。クラウド インフラストラクチャのランキングは比較的安定...

国内大学フォーラムの現状と収益モデルについて簡単に解説

国内の主要大学のフォーラムを見てみると、いくつかの古いフォーラムを除いて、うまく発展しているものはほ...

Ainameのドメイン名登録動向:2月下旬に1,709のドメイン名が追加

中国IDCレビューネットワークは3月14日に次のように報告した。WebHosting.infoが提供...

代替ソリューションは不安定なウェブサイトの組み込みの鍵となる

今日のインターネットはもはや受動的なインターネットではなく、ますます能動的なインターネットになりつつ...

vapornode-$7/KVM/2g メモリ/60g SSD/2T トラフィック/Phoenix

vapornode の最新の格安 VPS プロモーション、フェニックス データ センター、1000M...

インターネットプロモーションはインターネットだけに頼ることはできません。オンラインとオフラインを組み合わせてみてはいかがでしょうか。

SEO 担当者として、このタイトルを見て、ナンセンスだと思いますか? ネットワーク プロモーションは...

オンラインライターの現状調査:収入があるのはわずか10%

東方新聞は4月18日に次のように報じた。少し前に、25歳の女性ネットライター「清軍」が病気で突然亡く...

分散フレームワークを読んで、知っておくべきNIOの基本的な知識

[[397718]]この記事はWeChatの公開アカウント「KK Architect」から転載したも...

Vultr CN2 が夜間にフリーズします。Vultr CN2 は夜間に遅すぎます。どうすればいいですか?

多くの人 (初心者) は、Vultr CN2 が夜間にフリーズし、夜間は速度が遅すぎると言っています...

Baidu が「ユーザー エクスペリエンス」を決定する方法

Baidu 検索エンジンは、ウェブサイトに「ユーザー エクスペリエンス」があるかどうかをどのような観...

domain.com - ホスティングが 40% オフ

domain.com では、スペース購入時に 40% 割引となる大きな割引コード 40HOSTING...

SEO トレーニングには危険がいっぱいです。その真の意味はいつ回復できるのでしょうか?

SEO研修がいつから人気になったのかは分かりませんが、ローカルSEO研修からインターネットSEO研修...

ウェブサイトのURL標準化がSEOに及ぼす影響について

サイト全体の最適化については、あまり多くを語る必要はありません。製品マーケティングにおけるその重要性...

SAPは製薬業界のデジタル変革とスマートライフサイエンス企業の構築を支援します

8月27日〜28日、中国医薬・医療情報化連盟(CIAPH)主催の第8回医薬・医療産業情報化サミットフ...

2021 年に成功するクラウドベースの SaaS アプリケーションを開発する方法

クラウドベースの SaaS は、企業がサブスクリプション ベースでソフトウェア ソリューションを提供...