LongAdderとLongAccumulator(Java)
1概要
この記事では、
java.util.concurrent
パッケージからの2つの構成要素を調べます。
LongAdder
および
LongAccumulator
.
どちらもマルチスレッド環境で非常に効率的になるように作成されており、どちらも非常に賢い戦術を利用して** ロックフリーでありながらスレッドセーフです。
2
LongAdder
AtomicLong
を使用することがボトルネックになる可能性がある場合、いくつかの値を頻繁にインクリメントするロジックを考えてみましょう。これは、比較とスワップ操作を使用します。これは、激しい競合の下では、多くのCPUサイクルの浪費につながる可能性があります。
一方、
LongAdder
は、スレッド間の競合を減らすために非常に巧妙なトリックを使用します。
LongAdderのインスタンスをインクリメントしたい場合は、
increment()__メソッドを呼び出す必要があります。その実装は
要求に応じて成長することができる
カウンタの配列を保持します。
そのため、より多くのスレッドが
increment()
を呼び出すと、配列は長くなります。配列内の各レコードは別々に更新できるため、競合が少なくなります。そのため、
LongAdder
は、複数のスレッドからカウンタを増分するための非常に効率的な方法です。
LongAdder
クラスのインスタンスを作成し、それを複数のスレッドから更新しましょう。
LongAdder counter = new LongAdder();
ExecutorService executorService = Executors.newFixedThreadPool(8);
int numberOfThreads = 4;
int numberOfIncrements = 100;
Runnable incrementAction = () -> IntStream
.range(0, numberOfIncrements)
.forEach(i -> counter.increment());
for (int i = 0; i < numberOfThreads; i++) {
executorService.execute(incrementAction);
}
LongAdder
のカウンタの結果は、
sum()
メソッドを呼び出すまで利用できません。そのメソッドは、下の配列のすべての値を反復処理し、それらの値を合計して適切な値を返します。
sum()
メソッドの呼び出しは非常にコストがかかる可能性があるため、注意が必要です。
assertEquals(counter.sum(), numberOfIncrements ** numberOfThreads);
ときには、
sum()
を呼び出した後、
LongAdder
のインスタンスに関連付けられているすべての状態を消去して、最初からカウントを開始したい場合があります。これを実現するには、
sumThenReset()
メソッドを使用します。
assertEquals(counter.sumThenReset(), numberOfIncrements ** numberOfThreads);
assertEquals(counter.sum(), 0);
その後の__sum()メソッドの呼び出しはゼロを返し、状態が正常にリセットされたことを意味します。
3ロングアキュムレータ
LongAccumulator
も非常に興味深いクラスです。これにより、ロックフリーのアルゴリズムをさまざまなシナリオで実装できます。たとえば、提供された
LongBinaryOperator
に従って結果を累積するために使用できます。これは、Stream APIの
reduce()
操作と同様に機能します。
LongAccumulator
のインスタンスは、
LongBinaryOperator
と初期値をそのコンストラクターに指定することで作成できます。 **
LongAccumulator
は、累積の順序が重要ではない可換関数を使って指定すると正しく機能することを覚えておくことが重要です。
LongAccumulator accumulator = new LongAccumulator(Long::sum, 0L);
LongAccumulator
wi
__ch
を作成すると、アキュムレータに既に存在していた値に新しい値が追加されます。
LongAccumulator
の初期値をゼロに設定しているので、
accumulate()
メソッドの最初の呼び出しでは、
previousValue__の値はゼロになります。
複数のスレッドから
accumulate()
メソッドを呼び出しましょう。
int numberOfThreads = 4;
int numberOfIncrements = 100;
Runnable accumulateAction = () -> IntStream
.rangeClosed(0, numberOfIncrements)
.forEach(accumulator::accumulate);
for (int i = 0; i < numberOfThreads; i++) {
executorService.execute(accumulateAction);
}
accumulate()
メソッドの引数として数値を渡していることに注目してください。そのメソッドは、
sum()
関数を呼び出します。
LongAccumulator
はcompare-and-swap実装を使用しています – これはこれらの興味深い意味論につながります。
まず、
LongBinaryOperatorとして定義されているアクションを実行し、次に
previousValue__が変更されたかどうかを確認します。変更された場合は、新しい値でアクションが再度実行されます。そうでなければ、アキュムレータに格納されている値を変更することに成功します。
これで、すべての反復からのすべての値の合計が
20200
であると主張できます。
assertEquals(accumulator.get(), 20200);
4結論
このクイックチュートリアルでは、
LongAdder
と
LongAccumulator
を見て、両方の構成要素を使用して非常に効率的でロックフリーなソリューションを実装する方法を示しました。
これらすべての例とコードスニペットの実装はhttps://github.com/eugenp/tutorials/tree/master/core-java-concurrency[GitHubプロジェクト]にあります – これはMavenプロジェクトです。そのままインポートして実行するのは簡単です。