まず、楽観的なロックと悲観的なロックを紹介しましょう。
悲観的なロック:常に最悪の場合を想定してください。データを取得するたびに、他の人がそれを変更すると思うので、データを取得するたびにロックします。そうすれば、他の人がロックを取得するまでブロックします。従来のリレーショナルデータベースは、操作を行う前にロックされるロック、書き込みロックなど、行ロック、テーブルロックなど、多くのロックメカニズムを使用しています。たとえば、Javaの同期キーワード同期キーワードの実装も悲観的なロックです。
楽観的なロック:名前が示すように、それは非常に楽観的です。データを取得するたびに、他の人がそれを変更しないと思うので、ロックしません。ただし、更新するときは、他の人がこの期間中にデータを更新したかどうかを判断し、バージョン番号やその他のメカニズムを使用できます。楽観的なロックは、スループットを改善できるマルチリードアプリケーションタイプに適しています。たとえば、データベースはwrite_conditionメカニズムと同様の楽観的なロックを提供しますが、実際にはすべて楽観的なロックによって提供されます。 Javaでは、java.util.concurrent.atomicパッケージの下の原子変数クラスは、楽観的なロックを使用してCASによって実装されます。
楽観的なロック-CASの実装(比較とスワップ):
ロックの問題:
JDK1.5の前に、Javaは同期されたキーワードに頼って同期を確保しました。このように、一貫したロックプロトコルを使用して共有状態へのアクセスを調整することにより、どのスレッドが共有変数のロックを保持しても、これらの変数にアクセスするために排他的な方法を使用することを保証できます。これは一種の排他的なロックです。排他的ロックは実際には一種の悲観的なロックなので、同期は悲観的なロックであると言えます。
悲観的なロックメカニズムには次の問題があります。
1.マルチスレッド競争では、ロックを追加およびリリースすると、より多くのコンテキストの切り替えとスケジューリングの遅延が発生し、パフォーマンスの問題が発生します。
2。ロックを保持するスレッドは、このロックを吊るす必要がある他のすべてのスレッドを引き起こします。
3.優先度の高いスレッドがロックをリリースするスレッドが低いスレッドを待機する場合、優先度の反転を引き起こし、パフォーマンスリスクを引き起こします。
悲観的なロックのこれらの問題と比較して、もう1つのより効果的なロックは楽観的なロックです。実際、楽観的なロックは次のとおりです。ロックを追加しないたびに、同時紛争がないと仮定して操作を完了します。同時競合が失敗した場合は、成功するまで再試行してください。
楽観的なロック:
楽観的なロックは上記で言及されていますが、実際には一種の考えです。悲観的なロックと比較して、楽観的なロックは、データが一般に同時競合を引き起こさないと想定しているため、データが送信および更新されると、データが同時に競合するかどうかを正式に検出します。同時紛争が見つかった場合、ユーザーの間違った情報が返され、ユーザーはそれを行う方法を決定します。
上記の楽観的ロックの概念は、実際にその特定の実装の詳細を説明しています。主に競合の検出とデータの更新という2つのステップが含まれています。典型的な実装方法の1つは、比較とスワップ(CAS)です。
CAS:
CASは楽観的なロックテクノロジーです。複数のスレッドがCASを使用して同じ変数を同時に更新しようとすると、1つのスレッドのみが変数の値を更新できますが、他のスレッドは失敗します。失敗したスレッドは停止されませんが、この競争が失敗し、再試行できると言われます。
CASの操作には、読み取りおよび書き込みが必要なメモリの位置(v)、比較のために予想される元の値(a)、および記述する新しい値(b)の3つのオペランドが含まれています。メモリ位置Vの値が予想される元の値aと一致する場合、プロセッサは位置値を新しい値Bに自動的に更新します。そうしないと、プロセッサは何もしません。どちらの場合でも、CAS指令の前にその場所の値を返します。 (CASの特別な場合、CASが現在の値を抽出せずに成功しているかどうかのみである。)CASは、「ポジションVが値Aを含めるべきだと思う。それが含まれる場合、Bをこの位置に入れてください。そうしないと、位置を変更しないでください。この位置の現在の値を教えてください。」これは、実際には、楽観的なロックの競合チェック +データの更新の原則と同じです。
楽観的なロックは一種の考えであることをここで強調しましょう。 CASはこのアイデアを実現する方法です。
CASのJavaサポート:
JDK1.5の新しいjava.util.concurrent(juc)は、CAS上に構築されています。同期などのブロッキングアルゴリズムと比較して、CASは非ブロッキングアルゴリズムの一般的な実装です。したがって、JUCはパフォーマンスを大幅に改善しました。
java.util.concurrentのAtomicintegerを例として、ロックを使用せずにスレッドの安全性を確保する方法を確認します。主に、++ I操作に相当するgetandincrementメソッドを理解しています。
パブリッククラスAtomicInteger拡張番号java.io.serializable {private volatile int value; public final int get(){return値; } public final int getandincrement(){for(;;){int current = get(); int next = current + 1; if(compareandset(current、next))return current; }} public final boolean Compareandset(int expect、int update){return unsafe.compareandswapint(this、value offset、expect、update); }}ロックなしのメカニズムでは、スレッド間のデータが可視性であることを確認するために、フィールド値を使用する必要があります。このようにして、変数の値を取得すると直接読むことができます。次に、私がどのように完了したか見てみましょう。
getAndinCrementはCAS操作を使用し、メモリからデータを読むたびに、このデータでCAS操作を実行し、+1以降の結果を実行します。成功した場合、結果は返されます。そうでなければ、成功するまで再試行してください。
CompareAndsetはJNI(Javaネイティブインターフェイス)を使用して、CPU命令の操作を完了します。
Public Final Boolean CompareAndset(int expect、int update){unsafe.compareandswapint(this、value offset、expect、update); }unsafe.compareandswapint(this、value offset、expect、update);次のロジックに似ています。
if(this == expect){this = update return true; } else {return false; }これを比較する方法==この2つのステップの原子性を達成するために、これを期待する、この= update、compareandswapintを置き換えますか? CASの原則を参照してください
CASの原則:
CASはJNIコードを呼び出すことで実装されます。 CompareAndswapintは、Cを使用して基礎となるCPU命令を呼び出すことで実装されます。
以下は、より一般的に使用されるCPU(Intel X86)の分析からのCASの実装原則を説明します。
sun.misc.unsafeクラスのCompareandswapint()メソッドのソースコードは次のとおりです。
パブリックファイナルネイティブブールアンドスワピント(オブジェクトO、ロングオフセット、int redict、int x);
これはローカルメソッドコールであることがわかります。このローカルメソッドがJDKで呼ぶC ++コードは次のとおりです。
#define lock_if_mp(mp)__asm cmmp mp、0 / __asm je l0 / __asm _emit 0xf0 / __asm l0:inline jint atomic :: cmpxchg(jint exchange_value、volatile jint*、jint compare_value) os :: is_mp(); __asm {mov edx、dest mov ecx、exchange_value mov eax、compare_value lock_if_mp(mp)cmmpxchg dword ptr [edx]、ecx}}}上記のソースコードに示すように、プログラムは、現在のプロセッサのタイプに基づいてCMMPXCHG命令にロックプレフィックスを追加するかどうかを決定します。プログラムがマルチプロセッサで実行されている場合は、CMMPXCHG命令にロックプレフィックスを追加します。それどころか、プログラムが単一のプロセッサで実行されている場合、ロックプレフィックスは省略されます(単一のプロセッサ自体は、単一のプロセッサ内の順次一貫性を維持し、ロックプレフィックスによって提供されるメモリバリア効果を必要としません)。
CASの短所:
1。ABAの質問:
たとえば、スレッド1がメモリ位置VからAを取り出した場合、別のスレッド2もメモリからAを取り出し、2つが操作を実行してBになり、2つはV位置Aでデータを回します。 Thread OneのCAS操作は成功していますが、隠された問題がある可能性があります。以下に示すように:
一方向リンクリストに実装されたスタックがあり、スタックの上部はAです。この時点で、Thread T1はA.NextがBであることをすでに知っており、スタックの上部をBにCASに置き換えることを望んでいます。
head.compareandset(a、b);
T1が上記の命令を実行する前に、スレッドT2が介入し、aとbをスタックから出し、次にpushd、c and Aを配置します。
この時点で、CAS操作を実行するのはT1のターンです。検出では、スタックの上部がまだAであるため、CASは成功し、スタックの上部がBになりますが、実際にはB.nextはnullであるため、現時点での状況は次のようになります。
スタックには1つの要素Bのみがあり、CとDで構成されるリンクリストはスタックに存在しなくなります。 CとDは理由もなく捨てられます。
Java 1.5から、JDKのAtomic Packageは、ABA問題を解決するためのクラスAtomicStampedReferenceを提供します。このクラスの比較方法は、最初に現在の参照が予想される参照に等しいかどうか、および現在のフラグが予想されるフラグに等しいかどうかを確認することです。すべてが等しい場合、フラグの参照と値は、特定の更新値にアトミック的に設定されます。
public boolean CompareAndset(v expectionReference、// Progestion Reference v newReference、// Reference int expectionStamp、//予想フラグ//更新フラグ)
実際のアプリケーションコード:
private static AtomicStampedReference <integer> AtomicStampedRef = new AtomicStampedReference <integer>(100、0); ......... AtomicStampedref.comPareandset(100、101、Stamp、Stamp + 1);
2。長いサイクル時間と高いオーバーヘッド:
スピンCAS(失敗した場合、成功するまで実行されるまで実行されます)長い間失敗した場合、CPUに大きな実行をもたらします。 JVMがプロセッサが提供する一時型命令をサポートできる場合、効率はある程度改善されます。一時停止命令には2つの関数があります。まず、パイプラインの実行命令(パイプライン)を遅らせることができ、CPUが実行リソースをあまり消費しないようにします。遅延時間は、特定の実装バージョンに依存します。一部のプロセッサでは、遅延時間はゼロです。第二に、ループを終了するときにメモリオーダー違反によって引き起こされるCPUパイプラインフラッシュを回避することができ、それによりCPUの実行効率が向上します。
3.共有変数の原子操作のみを保証できます。
共有変数で操作を実行する場合、環状CASメソッドを使用して原子動作を確保できます。ただし、複数の共有変数を操作する場合、環状CASは動作の原子性を保証することはできません。現時点では、ロックを使用するか、複数の共有変数を共有変数にマージして動作させるトリックがあります。たとえば、2つの共有変数I = 2、j = a、マージIJ = 2aがあり、CASを使用してIJを操作します。 Java 1.5から始めて、JDKは、参照オブジェクト間の原子性を確保するためにAtomicReferenceクラスを提供します。 CAS操作のために複数の変数を1つのオブジェクトに配置できます。
CASおよび同期の使用シナリオ:
1.リソース競合が少ない状況(ライトスレッドの競合)の場合、スレッドブロッキングとウェイクアップスイッチングおよびユーザー状態のカーネル状態間のスイッチング操作に同期した同期ロックを使用して、CPUリソースの無駄です。 CASはハードウェアに基づいて実装されていますが、カーネルを入力する必要はなく、スレッドを切り替える必要はなく、スピンを操作する可能性は低いため、より高いパフォーマンスを得ることができます。
2。リソースの競争が深刻な状況(深刻な糸の競合)では、CASスピンの確率は比較的高く、CPUリソースがより多く、同期よりも効率が低くなります。
サプリメント:Synchronizedは、JDK1.6の後に改善および最適化されました。同期の基礎となる実装は、主にロックフリーのキューに依存しています。基本的なアイデアは、スピンをブロックし、競争の切り替え後もロックを競い合い続け、公平性をわずかに犠牲にしますが、高いスループットを獲得することです。スレッドの競合が少ない場合、同様のパフォーマンスを取得できます。深刻なスレッドの競合がある場合、パフォーマンスはCASのパフォーマンスよりもはるかに高くなります。
同時パッケージの実装:
JavaのCASには、揮発性読み取りと揮発性の書き込みのメモリセマンティクスの両方があるため、Javaスレッド間で通信する4つの方法があります。
1.スレッドAは揮発性変数を書き込み、スレッドBは揮発性変数を読み取ります。
2.スレッドAは揮発性変数を書き込み、スレッドBはCASを使用して揮発性変数を更新します。
3.スレッドAはCASを使用して揮発性変数を更新し、スレッドBはCASを使用してこの揮発性変数を更新します。
4.スレッドAはCASを使用して揮発性変数を更新し、スレッドBはこの揮発性変数を読み取ります。
Java's CASは、メモリを原子的に読み取り変化の操作を実行する最新のプロセッサで提供される機械レベルの効率的な原子命令を使用します。これは、マルチプロセッサで同期を実現するための鍵です(基本的に、アトミック変化のワイト命令をサポートできるコンピューターマシンは、アトモスのパフォーマンスをサポートするために現代のマシンをサポートするために同等のマシンをサポートするため、それを補助することになります。メモリ上のアトミック読み取りチェンジワイト操作)。同時に、揮発性変数の読み取り/書き込みとCASは、スレッド間の通信を実現できます。これらの機能を統合することは、同時パッケージ全体の実装の基礎を形成します。同時パッケージのソースコード実装を慎重に分析すると、一般的な実装パターンがあります。
1.最初に、共有変数が揮発性であると宣言します。
2。次に、CASの原子状態更新を使用して、スレッド間の同期を実現します。
3。同時に、揮発性の読み取り/書き込みと揮発性の読み取りと書き込みの記憶セマンティクスを使用することにより、スレッド間の通信が達成されます。
AQ、非ブロッキングデータ構造、アトミック変数クラス(java.util.concurrent.atomicパッケージのクラス)、これらの同時パッケージの基本クラスはこのパターンを使用して実装され、同時パッケージの高レベルクラスはこれらの基本クラスに依存して実装します。一般的な観点から、同時パッケージの実装図は次のとおりです。
CAS(ヒープ内のオブジェクトの割り当て):
Javaはnew object()を呼び出してオブジェクトを作成します。これはJVMヒープに割り当てられます。では、このオブジェクトはどのようにヒープに保存されていますか?
まず第一に、 new object()が実行されると、Javaのさまざまなデータ型と摂取するスペースが固定されているため、このオブジェクトが必要とするスペースの量が実際に決定されます(その原則について明確でない場合は、自分でグーグルで検索してください)。次の仕事は、このオブジェクトを保存するためにヒープ内のスペースの一部を見つけることです。
単一スレッドの場合、通常、2つの割り当て戦略があります。
1。ポインター衝突:これは一般に、完全に規則的なメモリに適用されます(メモリが通常のかどうかは、メモリリサイクル戦略によって異なります)。スペースを割り当てるタスクは、フリーメモリの側面のオブジェクトサイズの距離のようにポインターを移動することです。
2。フリーリスト:これは、非正規メモリに適しています。この場合、JVMはメモリリストを維持して、どのメモリ領域が無料で、どのサイズのメモリ領域を記録します。スペースをオブジェクトに割り当てるときは、無料リストに移動して適切な領域を照会し、割り当てます。
ただし、JVMが常に単一のスレッド状態で実行することは不可能であるため、効率が低すぎます。メモリを別のオブジェクトに割り当てるときは原子操作ではないため、少なくとも次の手順が必要です。無料リストの検索、メモリの割り当て、フリーリストの変更などは安全ではありません。並行性中にセキュリティの問題を解決するための2つの戦略もあります。
1。CAS:実際、仮想マシンはCASを使用して、再試行に失敗して更新操作の原子性を確保し、原則は上記と同じです。
2。TLAB:CASが使用された場合、実際にパフォーマンスに影響を与えるため、JVMはより高度な最適化戦略を提案しています。各スレッドは、ローカルスレッド割り当てバッファー(TLAB)と呼ばれるJavaヒープの小さなメモリを事前に割り当てます。スレッドがメモリ内にメモリを割り当てる必要がある場合、スレッドの競合を回避して、TLABに直接割り当てるだけで十分です。バッファメモリが使用され、メモリを再配分する必要がある場合にのみ、より大きなメモリスペースを割り当てるためにCAS操作が実行されます。
Virtual Machineが使用するかどうかは、TLABを-XX:+/-UseTLABパラメーター(JDK5以降のバージョンはデフォルトで有効になります)を介して構成できます。
上記はこの記事のすべての内容です。みんなの学習に役立つことを願っています。誰もがwulin.comをもっとサポートすることを願っています。