序文
最近、Java 並行プログラミングの関連するソース コードを調べています。単語が見つかりました - Treiber スタック。この記事では、Treiber スタックとそのコア アルゴリズム CAS とは何かを紹介します。
たとえば、FutureTask では次のようになります。
/** Treiber stack of waiting threads */
private volatile WaitNode waiters;
1. Treiber スタックの起源
Treiber スタックは、R. Kent Treiber の 1986 年の論文 Systems Programming: Coping with Parallelism (Systems Programming: Coping with Parallelism) で最初に登場しました。これはロックフリーの並行スタックであり、そのロックフリー機能は、CAS (Compare and swap: compare and exchange) アルゴリズムによって実装されるアトミック操作に基づいています。
2. キャス
1.CASとは?
CAS: Compare and Swap、つまり比較と交換。楽観的ロックのロックフリーです。
このアルゴリズムには、次の 3 つのオペランドが含まれます。
- 読み書きが必要なメモリ位置 V
- 比較する必要がある期待値 A
- 書き込む必要がある新しい値 U
CAS アルゴリズム解析: 具体的に CAS を実行すると、期待値 A がメモリ アドレス V に格納されている値と一致する場合にのみ、古い値が新しい値 U に置き換えられ、メモリ アドレス V に書き込まれます。それ以外の場合は更新しないでください。
プロセスは次のとおりです。
2. Java での CAS
Java15 並行プログラミングでは、java.util.concurrent.atomic.* の下に、基本的なデータ型に基づく CAS 実装がいくつかあります。ここでは、例として AtomicBoolean を取り上げます。
public class AtomicBoolean implements java.io.Serializable {
private static final long serialVersionUID = 4654671469794556979L;
//VarHandle 之后会写一篇文章对比 Unsafe 的访问方法
//可以先粗浅的理解为,获取到了 AtomicBoolean 实例的变量。
private static final VarHandle VALUE;
static {
try {
MethodHandles.Lookup l = MethodHandles.lookup();
VALUE = l.findVarHandle(AtomicBoolean.class, "value", int.class);
} catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
}
}
public final boolean compareAndSet(boolean expectedValue, boolean newValue) {
return VALUE.compareAndSet(this,
(expectedValue ? 1 : 0),
(newValue ? 1 : 0));
}
}
VarHandle クラスにジャンプします。
public final native
@MethodHandle.PolymorphicSignature
@HotSpotIntrinsicCandidate
boolean compareAndSet(Object... args);
public final native
@MethodHandle.PolymorphicSignature
@HotSpotIntrinsicCandidate
boolean weakCompareAndSet(Object... args);
これはネイティブ メソッド、つまり、Java によって外部的に実装された基になる操作であることがわかりました。特定の c 実装については後で説明します。
3. CASの問題点
- ABA の質問
- スレッド 1 とスレッド 2 は、メイン メモリの値 A を取得し、それをそれぞれのメモリ セグメントにコピーして、計算を実行します。
- スレッド 2 の計算プロセスでは、スレッド 1 が最初に計算を完了し、更新された値 B をメイン メモリに更新します。
- スレッド 2 の計算プロセス中に、スレッド 3 に参加します。
- スレッド 2 の計算プロセス中に、スレッド 3 はメイン メモリの値 B をコピーし、計算を完了して、更新された値 A をメイン メモリに更新します。
- この時点で、スレッド 2 は計算を完了し、CAS アルゴリズムに従って、スレッド 2 は更新された値 C をメイン メモリに更新します。
この問題は、データに強い意味を持つビジネス シナリオでデータ エラーにつながります。
例: 2 人のユーザー A と B が e コマース プラットフォームで iPhone を手に取り、ユーザー A がそれを手に入れましたが、ユーザー B はインターネット アクセスが不十分なために実際には手に入れませんでしたが、この時点でユーザー C は iPhone を返しました。最終的な結果は、ユーザー A と B の両方が iPhone を手に入れるということですが、実際のビジネス上の意味とデータは逸脱しています。
- 長期的なスレッド スピンは高価です
CPU (シングル コア) にプリエンプティブ スケジューラがない場合 (つまり、スレッドが他のスレッドを実行するためにクロックによって中断される場合)、スレッド スピンは CPU リソースを解放せず、常にアクセスします。ターゲット メモリ セグメント。短時間のスレッドのスピンはかなり費用対効果の高い選択ですが、複数のスレッドが長時間スピンすると、CPU に負担がかかります。
3. FutureTask のロックフリー並行スタック実装
最初に戻る:
/** Treiber stack of waiting threads */
private volatile WaitNode waiters;
WaitNode とは何かを見てみましょう。
static final class WaitNode {
volatile Thread thread;
volatile WaitNode next;
WaitNode() {
thread = Thread.currentThread(); }
}
WaitNode は FutureTask の内部クラスであり、その構造は一方向。
もう一度検索してみましょう。waiters 変数の使用関数です。当面はそのビジネス上の重要性について心配する必要はありません。これはスタックであるため、最も重要な手順を見てみましょう。
- ノードがスタックにプッシュされ、ポインタがトップ ノードを指している:
q がスタックにプッシュされたとき、次のポインタがウェイターを指しているかどうかを確認し、それが別のノードである場合、スレッドが安全ではない (改ざんされた) ことを証明します。 )、queued = false で、次のサイクルで再試行します。
private int awaitDone(boolean timed, long nanos)
throws InterruptedException {
//……………………
else if (!queued)
queued = WAITERS.weakCompareAndSet(this, q.next = waiters, q);
//……………………
}
- ノードはスタックからポップアウトされ、ポインターはトップノードを指します:
q がスタックからポップアウトされると、現在のウェイターが q であるかどうかを確認し、そうでない場合は、スレッドが安全ではない (改ざんされた) ことを証明します。と)、スピンブロックが待機します。
private void removeWaiter(WaitNode node) {
//……………………
s = q.next;
//……………………
else if (!WAITERS.compareAndSet(this, q, s))
continue retry;
//……………………
}
- スタックがクリアされます。
ダイレクト ポインターは null を指し、gc はメモリ内に浮かんでいるウェイター リストを再利用します。
private void finishCompletion() {
//……………………
if (WAITERS.weakCompareAndSet(this, q, null)) {
//……………………
}
//……………………
}
以上がロックフリーのコンカレントスタックの実装です.コアはCASアルゴリズムで動作するスタックの実装です.じっくりと体験してください.
compareAndSet と weakCompareAndSet の違いについては、次の記事を参照してください。