LongAdderとLongAccumulator(Java)

JavaのLongAdderおよびLongAccumulator

1. 概要

この記事では、java.util.concurrentパッケージの2つの構成要素(LongAdderLongAccumulator.)について説明します。

どちらもマルチスレッド環境で非常に効率的になるように作成されており、非常に巧妙な戦術を活用してlock-free and still remain thread-safe.になります。

2. LongAdder

AtomicLongを使用するとボトルネックになる可能性がある、いくつかの値を非常に頻繁にインクリメントするロジックについて考えてみましょう。 これは、比較とスワップ操作を使用します。これは、激しい競合の下で、多くのCPUサイクルの浪費につながる可能性があります。

一方、LongAdderは非常に巧妙なトリックを使用して、スレッドが増加しているときにスレッド間の競合を減らします。

LongAdder,のインスタンスをインクリメントする場合は、increment()メソッドを呼び出す必要があります。 その実装keeps an array of counters that can grow on demand

したがって、より多くのスレッドが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);
}

sum()メソッドを呼び出すまで、LongAdderのカウンターの結果は利用できません。 そのメソッドは、下の配列のすべての値を反復処理し、それらの値を合計して適切な値を返します。 ただし、sum()メソッドの呼び出しには非常にコストがかかる可能性があるため、注意が必要です。

assertEquals(counter.sum(), numberOfIncrements * numberOfThreads);

場合によっては、sum()を呼び出した後、LongAdderのインスタンスに関連付けられているすべての状態をクリアして、最初からカウントを開始したいことがあります。 これを実現するには、sumThenReset()メソッドを使用できます。

assertEquals(counter.sumThenReset(), numberOfIncrements * numberOfThreads);
assertEquals(counter.sum(), 0);

後続のsum()メソッドの呼び出しはゼロを返すことに注意してください。これは、状態が正常にリセットされたことを意味します。

3. LongAccumulator

LongAccumulatorも非常に興味深いクラスです。これにより、さまざまなシナリオでロックフリーアルゴリズムを実装できます。 たとえば、提供されたLongBinaryOperatorに従って結果を累積するために使用できます。これは、Stream APIからのreduce()操作と同様に機能します。

LongAccumulatorのインスタンスは、LongBinaryOperatorとそのコンストラクターに初期値を指定することで作成できます。 LongAccumulator will work correctly if we supply it with a commutative function where the order of accumulation does not matter.を覚えておくべき重要なこと

LongAccumulator accumulator = new LongAccumulator(Long::sum, 0L);

LongAccumulatorを作成していますが、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は、コンペアアンドスワップの実装を使用しています。これにより、これらの興味深いセマンティクスが実現します。

まず、LongBinaryOperator,として定義されたアクションを実行し、次にpreviousValueが変更されたかどうかを確認します。 変更された場合、アクションは新しい値で再度実行されます。 そうでない場合、アキュムレータに保存されている値の変更に成功します。

これで、すべての反復からのすべての値の合計が20200であったと断言できます。

assertEquals(accumulator.get(), 20200);

4. 結論

このクイックチュートリアルでは、LongAdderLongAccumulatorを確認し、両方の構成を使用して非常に効率的でロックフリーのソリューションを実装する方法を示しました。

これらすべての例とコードスニペットの実装は、GitHub projectにあります。これはMavenプロジェクトであるため、そのままインポートして実行するのは簡単です。