1. 概要

ランダムな値を生成することは非常に一般的なタスクです。 これが、Javaがjava.util.Randomクラスを提供する理由です。

ただし、このクラスはマルチスレッド環境ではうまく機能しません。

簡単に言うと、マルチスレッド環境で Random のパフォーマンスが低下する理由は、複数のスレッドが同じRandomインスタンスを共有している場合の競合によるものです。

この制限に対処するために、JavaはJDK7にjava.util.concurrent.ThreadLocalRandomクラスを導入しました–マルチスレッド環境で乱数を生成するために

ThreadLocalRandom のパフォーマンスと、実際のアプリケーションでの使用方法を見てみましょう。

2. ThreadLocalRandom Over Random

ThreadLocalRandomは、ThreadLocalクラスとRandomクラスの組み合わせであり(これについては後で詳しく説明します)、現在のスレッドに分離されます。したがって、のインスタンスへの同時アクセスを回避するだけで、マルチスレッド環境でのパフォーマンスが向上します。 X267X]ランダム。

java.util.Random はグローバルに乱数を提供しますが、一方のスレッドによって取得された乱数はもう一方のスレッドの影響を受けません。

また、 Randomとは異なり、 ThreadLocalRandomはシードの明示的な設定をサポートしていません。 代わりに、 Randomから継承されたsetSeed(long seed)メソッドをオーバーライドして、呼び出された場合に常にUnsupportedOperationExceptionをスローします。

2.1. スレッドの競合

これまでのところ、ランダムクラスは、高度な同時環境ではパフォーマンスが低いことを確認しています。 これをよりよく理解するために、その主要な操作の1つである next(int)がどのように実装されているかを見てみましょう。

private final AtomicLong seed;

protected int next(int bits) {
    long oldseed, nextseed;
    AtomicLong seed = this.seed;
    do {
        oldseed = seed.get();
        nextseed = (oldseed * multiplier + addend) & mask;
    } while (!seed.compareAndSet(oldseed, nextseed));

    return (int)(nextseed >>> (48 - bits));
}

これは、線形合同法アルゴリズムのJava実装です。 すべてのスレッドが同じシードインスタンス変数を共有していることは明らかです。

次のランダムなビットセットを生成するために、最初に共有シード値をcompareAndSetまたはCASの略でアトミックに変更しようとします。

複数のスレッドがCASを使用してシードを同時に更新しようとすると、1つのスレッドが勝ってシードを更新し、と残りは負けます。 スレッドを失うと、値を更新する機会が得られるまで、同じプロセスが何度も試行され、 最終的にランダムな数を生成します。

このアルゴリズムはロックフリーであり、異なるスレッドが同時に進行する可能性があります。 ただし、競合が多い場合、CASの失敗と再試行の数によって、全体的なパフォーマンスが大幅に低下します。

一方、 ThreadLocalRandom は、各スレッドに Random の独自のインスタンスがあり、その結果、独自の制限された seedがあるため、この競合を完全に取り除きます。

次に、ランダムな int、long 、およびdouble値を生成するいくつかの方法を見てみましょう。

3. ThreadLocalRandomを使用したランダム値の生成

Oracleのドキュメントによると、 ThreadLocalRandom.current()メソッドを呼び出すだけで、現在のスレッドのThreadLocalRandomのインスタンスが返されます。 次に、クラスの使用可能なインスタンスメソッドを呼び出すことにより、ランダムな値を生成できます。

境界のないランダムなint値を生成してみましょう。

int unboundedRandomValue = ThreadLocalRandom.current().nextInt());

次に、ランダムに制限された int 値、つまり指定された下限と上限の間の値を生成する方法を見てみましょう。

0〜100の間のランダムなint値を生成する例を次に示します。

int boundedRandomValue = ThreadLocalRandom.current().nextInt(0, 100);

0は包括的下限であり、100は排他的上限であることに注意してください。

longおよびdoubleのランダム値は、 nextLong()および nextDouble()メソッドを次のように呼び出すことで生成できます。上記の例。

Java 8は、 nextGaussian()メソッドも追加して、ジェネレーターのシーケンスから平均0.0および標準偏差1.0の次の正規分布値を生成します。

Random クラスと同様に、 doubles()、ints()、および longs()メソッドを使用してランダム値のストリームを生成することもできます。

4. JMHを使用したThreadLocalRandomRandomの比較

2つのクラスを使用して、マルチスレッド環境でランダムな値を生成する方法を見てから、JMHを使用してそれらのパフォーマンスを比較してみましょう。

まず、すべてのスレッドが Randomの単一インスタンスを共有している例を作成しましょう。ここでは、Randomインスタンスを使用してランダム値を生成するタスクを ExecutorService:

ExecutorService executor = Executors.newWorkStealingPool();
List<Callable<Integer>> callables = new ArrayList<>();
Random random = new Random();
for (int i = 0; i < 1000; i++) {
    callables.add(() -> {
         return random.nextInt();
    });
}
executor.invokeAll(callables);

JMHベンチマークを使用して、上記のコードのパフォーマンスを確認しましょう。

# Run complete. Total time: 00:00:36
Benchmark                                            Mode Cnt Score    Error    Units
ThreadLocalRandomBenchMarker.randomValuesUsingRandom avgt 20  771.613 ± 222.220 us/op

同様に、プール内のスレッドごとにThreadLocalRandomの1つのインスタンスを使用するRandomインスタンスの代わりに、ThreadLocalRandomを使用してみましょう。

ExecutorService executor = Executors.newWorkStealingPool();
List<Callable<Integer>> callables = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
    callables.add(() -> {
        return ThreadLocalRandom.current().nextInt();
    });
}
executor.invokeAll(callables);

ThreadLocalRandom:を使用した結果は次のとおりです。

# Run complete. Total time: 00:00:36
Benchmark                                                       Mode Cnt Score    Error   Units
ThreadLocalRandomBenchMarker.randomValuesUsingThreadLocalRandom avgt 20  624.911 ± 113.268 us/op

最後に、RandomThreadLocalRandomの両方について上記のJMHの結果を比較すると、Randomを使用して1000個のランダム値を生成するのにかかる平均時間が772であることがはっきりとわかります。 ThreadLocalRandom を使用すると、約625マイクロ秒になります。

したがって、 ThreadLocalRandomは、高度に同時実行される環境でより効率的であると結論付けることができます。

JMH の詳細については、以前のの記事をご覧ください。

5. 実装の詳細

ThreadLocalRandomThreadLocalクラスとRandomクラスの組み合わせと考えるのは良いメンタルモデルです。 実際のところ、このメンタルモデルはJava8より前の実際の実装と一致していました。

ただし、Java 8の時点では、ThreadLocalRandomがシングルトンになると、この配置は完全に機能しなくなりました。 current()メソッドがJava8+でどのように表示されるかを次に示します。

static final ThreadLocalRandom instance = new ThreadLocalRandom();

public static ThreadLocalRandom current() {
    if (U.getInt(Thread.currentThread(), PROBE) == 0)
        localInit();

    return instance;
}

1つのグローバルランダムインスタンスを共有すると、競合の激しいパフォーマンスが最適化されないことは事実です。 ただし、スレッドごとに1つの専用インスタンスを使用することもやり過ぎです。

スレッドごとのランダムの専用インスタンスの代わりに、各スレッドは独自のシード値を維持するだけで済みます。 Java 8の時点で、スレッドクラス自体は、シード値を維持するように改良されています。

public class Thread implements Runnable {
    // omitted

    @jdk.internal.vm.annotation.Contended("tlr")
    long threadLocalRandomSeed;

    @jdk.internal.vm.annotation.Contended("tlr")
    int threadLocalRandomProbe;

    @jdk.internal.vm.annotation.Contended("tlr")
    int threadLocalRandomSecondarySeed;
}

The threadLocalRandomSeed 変数は、の現在のシード値を維持する責任があります ThreadLocalRandom。 また、二次シード、 threadLocalRandomSecondarySeed 、通常、のようなものによって内部的に使用されます ForkJoinPool。

この実装には、ThreadLocalRandomのパフォーマンスをさらに向上させるためのいくつかの最適化が組み込まれています。

  • @Contented アノテーションを使用して、偽共有を回避します。これにより、基本的に、競合する変数を独自のキャッシュラインで分離するのに十分なパディングが追加されます。
  • Reflection APIを使用する代わりに、sun.misc.Unsafeを使用してこれらの3つの変数を更新します
  • ThreadLocal実装に関連する余分なハッシュテーブルルックアップの回避

6. 結論

この記事では、java.util.Randomjava.util.concurrent.ThreadLocalRandomの違いについて説明しました。

また、マルチスレッド環境でのRandomに対するThreadLocalRandomの利点、パフォーマンス、およびクラスを使用してランダム値を生成する方法も確認しました。

ThreadLocalRandom はJDKへの単純な追加ですが、同時実行性の高いアプリケーションに顕著な影響を与える可能性があります。

そして、いつものように、これらすべての例の実装は、GitHubにあります。