この記事では、java に関する関連知識を提供します。主に、Java のメモリ モデル、volatile の詳細な説明、synchronized の実装原理など、同時プログラミングに関連する問題を整理しています。一緒に見てください、皆さんのお役に立てれば幸いです。
推奨学習: 「java ビデオ チュートリアル 」
Java メモリモデルは Java Memory Model (略して JMM) です。 JMM は、Java 仮想マシン (JVM) がコンピュータ メモリ (RAM) 内でどのように動作するかを定義します。 JVM はコンピュータ全体の仮想モデルであるため、JMM は JVM に関連付けられます。 Java1.5版ではリファクタリングが行われており、現在のJavaは引き続きJava1.5版を使用している。 Jmm が遭遇する問題は、現代のコンピューターで遭遇する問題と似ています。
物理コンピュータの同時実行性の問題. 物理マシンが遭遇する同時実行性の問題は、仮想マシンの状況と多くの類似点があります。物理マシンの同時実行性処理スキームも、仮想マシンの実装においてかなりの参考になります。
「Google All-Engineering Conference での Jeff Dean のレポート」に基づくと、
コンピューターが通常の基本操作を実行するときに、必要な応答時間は次のとおりです。異なります。
次のケースは説明のみを目的としており、実際の状況を表すものではありません。
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の命令速度がメモリのアクセス速度をはるかに上回り、コンピュータの記憶装置との間には数桁の差があるため、最新のコンピュータでは、コンピュータ システムは、メモリとプロセッサの間のバッファとして機能するために、プロセッサの動作速度に可能な限り近い読み取りおよび書き込み速度のキャッシュ層 (キャッシュ) を追加する必要があります。操作に必要なデータがキャッシュに格納されるため、操作は迅速に続行できます。操作が完了すると、キャッシュからメモリーに同期されて戻されるため、プロセッサーは遅いメモリーの読み取りおよび書き込みを待つ必要がなくなります。 。
コンピュータ システムでは、レジスタは L0 レベルのキャッシュであり、その後に L1、L2、および L3 (メモリ、ローカルディスク、リモートストレージ)。さらに上にあるキャッシュ記憶領域は小さく、速度は速く、コストは高く、下にあるキャッシュ記憶領域は大きく、速度は遅く、コストは低くなります。上から下まで、各層は次の層のキャッシュと見なすことができます。つまり、L0 レジスタは L1 の 1 次キャッシュのキャッシュ、L1 は L2 のキャッシュなど、次の層のデータが格納されます。各レイヤーはその下のレイヤーから取得されるため、各レイヤーのデータは次のレイヤーのデータのサブセットになります。
最近の CPU では、一般的に、L0、L1、L2、L3 が CPU 内に統合されており、L1 も第 1 レベルのデータ キャッシュ (データ キャッシュ) に分割されています。 、Dキャッシュ、L1d)と第1レベル命令キャッシュ(命令キャッシュ、Iキャッシュ、L1i)であり、それぞれデータの格納とデータの命令デコードの実行に使用される。各コアには独立した演算処理装置、コントローラー、レジスター、L1、および L2 キャッシュがあり、CPU の複数のコアが CPU キャッシュ L3 の最後の層を共有します。抽象的な観点から、JMM はスレッドとメイン メモリ間の抽象的な関係を定義します: スレッド間の共有変数はメイン メモリ (Main Memory) に保存され、それぞれスレッドにはプライベート ローカル メモリ (ローカル メモリ) があり、スレッドが読み書きできる共有変数のコピーが保存されます。ローカル メモリは JMM の抽象概念であり、実際には存在しません。キャッシュ、書き込みバッファ、レジスタ、その他のハードウェアとコンパイラの最適化について説明します。
2.1 可視性 可視性とは、複数のスレッドが同じ変数にアクセスするときに、スレッドがこの変数の値を指定することを意味します。が変更されると、他のスレッドは変更された値をすぐに確認できるようになります。 スレッドによる変数に対するすべての操作は作業メモリ内で実行する必要があり、メイン メモリ内の変数を直接読み書きすることはできないため、共有変数 V については、最初に独自の作業メモリに配置され、次にメイン メモリに同期されます。メモリ。 。ただし、メインメモリへのフラッシュが間に合わず、ある程度の時間差が生じます。明らかに、この時点では、変数 V に対するスレッド A の操作はスレッド B からは認識されなくなります。
共有オブジェクトの可視性の問題を解決するには、volatile キーワードまたはロックを使用できます。
アトミック性: つまり、1 つの操作または複数の操作、すべてが実行され、実行プロセスはいかなる要因によっても中断されないか、またはなしのいずれかです。が実装されています。 CPU リソースがスレッドの単位で割り当てられ、タイムシェアリング方式で呼び出されることは誰もが知っています。オペレーティング システムでは、プロセスが 50 ミリ秒などの短期間実行できます。50 ミリ秒を過ぎると、オペレーティング システムは実行するプロセスを再選択します (これを「タスク切り替え」と呼びます)、この 50 ミリ秒は「タイム スライス」と呼ばれます。ほとんどのタスクは時間セグメントの終了後に切り替えられます。
それでは、なぜスレッドの切り替えによってバグが発生するのでしょうか? オペレーティング システムはタスクの切り替えを実行するため、CPU 命令の実行後にタスクの切り替えが発生する可能性があります。これは CPU 命令、CPU 命令、CPU 命令であり、高級言語のステートメントではないことに注意してください。たとえば、Java では count は 1 つの文にすぎませんが、高級言語では、ステートメントを完了するために複数の CPU 命令が必要になることがよくあります。実際、count には少なくとも 3 つの CPU 命令が含まれています。
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 ++; }}
は次のコードのようになります。したがって、volatile 変数自体は次のようになります。機能: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); // 调用已同步的写方法 }}
同期ブロックの場合、MonitorEnter 命令は同期コード ブロックの先頭に挿入され、monitorExit 命令はメソッドと例外の最後に挿入されます。JVM は、各 MonitorEnter に対応する MonitorExit が必要であることを保証します。一般に、コードがこの命令を実行すると、オブジェクト Monitor の所有権を取得しようとします。つまり、オブジェクトのロックを取得しようとします。
synchronized で使用されるロックは Java オブジェクト ヘッダーに保存されます。Java オブジェクトのオブジェクト ヘッダーは 2 つの部分で構成されます: mark word と klass pointer:
#ロック情報はオブジェクトのマーク ワードに存在し、MarkWord のデフォルト データにはオブジェクトの HashCode とその他の情報が格納されます。
ただし、オブジェクトの動作が変わると変更されます。異なるロック状態は、異なるレコード保存方法に対応します。
上の図を比較すると、 ロックなし状態、偏ったロック状態、軽量ロック状態、重量ロック状態 の 4 つのロック状態があることがわかり、段階的にロック状態が変化します。競争状況に応じてエスカレートします。ロックの取得と解放の効率を向上させるために、ロックはアップグレードできますが、ダウングレードすることはできません。
背景の紹介: ほとんどの場合、ロックにはマルチスレッドの競合がないだけでなく、常に複数回ロックが取得されます。スレッドを許可するために、ロックの取得コストが低くなり、不必要な CAS 操作を減らすために偏ったロックが導入されます。
は、名前が示すように、ロックにバイアスされます。スレッドへの最初のアクセスにバイアスされます。同期ロックが操作中にスレッドによってのみアクセスされる場合、マルチスレッドの紛争は発生しません。 CAS 操作のロック/ロック解除 (キューを待機している一部の CAS 操作など) この場合、バイアス ロックがスレッドに追加されます。動作中に他のスレッドがロックをプリエンプトした場合、バイアスされたロックを保持しているスレッドは一時停止され、JVM はそのロックにあるバイアスされたロックを削除し、ロックを標準の軽量ロックに戻します。リソースの競合がない場合は同期プリミティブを排除することで、プログラムの実行パフォーマンスがさらに向上します。
## ステップ 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
軽量ロックは、バイアスされたロックからアップグレードされます. バイアスされたロックは、1 つのスレッドが同期ブロックに入ったときに実行されます. 2 番目のスレッドがロック競合に参加すると、バイアスされたロックがロックされます軽量ロックにアップグレードされる;
軽量ロック ロック プロセス:
スピン ロックの原理は非常に単純で、ロックを保持しているスレッドが短時間でロック リソースを解放できれば、待機しているスレッドはロック リソースを解放できます。ロックを保持しているスレッドは、ブロックおよびサスペンド状態に入るためにカーネル モードとユーザー モードを切り替える必要はなく、ロックを保持しているスレッドがロックを解放した直後に待機 (スピン) してロックを取得するだけで済みます。このようにして、ユーザー スレッドとカーネル間の切り替えコストを回避します。
ただし、スレッドの回転には CPU を消費する必要があります。率直に言うと、CPU が無駄な作業を行っていることを意味します。スレッドが常に CPU を占有して無駄な作業を行うとは限らないため、最大スピン待機を設定する必要があります時間。
ロックを保持しているスレッドの実行時間が最大スピン待機時間を超えてロックが解放されない場合、ロックを競合している他のスレッドは最大待機時間内にロックを取得できません。 、競合スレッドは回転を停止し、ブロッキング状態になります。
スピン ロックは、スレッド ブロッキングを可能な限り減らします。これは、ロックをめぐって激しく競合せず、非常に短いロックを占有するコード ブロックです。パフォーマンスの面では、スピンの消費量がスレッドのブロックおよびサスペンド操作の消費量よりも少なくなるため、パフォーマンスが大幅に向上します。
ただし、ロックの競合が激しい場合、またはロックを保持しているスレッドが同期ブロックを実行するために長時間ロックを占有する必要がある場合は、現時点でスピン ロックを使用するのは適していません。ロックは、ロックを取得する前に常に CPU を占有します。これは無駄な作業であり、ピットを占有します。スレッドのスピンの消費量は、スレッドのブロックおよび中断操作の消費量よりも大きくなります。カップを必要とする他のスレッドは CPU を取得できず、無駄が発生します。 CPUの。
スピン ロックの目的は、CPU リソースを解放せずに占有し、ロックが取得されるまで待機してすぐに処理することです。しかし、スピンの実行時間はどのように選択すればよいのでしょうか?スピンの実行時間が長すぎると、多数のスレッドがスピン状態になり CPU リソースを占有し、システム全体のパフォーマンスに影響を与えます。ですので、回転数が重要になります。
##これは、同じロックのスピン時間とロック所有者のステータスによって決定されます。基本的には、スレッドのコンテキスト切り替えの時間が最適な時間であると考えられています。
JDK1.6-XX: UseSpinning はスピン ロックをオンにします。JDK1.7 以降、このパラメータは削除され、jvm によって制御されます。
推奨学習: 「java ビデオ チュートリアル 」
以上がJava スレッド学習のための同時プログラミングのナレッジ ポイントの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。