blog

LongAdderより効率的なロックフリーの実装

LongAdderは、並行処理を減らすために多くの工夫をしています。そして、すべてのステップで、「より良い方法がない」場合にのみ、より高価な演算を選択し、可能な限り単純な方法で演算を完了するようにして...

May 11, 2015 · 15 min. read
シェア

AtomicLongに連絡した理由は、guavaのLoadingCache関連のコードを見て、LoadingCacheについて、実際には、アイデアも非常に単純明快です:キャッシュを解決するためのテンプレートパターンは、データを取得するロジックにヒットしない、このアイデアは、私はまた、以前のプロジェクトで使用されることが起こる。

  1. 彼は、これはAtomicLongよりも効率的だと言っています。

AtomicLongはすでに非常に優れたソリューションで、並行処理が必要な場合は、CAS演算を使ってハードウェア・レベルで比較とセットを行います。これは非常に効率的です。

そこで、なぜLongAdderがAtomicLongより効率的なのかを調べることにしました。

まず、LongAdderの継承ツリーを見てください:

Striped64から継承されたこのクラスは、いくつかの非常に重要な内部クラスと操作をラップしています。それは後で説明します。

その前に、AtomicLongは内部値変数で実装されており、複数のスレッドが同時にインクリメントとデクリメントを行う場合、マシン命令レベルからCAS命令によって操作され、同時実行の原子性が保証されることを知っておくことが重要です。

LongAdderメソッドをもう一度見てください:



AtomicLongと比較されるのも不思議ではありません。任意のAPIを選んで分析を始めると、このAPIはパスし、他のAPIもすべて似ているので、addメソッドを選びます。実際、他のすべてのAPIはこのメソッドに依存しています。



LongAdderはCell配列を含んでいます。CellはStriped64の内部クラスで、その名の通り、Cellは最小単位を表します。まずは定義を見てみましょう:



Cellは内部に非常に重要な値を持つ変数を持ち、CASがその値を更新するためのメソッドを提供します。

追加メソッドに戻ります:

AtomicLongはすでにCAS命令を使っており、非常に効率的ですが、LongAdderはまだCAS命令を使って値を更新するのであれば、どうしてAtomicLongより効率的なのでしょうか? 言うまでもなく、多くの内部判定があります!

それが私が始めたときの疑問でした。 その疑問を胸に、続きを。

***if判定では、***呼び出し時にcells配列はnullでなければならないので、casBaseメソッドに行きます:



更新が成功すればローカルコールが戻り始め、そうでなければブランチの中に入ります。

更新が失敗するのはどんな時ですか? AtomicLongは、更新の試行をデッドエンドにし、成功するまで返さないことでこれを処理します。

ブランチ内部では、スレッドローカル変数 threadHashCode を介して HashCode オブジェクトが取得されます:



ゼロ以外の乱数値を保持するコード変数があります。

追加メソッドに戻ります:

スレッドに関連付けられているHashCodeオブジェクトを取得した後、そのコード変数を取得します。

Cells配列内の現在のスレッドのHashCodeのインデックス位置を計算し、その位置にあるCellオブジェクトを取り出し、その値をCASで更新します。

もちろん、as が NULL で更新に失敗した場合は、retryUpdate メソッドが使用されます。

なぜLongAdderがAtomicLongよりも効率的なのか、多くの人が理解しているはずです。そうです、AtomicLongの効率的な理由の唯一の制約は高い同時実行性です。AtomicLong。LongAdderは、解決策を考えるのは非常に簡単です:同時実行を減らす、複数の値に単一の値を更新する圧力を共有し、単一の値の "ホットネス "を減らす、セグメント化された更新!

こうすることで、スレッド数が多くても複数の値に分散され、値を増やすだけで「熱さ」が下がるので、AtomicLongの悪循環が解消されます。 cellは「セグメント」であり、更新された値を保持するのはcell内の値なので、合計が必要なときはcell内の値を足し算すればいいだけです!

もちろん、巧妙さはここだけでなく、追加メソッドのコードを見て、casBaseメソッドは、直接分割の更新を行うことはできません、インデックスの位置を計算するために来て、値を更新しますか?

答えはよくない、ないため、casBase操作は、AtomicLong CAS操作と等価である、知って、LongAdderこのような方法は、悪い対処するために、セグメンテーション操作は必然的にスペースの無駄をもたらす、あなたは時間のためのスペースが、、、変更しないように変更することはできませんが、スペースと時間を節約している見て〜! したがって、casBaseの操作は、低同時性では、すぐにセグメント化された更新操作を行うには、ブランチを入力しないことを保証するため、低同時性、casBaseの操作は基本的に成功するため、ある程度まで高同時性は、ブランチを入力しますので、クラスの説明上のダグレアです:低同時性LongAdderとAtomicLongのパフォーマンスが似ている、高同時性LongAdderは、より効率的です!

しかし、ドウング・リーはまだそれほど単純ではありません。

この場合、retryUpdateで何が起こっているかは一目瞭然です。retryUpdateは、セルの値が更新に失敗したときや、セルの配列が空になったときに呼び出されるからです。

そのため、retryUpdateの内部では2つのことを行う必要があります:

  1. セルのアレイを拡張するエクスパンションは、セルあたりの同時実行量を減らし、これもまたセルのアレイのリハッシュ動作を意味します。
  2. 新しい Cell 配列を空の cells 変数に代入します。

そうでしょう? コードを見続けてください:

コードはかなり長いので、テキストにしてみましょう。if elseの分岐を見やすくするために、対応する{ }を同じ色でマークしています。見ての通り、これはDoug Leaが更新を確実に成功させるためにデッドループを使うことを厭わないポイントです!

final void retryUpdate(long x, HashCode hc, boolean wasUncontended) {   
      int h = hc.code;   
      boolean collide = false;                // True if last slot nonempty   
      for (;;) {   
          Cell[] as; Cell a; int n; long v;   
          if ((as = cells) != null && (n = as.length) > 0) {// 分岐1   
              if ((a = as[(n - 1) & h]) == null) {   
                  if (busy == 0) {            // Try to attach new Cell   
                      Cell r = new Cell(x);   // Optimistically create   
                      if (busy == 0 && casBusy()) {   
                          boolean created = false;   
                          try {               // Recheck under lock   
                              Cell[] rs; int m, j;   
                              if ((rs = cells) != null &&   
                                      (m = rs.length) > 0 &&   
                                      rs[j = (m - 1) & h] == null) {   
                                  rs[j] = r;   
                                  created = true;   
                              }   
                          } finally {   
                              busy = 0;   
                          }   
                          if (created)   
                              break;   
                          continue;           // Slot is now non-empty   
                      }   
                  }   
                  collide = false;   
              }   
              else if (!wasUncontended)       // CAS already known to fail   
                  wasUncontended = true;      // Continue after rehash   
              else if (a.cas(v = a.value, fn(v, x)))   
                  break;   
              else if (n >= NCPU || cells != as)   
                  collide = false;            // At max size or stale   
              else if (!collide)   
                  collide = true;   
              else if (busy == 0 && casBusy()) {   
                  try {   
                      if (cells == as) {      // Expand table unless stale   
                          Cell[] rs = new Cell[n << 1];   
                          for (int i = 0; i < n; ++i)   
                              rs[i] = as[i];   
                          cells = rs;   
                      }   
                  } finally {   
                      busy = 0;   
                  }   
                  collide = false;   
                  continue;                   // Retry with expanded table   
              }   
              h ^= h << 13;                   // Rehash  h ^= h >>> 17;   
              h ^= h << 5;   
          }   
          else if (busy == 0 && cells == as && casBusy()) {//第2枝   
              boolean init = false;   
              try {                           // Initialize table   
                  if (cells == as) {   
                      Cell[] rs = new Cell[2];   
                      rs[h & 1] = new Cell(x);   
                      cells = rs;   
                      init = true;   
                  }   
              } finally {   
                  busy = 0;   
              }   
              if (init)   
                  break;   
          }   
          else if (casBase(v = base, fn(v, x)))   
              break;                          // Fall back on using base   
      }   
      hc.code = h;                            // Record index for next time   
  } 

ブランチ2では、Cellsが空の場合、Cellsの配列を新規に作成する必要があります。

ブランチ1が分岐します:

busyというメソッドがいくつかの分岐で言及されていることに注意してください。これは、セル配列を更新する必要があるときだけ値を1に更新するCAS実装のロックと解釈できます。更新に失敗した場合は、現在セル配列を更新しているスレッドがあり、現在のスレッドは待つ必要があることを意味します。リトライ。

分岐1に戻って、まず現在のセル配列のインデックス位置にあるセル要素が空かどうかを判断し、空であれば配列にセルを追加します。

そうでない場合は、競合フラグwasUncontendedをtrueに更新して再試行します。

そうでなければ、もう一度セルの値を更新し、失敗したら再試行します。

。。。。。。。何度判定しても失敗する場合は、次善の策としてreHashを行い、セル配列のサイズを直接2倍にして、現在のスレッドのハッシュ値を更新し、次の更新ができるだけ成功するようにします。

お分かりのように、LongAdder は、並行処理を減らすために多くの工夫をしています。そして、すべてのステップで、「より良い方法がない」場合にのみ、よりオーバーヘッドの大きい処理を選択し、処理を完了するために可能な限り単純な方法を使用するようにしています。シンプルであるように努力しますが、決してそうではありません。

#p#

------- 陳昊ノート------

***残された疑問は2つ:

1) AtomicLongが廃止される可能性はありますか?

2) セル作成後も元のcasBaseが消えない場合、パフォーマンスは悪くなりますか?

-------水虫注意------

多くの問題が見つかっており、私自身が簡単にまとめました:

1.このクラスはjdk 1.7で利用できますか?

確認した結果、jdk-7u51にはまだありませんが、jdk-8u20にはあります。コードは基本的に同じですが、ダブル型のサポートが追加され、冗長なコードが削除されています。興味のある方は、JDK 1.8 lookをダウンロードしてください!

2.ベースはアグリゲーションに関与していましたか?

ベースは intValue などのメソッドを呼び出すときに集約されます:

3.セル作成後も元のcasBaseが消えない場合、性能は悪くなりますか? 塩基の順番を入れ替えることはできますか?

最初は、addメソッドでの判定の順番を入れ替えられないか、例えばcasBaseの判定を先にできないか、と考えていました。 よくよく考えてみると、入れ替えた後は毎回CASしないといけないし、同時並行性が高いと失敗する確率が非常に高くなって悪循環になるし、後者のオーバーヘッドは1回の判定より明らかに小さいし、副作用もないので、入れ替えない方がいいのかもしれません。したがって、スワップしないほうがよいでしょう。

4.AtomicLongは廃止できますか?

Read next

Linuxユーザーが知っておくべきコマンドラインの時間節約術

Linuxユーザが知っておくべき時間節約のヒントは何ですか?この記事では、技術的なユーザーにとってかなり重要で便利な、しかしあまり知られていないTipsをまとめています。興味のある方はご覧ください。

May 4, 2015 · 8 min read