ホームページ  >  記事  >  Java  >  Java スレッド学習のための同時プログラミングのナレッジ ポイント

Java スレッド学習のための同時プログラミングのナレッジ ポイント

WBOY
WBOY転載
2022-06-22 12:14:281511ブラウズ

この記事では、java に関する関連知識を提供します。主に、Java のメモリ モデル、volatile の詳細な説明、synchronized の実装原理など、同時プログラミングに関連する問題を整理しています。一緒に見てください、皆さんのお役に立てれば幸いです。

Java スレッド学習のための同時プログラミングのナレッジ ポイント

推奨学習: 「java ビデオ チュートリアル

1. JMM の基礎 - コンピューターの原理

Java メモリモデルは Java Memory Model (略して JMM) です。 JMM は、Java 仮想マシン (JVM) がコンピュータ メモリ (RAM) 内でどのように動作するかを定義します。 JVM はコンピュータ全体の仮想モデルであるため、JMM は JVM に関連付けられます。 Java1.5版ではリファクタリングが行われており、現在のJavaは引き続きJava1.5版を使用している。 Jmm が遭遇する問題は、現代のコンピューターで遭遇する問題と似ています。
物理コンピュータの同時実行性の問題. 物理マシンが遭遇する同時実行性の問題は、仮想マシンの状況と多くの類似点があります。物理マシンの同時実行性処理スキームも、仮想マシンの実装においてかなりの参考になります。
「Google All-Engineering Conference での Jeff Dean のレポート」に基づくと、

Java スレッド学習のための同時プログラミングのナレッジ ポイント

コンピューターが通常の基本操作を実行するときに、必要な応答時間は次のとおりです。異なります。

次のケースは説明のみを目的としており、実際の状況を表すものではありません。

1M の int 型データを CPU がメモリから読み出して蓄積すると、どれくらいの時間がかかりますか?
簡単な計算を行います。1M データの場合、Java の int 型は 32 ビットと 4 バイトです。合計 1024*1024/4 = 262144 個の整数があります。CPU の計算時間は次のとおりです: 262144 0.6 = 157,286 ナノ秒であり、メモリから 1M データを読み取るには 250,000 ナノ秒かかることがわかっています。両者の間にはギャップがありますが (もちろん、このギャップは小さくありません。10 万ナノ秒は、CPU がほぼ 2 つのデータを実行するのに十分な時間です) 10万命令)、それでも桁違いです。ただし、キャッシュ メカニズムがないと、各数値をメモリから読み取る必要があることを意味します。この場合、CPU がメモリを 1 回読み取るのに 100 ナノ秒かかり、262144 個の整数がメモリから CPU に読み取られます。計算時間は 262144100 250000 = 26 464 400 ナノ秒かかり、桁違いに違います。

そして実際には、ほとんどのコンピューティング タスクは、プロセッサによる「計算」だけでは完了できません。プロセッサは、コンピューティング データの読み取り、コンピューティング結果の保存など、少なくともメモリと対話する必要があります。 O 操作を排除することは基本的に不可能です (すべてのコンピューティング タスクを完了するためにレジスタのみに依存することはできません)。初期のコンピュータではCPUとメモリの速度はほぼ同じでしたが、現代のコンピュータではCPUの命令速度がメモリのアクセス速度をはるかに上回り、コンピュータの記憶装置との間には数桁の差があるため、最新のコンピュータでは、コンピュータ システムは、メモリとプロセッサの間のバッファとして機能するために、プロセッサの動作速度に可能な限り近い読み取りおよび書き込み速度のキャッシュ層 (キャッシュ) を追加する必要があります。操作に必要なデータがキャッシュに格納されるため、操作は迅速に続行できます。操作が完了すると、キャッシュからメモリーに同期されて戻されるため、プロセッサーは遅いメモリーの読み取りおよび書き込みを待つ必要がなくなります。 。

Java スレッド学習のための同時プログラミングのナレッジ ポイント

Java スレッド学習のための同時プログラミングのナレッジ ポイント

コンピュータ システムでは、レジスタは L0 レベルのキャッシュであり、その後に L1、L2、および L3 (メモリ、ローカルディスク、リモートストレージ)。さらに上にあるキャッシュ記憶領域は小さく、速度は速く、コストは高く、下にあるキャッシュ記憶領域は大きく、速度は遅く、コストは低くなります。上から下まで、各層は次の層のキャッシュと見なすことができます。つまり、L0 レジスタは L1 の 1 次キャッシュのキャッシュ、L1 は L2 のキャッシュなど、次の層のデータが格納されます。各レイヤーはその下のレイヤーから取得されるため、各レイヤーのデータは次のレイヤーのデータのサブセットになります。

Java スレッド学習のための同時プログラミングのナレッジ ポイント

最近の CPU では、一般的に、L0、L1、L2、L3 が CPU 内に統合されており、L1 も第 1 レベルのデータ キャッシュ (データ キャッシュ) に分割されています。 、Dキャッシュ、L1d)と第1レベル命令キャッシュ(命令キャッシュ、Iキャッシュ、L1i)であり、それぞれデータの格納とデータの命令デコードの実行に使用される。各コアには独立した演算処理装置、コントローラー、レジスター、L1、および L2 キャッシュがあり、CPU の複数のコアが CPU キャッシュ L3 の最後の層を共有します。

2. Java メモリ モデル (JMM)

抽象的な観点から、JMM はスレッドとメイン メモリ間の抽象的な関係を定義します: スレッド間の共有変数はメイン メモリ (Main Memory) に保存され、それぞれスレッドにはプライベート ローカル メモリ (ローカル メモリ) があり、スレッドが読み書きできる共有変数のコピーが保存されます。ローカル メモリは JMM の抽象概念であり、実際には存在しません。キャッシュ、書き込みバッファ、レジスタ、その他のハードウェアとコンパイラの最適化について説明します。

Java スレッド学習のための同時プログラミングのナレッジ ポイント

Java スレッド学習のための同時プログラミングのナレッジ ポイント

2.1 可視性

可視性とは、複数のスレッドが同じ変数にアクセスするときに、スレッドがこの変数の値を指定することを意味します。が変更されると、他のスレッドは変更された値をすぐに確認できるようになります。

スレッドによる変数に対するすべての操作は作業メモリ内で実行する必要があり、メイン メモリ内の変数を直接読み書きすることはできないため、共有変数 V については、最初に独自の作業メモリに配置され、次にメイン メモリに同期されます。メモリ。 。ただし、メインメモリへのフラッシュが間に合わず、ある程度の時間差が生じます。明らかに、この時点では、変数 V に対するスレッド A の操作はスレッド B からは認識されなくなります。
共有オブジェクトの可視性の問題を解決するには、volatile キーワードまたはロックを使用できます。

2.2. アトミック性

アトミック性: つまり、1 つの操作または複数の操作、すべてが実行され、実行プロセスはいかなる要因によっても中断されないか、またはなしのいずれかです。が実装されています。 CPU リソースがスレッドの単位で割り当てられ、タイムシェアリング方式で呼び出されることは誰もが知っています。オペレーティング システムでは、プロセスが 50 ミリ秒などの短期間実行できます。50 ミリ秒を過ぎると、オペレーティング システムは実行するプロセスを再選択します (これを「タスク切り替え」と呼びます)、この 50 ミリ秒は「タイム スライス」と呼ばれます。ほとんどのタスクは時間セグメントの終了後に切り替えられます。

それでは、なぜスレッドの切り替えによってバグが発生するのでしょうか? オペレーティング システムはタスクの切り替えを実行するため、CPU 命令の実行後にタスクの切り替えが発生する可能性があります。これは CPU 命令、CPU 命令、CPU 命令であり、高級言語のステートメントではないことに注意してください。たとえば、Java では count は 1 つの文にすぎませんが、高級言語では、ステートメントを完了するために複数の CPU 命令が必要になることがよくあります。実際、count には少なくとも 3 つの CPU 命令が含まれています。

3. volatile の詳細説明

3.1. Volatile の機能

volatile 変数の単一の

read/write を同じものとして扱うことができます。これらの単一の read/write 操作を同期する

public class Volati {


    //    使用volatile 声明一个64位的long型变量
    volatile long i = 0L;//    单个volatile 变量的读
    public long getI() {
        return i;
    }//    单个volatile 变量的写
    public void setI(long i) {
        this.i = i;
    }//    复合(多个)volatile 变量的 读/写
    public void iCount(){
        i ++;
    }}
は次のコードのようになります。

public class VolaLikeSyn {

    //    使用 long 型变量
    long i = 0L;
    public synchronized long getI() {
        return i;
    }//     对单个的普通变量的读用同一个锁同步
    public synchronized void setI(long i) {
        this.i = i;
    }//    普通方法调用
    public void iCount(){
        long temp = getI();   // 调用已同步的读方法
        temp = temp + 1L;     // 普通写操作
        setI(temp);           // 调用已同步的写方法
    }}
したがって、volatile 変数自体は次のようになります。機能:

  • 可視性: volatile 変数を読み取ると、volatile 変数への最後の書き込みを常に (どのスレッドでも) 確認できます。
  • アトミック性 : 単一の volatile 変数の読み取り/書き込みはアトミックですが、volatile のような複合操作はアトミックではありません。
volatile では、実行後に変数が時間内にメイン メモリにフラッシュされることを保証できますが、count に関しては、スレッドの切り替えにより非アトミックな複数命令の状況となり、スレッド A は count をロードしたばかりです。 =0 作業メモリに到達した後、スレッド B は動作を開始できます。これにより、スレッド A と B の実行結果は両方とも 1 になり、両方ともメイン メモリに書き込まれます。メイン メモリの値は 1 のままでありません。 2

3.2、volatile の実装原則

    volatile キーワードによって変更された変数には、「lock:」プレフィックスが付きます。
  • ロック接頭辞、ロックはメモリ バリアではありませんが、メモリ バリアと同様の機能を完了できます。 Lock は CPU バスとキャッシュをロックします。これは、CPU 命令レベルでのロックとして理解できます。
  • 同時に、この命令は現在のプロセッサのキャッシュ ラインのデータをシステム メモリに直接書き込み、この書き込み操作により他の CPU のこのアドレスにキャッシュされたデータが無効になります。
4. synchronized の実装原理

JVM での Synchronized の実装は、メソッドの同期とコード ブロックの同期を実現するための Monitor オブジェクトへの出入りに基づいていますが、実装の詳細は異なりますが、MonitorEnter 命令と MonitorExit 命令のペアを通じて実装できます。

同期ブロックの場合、MonitorEnter 命令は同期コード ブロックの先頭に挿入され、monitorExit 命令はメソッドと例外の最後に挿入されます。JVM は、各 MonitorEnter に対応する MonitorExit が必要であることを保証します。一般に、コードがこの命令を実行すると、オブジェクト Monitor の所有権を取得しようとします。つまり、オブジェクトのロックを取得しようとします。

  1. モニターのエントリー番号が 0 の場合、スレッドはモニターに入り、エントリー番号を 1 に設定し、スレッドはモニターの所有者になります。
  2. スレッドがすでにモニターを占有しており、再び入ったばかりの場合、モニターへのエントリー数は 1 つ増加します。
  3. 他のスレッドがすでにモニターを占有している場合、スレッドはモニターのエントリー番号が 0 になるまでブロック状態に入り、その後モニターの所有権を再度取得しようとします。同期メソッドの逆コンパイル結果から判断すると、同期メソッドの同期は、monitorenter およびmonitorexit 命令によって実装されておらず、通常のメソッドと比較して、定数プールには ACC_SYNCHRONIZED 識別子が追加されています。
    JVM は、この識別子に基づいてメソッド同期を実装します。メソッドが呼び出されるとき、呼び出し命令は、メソッドの ACC_SYNCHRONIZED アクセス フラグが設定されているかどうかを確認します。設定されている場合、実行スレッドは最初にモニターを取得します。取得が成功すると、メソッド本体を実行でき、メソッド実行後にモニターを解放できます。メソッドの実行中、他のスレッドは同じモニター オブジェクトを再度取得できません。

synchronized で使用されるロックは Java オブジェクト ヘッダーに保存されます。Java オブジェクトのオブジェクト ヘッダーは 2 つの部分で構成されます: mark word と klass pointer:

  1. mark word は同期ステータス、フラグ、ハッシュコード、GC ステータスなどを保存します。
  2. klass ポインタには、クラス メタデータを指すオブジェクトの型ポインタが格納されます。さらに、配列の場合は、配列の長さを記録するデータもあります。

Java スレッド学習のための同時プログラミングのナレッジ ポイント

#ロック情報はオブジェクトのマーク ワードに存在し、MarkWord のデフォルト データにはオブジェクトの HashCode とその他の情報が格納されます。

Java スレッド学習のための同時プログラミングのナレッジ ポイント

ただし、オブジェクトの動作が変わると変更されます。異なるロック状態は、異なるレコード保存方法に対応します。

Java スレッド学習のための同時プログラミングのナレッジ ポイント

4.1. ロック状態

上の図を比較すると、 ロックなし状態、偏ったロック状態、軽量ロック状態、重量ロック状態 の 4 つのロック状態があることがわかり、段階的にロック状態が変化します。競争状況に応じてエスカレートします。ロックの取得と解放の効率を向上させるために、ロックはアップグレードできますが、ダウングレードすることはできません。

4.2. 偏ったロック

背景の紹介: ほとんどの場合、ロックにはマルチスレッドの競合がないだけでなく、常に複数回ロックが取得されます。スレッドを許可するために、ロックの取得コストが低くなり、不必要な CAS 操作を減らすために偏ったロックが導入されます。
は、名前が示すように、ロックにバイアスされます。スレッドへの最初のアクセスにバイアスされます。同期ロックが操作中にスレッドによってのみアクセスされる場合、マルチスレッドの紛争は発生しません。 CAS 操作のロック/ロック解除 (キューを待機している一部の CAS 操作など) この場合、バイアス ロックがスレッドに追加されます。動作中に他のスレッドがロックをプリエンプトした場合、バイアスされたロックを保持しているスレッドは一時停止され、JVM はそのロックにあるバイアスされたロックを削除し、ロックを標準の軽量ロックに戻します。リソースの競合がない場合は同期プリミティブを排除することで、プログラムの実行パフォーマンスがさらに向上します。

#バイアス ロックを取得するプロセスを理解するには、下の図を参照してください。

Java スレッド学習のための同時プログラミングのナレッジ ポイント

## ステップ 1. バイアス ロックのロゴが表示されているかどうかを確認します。 in Mark Word がセットされている場合は、ロックフラグが 01 かどうかに関わらず、1 であれば偏向可能な状態であることを確認します。

ステップ 2. バイアス可能な状態にある場合は、スレッド ID が現在のスレッドを指しているかどうかをテストします。そうである場合はステップ 5 に進み、そうでない場合はステップ 3 に進みます。
ステップ 3. スレッド ID が現在のスレッドを指していない場合は、CAS 操作を通じてロックを競合します。競合に成功した場合は、Mark Word のスレッド ID を現在のスレッド ID に設定して 5 を実行し、競合に失敗した場合は 4 を実行します。
ステップ 4. CAS がバイアス ロックの取得に失敗した場合は、競合が存在することを意味します。グローバル安全ポイント (セーフポイント) に到達すると、バイアス ロックを取得したスレッドは一時停止され、バイアス ロックは軽量ロックにアップグレードされ、安全ポイントでブロックされたスレッドは同期コードの実行を継続します。 (バイアス ロックを無効にするとワードが停止します)
ステップ 5. 同期コードを実行します。

バイアスロック解除:

偏ったロックの解除については、上記の 4 番目の手順で説明します。バイアス ロックは、他のスレッドがバイアス ロックをめぐって競合しようとした場合にのみバイアス ロックを解放し、バイアス ロックを保持しているスレッドが率先してバイアス ロックを解放することはありません。バイアスされたロックをキャンセルするには、グローバル セーフティ ポイントを待つ必要があります (この時点ではバイトコードは実行されていません)。まず、バイアスされたロックを所有するスレッドを一時停止し、ロック オブジェクトがロック状態かどうかを判断します。ロック状態(フラグビットが"01")または軽量ロック状態(フラグビットが"00")

バイアスされたロックに適用できるシナリオ:

同期ブロックを実行するスレッドは常に 1 つだけです。実行が終了してロックが解放されるまで、他のスレッドは同期を実行しません。ブロック。ロックの競合がない場合に使用されます。競合が発生すると、軽量ロックにアップグレードされます。軽量ロックにアップグレードする場合、偏ったロックを取り消す必要があります。偏ったロックを取り消すと、 stop the word 操作;
in ロック競合がある場合、偏ったロックは多くの余分な操作を実行します。特に偏ったロックをキャンセルする場合、安全なポイントにつながります。安全なポイントは stw を引き起こし、パフォーマンスが低下するため、この場合は無効にする必要があります。

jvm バイアス ロックのオン/オフを切り替える
バイアス ロックをオンにする: -XX: UseBiasedLocking -XX:BiasedLockingStartupDelay=0 バイアス ロックをオフにする: -XX:-UseBiasedLocking

4.3. 軽量ロック

軽量ロックは、バイアスされたロックからアップグレードされます. バイアスされたロックは、1 つのスレッドが同期ブロックに入ったときに実行されます. 2 番目のスレッドがロック競合に参加すると、バイアスされたロックがロックされます軽量ロックにアップグレードされる;

軽量ロック ロック プロセス:

  1. コードが同期ブロックに入ると、同期オブジェクトのロック ステータスがロックの場合-free およびバイアスは許可されていません (ロック フラグは「01」、バイアスされたロックであるかどうかは「0」)。仮想マシンはまず、現在のスレッドのスタック フレームにロック レコード (ロック レコード) を作成します。スペースは、ロック オブジェクトの現在のマーク ワードのコピーを保存するために使用されます。これは正式には Displaced Mark Word と呼ばれます。
  2. オブジェクト ヘッダーのマーク ワードをロック レコードにコピーします。
  3. コピーが成功すると、仮想マシンは CAS 操作を使用して、オブジェクトのマーク ワードをロック レコードへのポインタに更新しようとし、ロック レコード内の所有者ポインタがオブジェクト マーク ワードを指すようにします。 。アップデートが成功した場合は手順 4 に進み、そうでない場合は手順 5 に進みます。
  4. 更新アクションが成功した場合、このスレッドはオブジェクトのロックを所有し、オブジェクト Mark Word のロック フラグが「00」に設定されます。これは、オブジェクトが軽量ロック状態にあることを意味します。
  5. この更新操作が失敗した場合、仮想マシンはまずオブジェクトのマーク ワードが現在のスレッドのスタック フレームを指しているかどうかを確認します。そうであれば、現在のスレッドがすでにこのスタック フレームのロックを所有していることを意味しますオブジェクトを作成し、同期ブロックに直接入って続行できます。それ以外の場合は、複数のスレッドがロックをめぐって競合しており、スピンしてロックを待機し、一定回数経過してもロック オブジェクトが取得されなかったことを意味します。ヘビーウェイト スレッド ポインタは競合スレッドを指し、競合スレッドもブロックされ、軽量スレッドがロックを解放してウェイクアップするのを待ちます。ロックフラグの状態値は「10」に変化し、マークワードに格納されるのは重量ロック(ミューテックス)へのポインタとなり、ロックを待っている後続のスレッドもブロッキング状態になります。

4.3.1. スピン ロックの原理

スピン ロックの原理は非常に単純で、ロックを保持しているスレッドが短時間でロック リソースを解放できれば、待機しているスレッドはロック リソースを解放できます。ロックを保持しているスレッドは、ブロックおよびサスペンド状態に入るためにカーネル モードとユーザー モードを切り替える必要はなく、ロックを保持しているスレッドがロックを解放した直後に待機 (スピン) してロックを取得するだけで済みます。このようにして、ユーザー スレッドとカーネル間の切り替えコストを回避します。
ただし、スレッドの回転には CPU を消費する必要があります。率直に言うと、CPU が無駄な作業を行っていることを意味します。スレッドが常に CPU を占有して無駄な作業を行うとは限らないため、最大スピン待機を設定する必要があります時間。
ロックを保持しているスレッドの実行時間が最大スピン待機時間を超えてロックが解放されない場合、ロックを競合している他のスレッドは最大待機時間内にロックを取得できません。 、競合スレッドは回転を停止し、ブロッキング状態になります。

4.3.2. スピン ロックの長所と短所

スピン ロックは、スレッド ブロッキングを可能な限り減らします。これは、ロックをめぐって激しく競合せず、非常に短いロックを占有するコード ブロックです。パフォーマンスの面では、スピンの消費量がスレッドのブロックおよびサスペンド操作の消費量よりも少なくなるため、パフォーマンスが大幅に向上します。
ただし、ロックの競合が激しい場合、またはロックを保持しているスレッドが同期ブロックを実行するために長時間ロックを占有する必要がある場合は、現時点でスピン ロックを使用するのは適していません。ロックは、ロックを取得する前に常に CPU を占有します。これは無駄な作業であり、ピットを占有します。スレッドのスピンの消費量は、スレッドのブロックおよび中断操作の消費量よりも大きくなります。カップを必要とする他のスレッドは CPU を取得できず、無駄が発生します。 CPUの。

4.3.3. スピン ロック時間のしきい値

スピン ロックの目的は、CPU リソースを解放せずに占有し、ロックが取得されるまで待機してすぐに処理することです。しかし、スピンの実行時間はどのように選択すればよいのでしょうか?スピンの実行時間が長すぎると、多数のスレッドがスピン状態になり CPU リソースを占有し、システム全体のパフォーマンスに影響を与えます。ですので、回転数が重要になります。
##これは、同じロックのスピン時間とロック所有者のステータスによって決定されます。基本的には、スレッドのコンテキスト切り替えの時間が最適な時間であると考えられています。

JDK1.6-XX: UseSpinning はスピン ロックをオンにします。JDK1.7 以降、このパラメータは削除され、jvm によって制御されます。

Java スレッド学習のための同時プログラミングのナレッジ ポイント

4.3.4. さまざまなロックの比較

Java スレッド学習のための同時プログラミングのナレッジ ポイント

推奨学習: 「java ビデオ チュートリアル

以上がJava スレッド学習のための同時プログラミングのナレッジ ポイントの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はcsdn.netで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。