ThreadLocalRandom クラス原理の分析

ThreadLocalRandom クラスは、JDK7 によって JUC パッケージに追加された新しい乱数ジェネレーターで、マルチスレッドにおける Random クラスの欠点を補います。

ランダム クラスとその制限事項

JDK7 より前も現在も、java.util.Random は広く使用されている乱数生成ツール クラスであり、java.lang.Math の乱数生成でも java.util.Random のインスタンスを使用します。

ここに画像の説明を挿入しますコード (1) は、デフォルトの乱数ジェネレーターを作成し、デフォルトのシードを使用します。

コード (2) は、0 ~ 5 (0 を含み、5 を除く) の 10 個の乱数を出力します。乱数の生成にはデフォルト シードが必要ですが、これは実際にはlong型の数値です。Random オブジェクトの作成時にコンストラクタを通じて指定できます。指定しない場合は、デフォルト コンストラクタ内でデフォルト値が生成されます。デフォルトのシードを使用して乱数を生成するにはどうすればよいでしょうか?

ここに画像の説明を挿入します
新しい乱数を生成するには、次の 2 つの手順が必要です。

  • まず、古いシードに基づいて新しいシードを生成します。
  • 次に、新しいシードに基づいて新しい乱数を計算します。

ステップ (4) は、seed=f(seed) として抽象化できます。ここで、f は、seed=f(seed)=a*seed+b などの固定関数です。

ステップ (5) は、g(seed,bound) として抽象化することもできます。ここで、g は、g(seed,bound)=(int)((bound*(long)seed)>>31) などの固定関数です。

シングルスレッドの状況では、nextInt が呼び出されるたびに、古いシードに基づいて新しいシードが計算され、乱数生成のランダム性が保証されます。

ただし、マルチスレッドでは、複数のスレッドが同じ古いシードを使用してステップ (4) を実行し、新しいシードを計算することがあります。これにより、ステップ (5) のアルゴリズムにより、複数のスレッドによって生成される新しいシードが同じになります。は修正されているため、複数のスレッドが同じランダム値を生成することになりますが、これは私たちが望んでいることではありません。

したがって、ステップ (4) では原子性を確保する必要があります、つまり、複数のスレッドが同じ古いシードに基づいて新しいシードを計算する場合、最初のスレッドの新しいシードが計算された後、2 番目のスレッドはその古いシードを破棄する必要があります。最初のスレッドのシードを使用して独自の新しいシードを計算するなど、これを保証することによってのみ、マルチスレッドで生成される乱数がランダムであることを保証できます。

Random 関数は、この効果を実現するためにアトミック変数を使用します。Random オブジェクトの作成時に初期化されたシードは、シード アトミック変数に保存されます。以下の next() のコードを参照してください。

ここに画像の説明を挿入します
コード (6) は、現在のアトミック変数シードの値を取得します。

コード (7) は、現在のシード値に基づいて新しいシードを計算します。

コード (8) は、新しいシードを使用して古いシードを更新する CAS 操作を使用します。マルチスレッドでは、複数のスレッドがコード (6) を同時に実行する可能性があるため、複数のスレッドによって取得される現在のシード値が同じになる可能性があります。 、ステップ (7) で計算された新しいシードも同じですが、ステップ (8) の CAS 操作により、1 つのスレッドのみが古いシードを新しいシードに更新できることが保証され、失敗したスレッドは再取得します。ループを介して更新されたシード。シードは古いシードを計算するための現在のシードとして使用され、これにより上記の問題が解決され、乱数のランダム性が保証されます。

コード (9) は、固定アルゴリズムを使用して、新しいシードに基づいて乱数を計算します。

各 Random インスタンスには、現在のシード値を記録するアトミック シード変数があります。新しい乱数を生成する場合は、現在のシードに基づいて新しいシードを計算し、アトミック変数に更新する必要があります。

マルチスレッドで単一の Random インスタンスを使用して乱数を生成する場合、複数のスレッドが同時に乱数を計算して新しいシードを計算すると、複数のスレッドが同じアトミック変数の更新操作で競合します。アトミック変数は CAS 操作です。同時に成功するスレッドは 1 つだけです。そのため、多数のスレッドがスピンして再試行することになり、同時実行パフォーマンスが低下します。そのため、ThreadLocalRandom が登場しました。

スレッドローカルランダム

マルチスレッドの同時実行性が高い状況における Random の欠点を補うために、ThreadLocalRandom クラスが JUC パッケージの下に追加されました。

ここに画像の説明を挿入します
コード (10) は、ThreadLocalRandom.current() を呼び出して、現在のスレッドの乱数ジェネレーターを取得します。

ThreadLocalRandom の実装原理を分析してみましょう。

ThreadLocal を使用すると、各スレッドが変数のコピーをコピーできるため、各スレッドが変数を操作するときに、実際には独自のローカル メモリ内でコピーが操作されるため、共有変数を同期する必要がなくなります。

実際、ThreadLocalRandom の実装はこの原則に従っていますが、Random の欠点は、複数のスレッドが同じアトミック シード変数を使用するため、アトミック変数の更新の競合が発生することです。

ここに画像の説明を挿入します

各スレッドがシード変数を維持する場合、各スレッドは乱数を生成するときに、自身の古いシードに基づいて新しいシードを計算し、古いシードを新しいシードで更新し、新しいシードに基づいて乱数を計算します。競合の問題が解消され、同時実行パフォーマンスが大幅に向上します。

ここに画像の説明を挿入します

ソースコード分析

まず、ThreadLocalRandom のクラス図の構造を見てください。
ここに画像の説明を挿入します
ThreadLocalRandom クラスが Random クラスを継承し、nextInt メソッドをオーバーライドしていることがわかりますが、Random クラスから継承したアトミック シード変数は ThreadLocalRandom クラスでは使用されません。

ThreadLocalRandom には特定のシードは保存されません。特定のシードは、特定の呼び出しスレッドの threadLocalRandomSeed 変数に保存されます。

ThreadLocalRandom は ThreadLocal クラスに似たツール クラスです。

スレッドが ThreadLocalRandom の現在のメソッドを呼び出すと、ThreadLocalRandom は呼び出しスレッドの threadLocalRandomSeed 変数 (初期化シード) を初期化します。

ThreadLocalRandom の nextInt メソッドが呼び出されると、実際には現在のスレッドの threadLocalRandomSeed 変数を現在のシードとして取得して新しいシードを計算し、新しいシードを現在のスレッドの threadLocalRandomSeed 変数に更新して、次の値に基づいて乱数を計算します。新しいシードと特定のアルゴリズムの使用。

ここで注意する必要があるのは、threadLocalRandomSeed 変数は Thread クラスの通常の long 変数であり、アトミック変数ではないということです。

実は理由はとても簡単で、この変数はスレッドレベルにあるのでアトミック変数を使う必要がまったくないのですが、それでも理解できない場合は、ThreadLocal の原理を考えてみるとよいでしょう。

このうち、シーダーとプローブジェネレーターは 2 つのアトミック変数で、呼び出しスレッドのシード変数とプローブ変数を初期化するときに使用され、各スレッドで 1 回だけ使用されます。

さらに、変数インスタンスは静的な ThreadLocalRandom のインスタンスです。

複数のスレッドが ThreadLocalRandom の現在のメソッドを通じて ThreadLocalRandom のインスタンスを取得する場合、実際には同じインスタンスを取得します。

ただし、特定のシードはスレッドに格納されるため、ThreadLocalRandom のインスタンスにはスレッドとは関係のない一般的なアルゴリズムのみが含まれるため、スレッドセーフです。

安全でないメカニズム

ここに画像の説明を挿入します

ThreadLocalRandom current()方法

このメソッドは ThreadLocalRandom インスタンスを取得し、呼び出し側スレッドの threadLocalRandomSeed 変数と threadLocalRandomProbe 変数を初期化します。

ここに画像の説明を挿入します
コード (12) では、現在のスレッドの threadLocalRandomProbe の変数値が 0 (デフォルトでは、スレッドの変数値は 0) の場合、現在のスレッドが ThreadLocalRandom の現在のメソッドを初めて呼び出したことを意味します。現在のスレッドの初期化シード変数を計算するには、locallnit メソッドを呼び出す必要があります。

ここで初期化を遅らせるため、乱数関数が必要ない場合には Thread クラスのシード変数を初期化しない最適化を行っています。

コード (13) は、まず、probeGenerator に基づいて現在のスレッドの threadLocalRandomProbe の初期化値を計算し、次にシーダーに基づいて現在のスレッドの初期化シードを計算し、これら 2 つの変数を現在のスレッドに設定します。

コード (14) は ThreadLocalRandom のインスタンスを返します。このメソッドは静的メソッドであり、複数のスレッドが同じ ThreadLocalRandom インスタンスを返すことに注意してください。

int nextlnt(int bound)方法

現在のスレッドの次の乱数を計算します。

ここに画像の説明を挿入します上記のコードの論理ステップは Random と似ていますが、nextSeed() メソッドに注目してみましょう。

ここに画像の説明を挿入します

まず r=UNSAFE.getLong(t,SEED) を使用して現在のスレッドの threadLocalRandomSeed 変数の値を取得し、次にシードに基づいて GAMMA 値を新しいシードとして蓄積し、次に UNSAFE の putLong メソッドを使用して新しいシードを配置します現在のスレッドの threadLocalRandomSeed 変数に代入します。

おすすめ

転載: blog.csdn.net/zhuyufan1986/article/details/135452213