1。同期問題を提案します
デュアルコアプロセッサを使用して2つのスレッドAとB、Core 1がスレッドAを実行し、Core 2がスレッドBを実行すると、両方のスレッドがOBJという名前のオブジェクトのメンバー変数Iに1を追加する必要があります。 Iの初期値は0であると仮定すると、理論的には、2つのスレッドが実行された後、Iの値は2になる必要がありますが、実際には結果が1になる可能性が非常に高いです。
今すぐ理由を分析しましょう。分析の簡単さのために、キャッシュの状況を考慮しません。実際、結果が1になる可能性を高めるキャッシュがあります。スレッドAは、メモリ内の変数Iをカーネル1算術操作ユニットに読み取り、追加操作を実行し、計算結果をメモリに書き戻します。上記の動作は原子動作ではないため、スレッドBがスレッドAの前にメモリ内の値を読み取る限り、スレッドAがメモリに1を追加することでiの値を書きます(Iの値はこの時点で0です)、Iの結果は間違いなく1です。
最も一般的なソリューションは、Synchronizeキーワードを使用して、2つのスレッドのivisibleコードに1を追加するコードでOBJオブジェクトをロックすることです。今日は、アトミックパッケージの関連クラスを使用してそれを解決するための新しいソリューションを紹介します。
2.原子ハードウェアサポート
単一のプロセッサシステム(Uniprocessor)では、単一の命令で完了することができる操作は「原子操作」と見なすことができます。これが、一部のCPU命令システムがTEST_AND_SET、TEST_AND_CLEAR、および重要なリソース相互排除のためのその他の命令を導入する理由でもあります。複数のプロセッサがシステムで独立して実行されているため、単一の命令で完了できる操作でさえ妨害される可能性があるため、対称的なマルチプロセッサ構造では異なります。
X86プラットフォームでは、CPUは、命令実行中にバスをロックする手段を提供します。 CPUチップにはリード#hlockpinがあります。接頭辞「ロック」がアセンブリ言語プログラムの命令に追加された場合、アセンブリマシンコードにより、この命令を実行するときにCPUが#hlockpinの可能性を低下させ、この命令の終了までリリースし、バスをロックします。このようにして、同じバスに乗っている他のCPUは、当面バスからメモリにアクセスできず、マルチプロセッサ環境でのこの命令の原子性を確保します。もちろん、すべての命令にロックで前に付けられるわけではありません。追加、ADC、および、BTC、BTR、BTS、CMPXCHG、DEC、INC、NEG、NOT、または、SBB、SUB、XOR、XADD、およびXCHG命令のみに、原子動作を実現するための「ロック」命令で接頭することができます。
Atomicのコア操作はCASです(CompareAndset、Atomic命令であるCMPXCHG命令を使用して実装されています)。この命令には、変数のメモリ値V(値の略語)、変数の現在の期待値e(例外の略語)、変数の値uが更新したい値(更新の略語)の3つのオペランドがあります。メモリ値が現在の期待値と同じ場合、変数の更新値は変数によって上書きされ、擬似コードは次のように実行されます。
if(v == e){v = u return true} else {return false}ここで、CAS操作を使用して上記の問題を解決します。スレッドBは、メモリ内の変数Iを一時変数に読み取ります(この時点で読み取られた値が0であると仮定)、次にIの値をCore1の算術演算単位に読み取ります。次に、1を追加して、一時変数の値がiの現在の値と同じかどうかを比較します。メモリ内のiの値が、操作ユニットの結果の値と同じである場合(つまり、I+1)(この部分はCAS操作であることに注意してください、それは原子操作であり、中断することはできず、他のスレッドでのCAS操作を同時に実行できません)。命令が失敗した場合、スレッドAがIの値を1増加したことを意味します。これから、両方のスレッドで読む値が最初に0である場合、CAS操作を同時に実行できないため、1つのスレッドのCAS操作のみが成功することがわかります。 CAS運用の実行に失敗したスレッドの場合、CASの操作がループルに実行される限り、それは間違いなく成功します。スレッドブロッキングはないことがわかります。これは、同期の原理とは本質的に異なります。
3。アトミックパッケージおよびソースコード分析の紹介
アトミックパッケージのクラスの基本的な特徴は、マルチスレッド環境では、複数のスレッドが単一の(基本タイプと参照タイプを含む)変数で動作する場合、複数のスレッドが同じ時間に変数の値を更新すると、1つのスレッドのみが成功し、無関係なスレッドがスピンロックのように試行され続けることができます。
Atomic Seriesクラスのコアメソッドは、安全でないクラスのいくつかのローカルメソッドを呼び出します。最初に、1つのことが安全でないクラスであり、そのフルネームがあることを知っておく必要があります:sun.misc.unsafe。このクラスには、多くの直接メモリの割り当てや原子操作の呼び出しを含む、Cコードの多数の操作が含まれています。それが非セキュアとしてマークされている理由は、この領域の多数のメソッド呼び出しがセキュリティリスクがあり、慎重に使用する必要があることを伝えることです。そうしないと、深刻な結果につながります。たとえば、安全でないことにメモリを割り当てる場合、特定の領域を自分で指定すると、C ++などのいくつかのポインターが境界を他のプロセスに渡ることがあります。
アトミックパッケージのクラスは、運用データ型に従って4つのグループに分けることができます。
AtomicBoolean,AtomicInteger,AtomicLong
スレッドセーフの原子運転の基本タイプ
AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
配列全体ではなく、配列内の単一の要素で動作するアレイタイプのスレッドセーフ原子動作
AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater
反射原理オブジェクトの基本タイプ(長い整数、整数、参照タイプ)に基づくスレッドセーフ操作
AtomicReference,AtomicMarkableReference,AtomicStampedReference
ABAの問題を防ぐ参照タイプのスレッドセーフ参照タイプと原子操作
通常、AtomicInteger、AtomicReference、AtomicStampedReferenceを使用します。次に、アトミックパッケージの原子整数のソースコードを分析しましょう。他のクラスのソースコードは、原則として類似しています。
1。パラメーターコンストラクター
public AtomicInteger(int initialValue){value = initialValue;}コンストラクター関数からわかるように、値はメンバー変数値に保存されます
民間の揮発性INT値。
メンバー変数値は揮発性タイプとして宣言されます。これは、複数のスレッドでの可視性を示しています。つまり、任意のスレッドの変更はすぐに他のスレッドで見られます。
2.比較方法(値の値は内部に渡され、valueOffset)
Public Final Boolean CompareAndset(int expect、int update){unsafe.compareandswapint(this、value offset、expect、update);}この方法は、最もコアCAS操作です
3.比較メソッドが呼び出されたGetandSetメソッド
public final int getandset(int newValue){for(;;){int current = get(); if(compareandset(current、newValue))戻り電流。 }}if(compareandset(current、newValue))を実行する前に他のスレッドが値の値を変更する場合、値の値は現在の値と異なる必要があります。比較アンドセットが実行に失敗した場合、値の値のみを再獲得し、成功するまで比較し続けることができます。
4。I++の実装
public final int getandincrement(){for(;;){int current = get(); int next = current + 1; if(compareandset(current、next))return current; }}5。++ iの実装
public final int incrementAndget(){for(;;){int current = get(); int next = current + 1; if(compareandset(current、next))は次を返します。 }}4. AtomicIntegerの例を使用します
次のプログラムでは、AtomicIntegerを使用してチケット販売プログラムをシミュレートします。 2つのプログラムは、ランニング結果で同じチケットを販売することはなく、チケットをネガティブとして販売することもありません。
javaleanningをパッケージ; java.util.concurrent.atomic.atomicinteger; public class selltickets {atomicintegerチケット= new AtomicInteger(100);クラスの売り手はrunnable {@override public void run(){while()> 0){int tmp = tmp = tmp.get tmp-1)){system.out.println(thread.currentthread()。getname()+""+tmp);}}}}} {selltickets st = new selltickets(); new Stread(); s.new sell()、 "s.nw"); new sell(); new sell(); "sellerb")。start();}}5。ABA問題
上記の例では、結果が完全に正しいです。これは、2つ(またはそれ以上)のスレッドが同じ方向のデータで動作するという事実に基づいています。上記の例では、両方のスレッドがDECREMENTでチケットで動作します。たとえば、複数のスレッドが共有キューでオブジェクト登録操作を実行する場合、AtomicReferenceクラスを通じて正しい結果を取得できます(これは実際にAQで維持されているキューの場合です)。ただし、複数のスレッドを登録または登録することができます。つまり、データの動作方向は一貫性がないため、ABAが発生する可能性があります。
次に、ABAの問題を説明するために、比較的簡単に理解しやすい例を挙げましょう。 2つのスレッドT1とT2があり、これらの2つのスレッドが同じスタックでスタッキングおよびスタッキング操作を実行するとします。
AtomicReferenceによって定義されたテールを使用して、スタックの上位位置を保存します
AtomicReference <T>尾;
T1スレッドが展開される準備ができていると仮定すると、積み重ね操作のために、図1に示すように、SPからCASの操作を通じてSPのトップポジションを更新するだけです。ただし、T1スレッドがTail.comPareandset(SP、NEWSP)を実行する前に、システムはスレッドスケジューリングを実行し、T2スレッドは実行を開始します。 T2は3つの操作を実行します。Aはスタックから外れ、Bはスタックから外れ、Aはスタック上にあります。この時点で、システムは再びスケジューリングを開始し、T1スレッドはスタッキング操作を実行し続けますが、T1スレッドのビューでは、スタックの上部の要素はまだAです(つまり、T1はまだBがまだスタックAの次の要素であると考えています)、実際の状況は図2に示されています)。スタックの上のポインターは、ノードBを指しています。実際、Bはスタックに存在しなくなりました。 T1がスタックからアウトした後の結果を図3に示しますが、これは明らかに正しい結果ではありません。
6。ABA問題の解決策
AtomicMarkableReference、AtomicStampedReferenceを使用します。上記の2つの原子クラスを使用して、操作を実行します。比較命令を実装する場合、オブジェクトの以前の値と期待値を比較するだけでなく、現在の(操作)スタンプ値と予想される(操作)スタンプ値を比較する必要があります。すべて同じことが真である場合にのみ、比較メソッドが成功することができます。更新が成功するたびに、スタンプ値が変更され、スタンプ値の設定はプログラマー自身によって制御されます。
public boolean CompareAndset(v expectionReference、v newReference、int hessidestStamp、int newstamp){pair <v> current = pair; return expectreference == current.reference && spectedstamp == current.Stamp &&((newReference == current.reference && newStamp == current.stamp)|| caspair(newReference);現時点では、比較アンドセットメソッドには、予想されるリファレンス、newReference、expectsStamp、newStampの4つのパラメーターが必要です。この方法を使用する場合、予想されるスタンプ値が更新スタンプ値と同じでないことを確認する必要があります。通常、newStamp = expectsStamp+1
上記の例を見てください
スレッドT1がスタックの前にあると仮定します:SPがAを指し、スタンプ値が100であるとします。
スレッドT2の実行:Aがリリースされた後、SPはBを指し、スタンプ値は101になります。
Bが解放された後、SPはCを指し、スタンプ値は102になります。
Aがスタックに入れられた後、SPはAを指し、スタンプ値は103になります。
スレッドT1は比較ステートメントを実行し続け、SPはまだAを指していますが、Stamp値100の期待値は現在の値103とは異なることを発見します。したがって、比較アンドセットは失敗します。 Newspの価値を取得する必要があります(現時点では、NewspはCを指します)、およびStamp値103の期待値を使用してから、比較操作を再度実行します。このようにして、Aはスタックを正常にロールアウトし、SPはCを指します。
CompareAndsetは一度に1つの値のみを変更できず、newReferenceとNewStampを同時に変更できないため、実装中にペアクラスが内部で定義され、NewReferenceとNewStampを1つのオブジェクトに変換することに注意してください。 CAS操作を実行するとき、それは実際にはペアオブジェクトの操作です。
private static classペア<t> {final t Reference;最終的なINTスタンプ。プライベートペア(tリファレンス、intスタンプ){this.reference = reference; this.stamp = stamp; } static <t> pair <t> of(t reference、int stamp){return new pair <t>(参照、スタンプ); }}AtomicMarkableReferenceの場合、Stamp値はブール変数であり、AtomicStampedReferenceのStamp値は整数変数です。
要約します
上記は、Javaの原子パッケージの実装原則とアプリケーションに関するこの記事の簡単な議論に関するものです。私はそれが誰にでも役立つことを願っています。興味のある友人は、このサイトの他の関連トピックを引き続き参照できます。欠点がある場合は、それを指摘するためにメッセージを残してください。