Java マルチスレッドの基礎-15: Java での同期処理の最適化 - ロックのアップグレード、ロックの削除、ロックの粗密化

並行プログラミングにおける一般的なロック戦略の概要から、同期には次の特徴があります。

  1. 楽観的ロックで開始され、ロックの競合が頻繁に発生する場合は悲観的ロックに変換されます。
  2. 軽量ロックの実装から開始され、ロックが長時間保持されると、重量ロックに変換されます。
  3. 軽量ロックを実装する場合、スピン ロック戦略が使用される可能性が最も高くなります。
  4. 不当なロックです。
  5. リエントラントロックです。
  6. 読み取り/書き込みロックではありません。

この記事では、ロックのアップグレード、ロックの削除、ロックの粗密化など、synchronized のいくつかの最適化操作を紹介します。

1.ロックのアップグレード

JVM は、同期ロックを 4 つの状態 (ロックなし、バイアスされたロック、軽量ロック、および重量ロック) に分類します。ロックプロセス中に、実際の状況に基づいてアップグレードが順次実行されます。(**現在の主流の JVM 実装では、アップグレードのみをロックでき、ダウングレードはロックできません。** 実装することは不可能ではありませんが、実装することによる利点とコストが不釣り合いになるため、実装する必要がある可能性があります。実装されていない。)

ロック全体のプロセス(ロックアップグレードプロセス):ロック開始時は偏ったロック状態にあり、ロック競合に遭遇するとスピンロック(軽量ロック)にアップグレードされ、競合が激しくなるとスピンロック(軽量ロック)になります。重みレベル ロック (ブロックして待機するためにカーネルに与えられます)。

1. バイアスロック

ロックを試行する最初のスレッドが、最初にバイアスされたロック状態に入ります。バイアス ロックは、スレッド同期のパフォーマンスを向上させるために Java 仮想マシン (JVM) で使用される最適化テクノロジです。マルチスレッド環境では、共有リソースに対する同期操作では、スレッドの相互排他的アクセスを保証するためにロック (同期) を使用する必要があります。従来のロック メカニズムには競合とコンテキスト切り替えのオーバーヘッドがあり、パフォーマンスに一定の影響を与えます。バイアスされたロックは、競合がない場合のロック操作のコストを削減するために導入されました。

バイアスされたロックは実際には「ロック」ではなく、スレッドが最初にロック オブジェクトをマークし、特定のロックがどのスレッドに属しているかを記録できるようにするだけです。

基本的な考え方は、スレッドがロックを取得してコードの同期ブロックにアクセスするときに、競合がなければ、次回スレッドが再び同期ブロックに入るときに、再度ロックを取得する必要がないということです。これは、競合がない場合、スレッドが同期されたコード ブロックに繰り返しアクセスすると仮定すると、毎回ロックをめぐって競合する必要はなく、ロックが偏った状態にあるかどうかを判断するだけでよく、そうであれば直接判断するためです。同期されたコードブロックを入力します。

平たく言えば、今後他のスレッドがロックをめぐって競合しない場合、実際にロックする必要はなくなり、ロックとロック解除のオーバーヘッドが回避されます。ただし、他のスレッドがこのロックをめぐって競合しようとすると、バイアスされたロックはすぐに実際のロック (軽量ロック) にアップグレードされ、他のスレッドは待つことしかできません。これにより、効率とスレッドの安全性の両方が保証されます。

ロックを競合する他のスレッドがあるかどうかを確認するにはどうすればよいですか?

バイアス ロックは、synchronized によって内部的に行われる作業であることに注意してください。synchronized はオブジェクトをロックします。このいわゆる「偏ったロック」は、このオブジェクトに痕跡を残します。

現在のロックがどのスレッドに属するかは先頭のロックオブジェクトに記録されているため、現在ロックを申請しているスレッドが先頭に記録されたスレッドであるかどうかは容易に識別できる。

別のスレッドが同じオブジェクトをロックしようとすると、最初にそのオブジェクトをマークしようとしますが、すでにマークされていることがわかります。そのため、JVM は最初に来たスレッドに通知し、ロックを迅速にアップグレードできるようにします。

バイアスされたロックは本質的に「遅延ロック」、つまりロックを解除できる場合はロックされず、不要なロックのオーバーヘッドが可能な限り回避されますが、作成すべきマークは作成する必要があり、そうでない場合は不可能になります。実際のロックが必要な場合を区別するため。

バイアスロックを理解するための例を示します

男性主人公が錠、女性主人公が糸だとします。女主人公と男主人公だけが曖昧な場合(つまり、このスレッドだけがこのロックを使用する場合)、男主人公と女主人公が結婚許可を取得しなくても(高コストの操作を回避でき)、一緒に暮らし続けることができる。

しかし、このとき、男性主人公に張り合って、男性主人公と関係を持とうとする女性パートナーが現れた場合、女性主人公はこの時点で即断しなければなりません。結婚証明書は、必ず完成する(つまり、実際のプロセスが完了する)ロック)と女性主人公を諦めさせます。

したがって、偏ったロック = 曖昧さに巻き込まれる~~

2.スピンロック

**スピンロックとは何ですか? **ロック戦略の記事で言及されている:

スピン ロックは典型的な軽量ロック実装であり、通常は純粋なユーザー モードであり、カーネル モードを経由する必要はありません。前の方法では、スレッドがロックの取得に失敗するとブロッキング状態になり、CPU を放棄するため、再度スケジュールできるようになるまでに長い時間がかかります。しかし実際には、ほとんどの場合、現在のロックの取得は失敗しても、ロックはすぐに解放されるため、CPU を放棄する必要はありません。現時点では、スピン ロックを使用してこのような問題に対処できます。

スピン ロックはビジー待機ロック メカニズムです。スレッドがスピン ロックを取得する必要がある場合、スレッドはすぐにブロックされるのではなく、ロックが利用可能かどうかを繰り返し確認します。ロックの取得が失敗した場合 (ロックがすでに別のスレッドによって占有されている場合)、現在のスレッドはすぐに再度ロックの取得を試み、ロックが取得されるまでロックが解放されるのを待ちながらスピン (アイドル) を続けます。ロックを取得する最初の試行は失敗し、2 回目の試行は非常に短い時間内に行われます。これにより、他のスレッドによってロックが解放された後、現在のスレッドができるだけ早くロックを取得できるようになります。

利点: CPU が放棄されることはなく、スレッドのブロックやスケジューリングも必要ありません。ロックが解除されると、すぐにロックを取得できます。
欠点: 他のスレッドによってロックが長時間保持されると、CPU リソースが消費され続けます (ビジー待機) が、サスペンドして待機している場合は CPU リソースは消費されません。

スピン ロックは、スピンによって CPU リソースが消費されるため、保護クリティカル セクションが小さく、ロックが占める時間が短い状況に適しています。スピン ロックは通常、アトミック操作または特別なハードウェア命令を使用して実装されます。

他のスレッドがロック競合に入ると、偏ったロック状態が解消され、軽量ロック状態、つまり適応スピン ロックに入ります。

ここでの軽量ロックは CAS を通じて実装されています。CAS を介してメモリの一部をチェックして更新します (たとえば、null がスレッドの参照と等しいかどうかを比較します)。更新が成功した場合、ロックは成功したとみなされ、更新が失敗した場合、ロックは占有されているとみなされ、スピンのような待機は処理中に諦めずに継続します CPUリソース。

( CASアルゴリズムの詳細説明を参照)

スピンロックを実現するCASアルゴリズムの原理

スピン操作は CPU をアイドリング状態に保ち、CPU リソースの無駄になるため、ここでのスピンは永久に継続するわけではなく、一定の時間または再試行回数に達するとスピンを停止します。これは「アダプティブ」とも呼ばれます。

3. ヘビーウェイトロック

**重量ロックとは何ですか? **ロック戦略の記事で言及されている:

簡単に言うと、軽量ロックは、ロックおよびロック解除のプロセスをより高速かつ効率的に行うロック戦略であり、一方、重量ロックは、ロックおよびロック解除のプロセスをより低速かつ効率的に行うロック戦略です。重量ロックのロック メカニズムは、OS が提供するミューテックス (mutex) に大きく依存します。

  • カーネル モード ユーザー モードの切り替えが多数発生します。
  • スレッドのスケジューリングが発生しやすくなります。

これら 2 つの操作のコストは比較的高く、ユーザー モードとカーネル モード間の切り替えが必要になると、効率が低くなります。

競争が激しくなるとスピンがすぐにロック状態にならない。重量級のロックに拡張されます。

スピン ロックは最も速くロックを取得できますが、多くの CPU を消費します (スピン中に CPU のアイドリングが速くなるため)。たとえば、現在のロックの競合が非常に激しい場合、50 のスレッドがロックをめぐって競合し、1 つのスレッドがそれをめぐって競合し、残りの 49 のスレッドが待機しているとします。非常に多くのスレッドが回転およびアイドル状態になるため、CPU の消費量が非常に多くなります。この場合、ロック戦略を変更してヘビーウェイト ロックにアップグレードし、他のスレッドがブロックしてカーネル内で待機できるようにします (これは、スレッドが一時的に CPU リソースを放棄する必要があり、カーネルが後続のスケジューリングを実行することを意味します)。

(追記: Windows や Linux などの現在の主流オペレーティング システムには、スケジューリングのオーバーヘッドが高くなります。システムは、指定されたスケジューリングを xx 時間以内に完了することを約束しません。極端な場合には、スケジューリングのオーバーヘッドが非常に大きくなる可能性があります。

ただし、別のリアルタイム オペレーティング システム (vxworks など) もあり、低コストでタスクのスケジューリングを完了できますが、他の機能がより多く犠牲になります。ロケットの打ち上げなど時間精度が比較的高い特殊な分野で使用されます。)

競争が激しくなるとスピンがすぐにロック状態にならない。重量級のロックに拡張されます。

ここでのヘビーウェイト ロックは、カーネルによって提供されるミューテックスを指します。

  1. スレッドがロック操作を実行すると、最初にカーネル状態に入ります。
  2. 現在のロックがカーネル モードの別のスレッドによってすでに占有されているかどうかを確認します。
  3. ロックが占有されていない場合、ロックは成功し、ユーザー モードはユーザー モードに戻ります。
  4. ロックが占有されている場合、ロックは失敗します。この時点で、スレッドはロック待機キューに入り、ハングし、オペレーティング システムによって起動されるのを待ちます。
  5. 一連の「人生の変遷」を経て、最終的に他のスレッドによってロックが解放されましたが、このとき、オペレーティング システムも中断されたスレッドを記憶していたので、スレッドを起動してロックの再取得を試行させました。

2. ロックの解除

ロックの排除は、「ロック不要、ロックなし」の現れでもあります。ロック アップグレードとは異なり、ロック アップグレードはプログラムの実行フェーズ中に JVM によって行われる最適化方法です。ロックの削除は、プログラムのコンパイル段階での最適化方法です。コンパイラと JVM は、現在のコードが複数のスレッドで実行されているかどうか、またはロックが必要かどうかを検出します。不要であるにもかかわらずロックが書き込まれている場合、ロックはコンパイル プロセス中に自動的に削除されます。

一部のアプリケーション コードでは、不必要に synchronized が使用される場合があります。たとえば、StringBuffer はスレッドセーフであり、その主要なメソッドにはそれぞれ synchronized キーワードが追加されています。

StringBufferのソースコードの一部

しかし、ここで問題が発生します。StringBuffer が単一のスレッドで使用されている場合、スレッドの安全性の問題は関係ありません。実際にはこの時点ではロックする必要はありません。その後、コンパイラはこの時点で synchronized が不要であると判断し、コンパイル段階で synchronized を削除します。これは、ロック操作が実際にはコンパイルされていないことと同じです。

StringBuffer sb = new StringBuffer();
sb.append("a");
sb.append("b");
sb.append("c");
sb.append("d");

この時点で、追加の呼び出しにはすべてロックとロック解除が含まれます。ただし、このコードが単一スレッドでのみ実行される場合、これらのロックおよびロック解除操作は不要であり、リソースのオーバーヘッドの一部が無駄になります。

ロックの削除は一般に比較的保守的な最適化方法であるため、コンパイラは削除操作が信頼できるものであることを保証する必要があります。したがって、ロックの削除は絶対に確実な場合にのみ実装され、それ以外の場合はロックされたままとなり、この時点では、他の操作戦略 (上記のロックのアップグレードなど) を使用してロックを最適化します。

3. ロックの粗し加工

ロックの粒度は、同期されたコード ブロックに含まれるコードの量を指します。コードが多いほど粒度は大きくなり、コードが少ないほど粒度は小さくなります。

一般に、コードを記述するときは、ほとんどの場合、ロックの粒度を小さくする必要があります。(ロックの粒度が小さいということは、シリアルに実行されるコードが少なくなり、同時に実行されるコードが増えることを意味します)。シナリオで頻繁なロックとロック解除が必要な場合、コンパイラーはこの操作を最適化して、より粒度の粗いロック、つまりロックの粗化を行うことがあります。

実際の開発プロセスでは、ロックが解放されたときに他のスレッドがそのロックを使用できるようにするために、きめ細かいロックが使用されます。しかし実際には、このロックを捕捉するスレッドが他に存在しない可能性があります。この場合、JVM はロックを自動的に粗くして、頻繁なロックの適用と解放によって生じる不必要なオーバーヘッドを回避します。

ロックの粗大化を理解するためにクリを与える

出勤したらリーダーに仕事の報告をしてください。あなたのリーダーはあなたに 3 つの仕事、A、B、C を割り当てました。
報告方法には次のようなものがあります。

  1. 最初に電話をかけ、Aの作業の進捗状況を報告し、電話を切ります。2回目の電話をかけ、Bの作業の進捗状況を報告し、電話を切ります。再度電話をかけ、Cの作業の進捗状況を報告し、電話を切ります。電話。(あなたがリーダーに電話をかけると、リーダーはあなたの電話に応答し、リーダーは他のことをすることができません。他の人がリーダーに電話をかけたい場合は、ブロックして待つことしかできません。ロックの競合ごとに、一定の待機オーバーヘッドが発生する可能性があります。この時点では、時間がかかると、全体の効率はさらに低くなる可能性があります。)
  2. 電話をかけ、仕事A、仕事B、仕事Cを一気に報告し、電話を切る。

明らかに 2 番目の方法の方が効率的です。

同期戦略は比較的複雑であり、非常に「インテリジェントな」ロックであることがわかります。

おすすめ

転載: blog.csdn.net/wyd_333/article/details/131817841