11 擬似乱数ジェネレータ

Java SEに含まれている乱数ジェネレータは、より正確には擬似乱数ジェネレータ(PRNG)と呼ばれます。これらは決定性アルゴリズムに基づいて一連の数字を作成します。

最も重要なインタフェースおよびクラスは、PRNGアルゴリズムを指定して様々なプリミティブ型の乱数を生成できるRandomGeneratorと、アルゴリズム名以外の特性に基づいてPRNGを作成できるRandomGeneratorFactoryです。

Java SEに実装されているPRNGの詳細は、java.util.randomパッケージに関する項を参照してください。

PRNGの特性

PRNGは、「ランダムな」物理ソースではなく、アルゴリズムに基づいて値のシーケンスを生成するため、このシーケンスは最終的に再起動されます。PRNGが再起動する前に生成する値の数は、周期と呼ばれます。

PRNGの状態サイクルは、PRNGが生成可能なすべての値のシーケンスで構成されます。PRNGの状態とは、その状態サイクルの中で最後に生成された値の位置です。

一般に、値を生成する際に、PRNGは以前に生成された値を基にしています。ただし、一部のPRNGは、中間値を計算せずに、シーケンスのさらに先の多くの値を生成できるものもあります。これらは、値のシーケンスのはるか先にジャンプすることができるため、ジャンプ可能なPRNGと呼ばれます。通常は、264という一定の距離だけジャンプします。リープ可能なPRNGはさらにジャンプすることができ、通常の値は2128です。任意でジャンプ可能なPRNGは、生成された値のシーケンス内の任意の値にジャンプできます。

java.util.Randomクラスと他のPRNGとの比較

java.util.random.RandomGeneratorFactoryクラスを使用すると、様々なPRNGを作成できますが、その多くは、jdk.randomパッケージに含まれています。jdk.randomに含まれるPRNGとjava.util.Randomクラスの最も大きな違いは、Randomの周期が非常に短く、248の値しかないことです。

RandomGeneratorインタフェースを使用した擬似乱数の生成

次の例は、PRNGを作成し、それを使用して乱数を生成するための基本的な方法を示しています:

        RandomGenerator random1 = RandomGenerator.of("Random");
        long value1 = random1.nextLong();
        System.out.println(value1);

メソッドRandomGenerator.of(String)を使用します。このメソッドの引数は、PRNGのアルゴリズム名です。Java SEには多数のPRNGクラスが含まれています。ただし、Randomとは異なり、そのほとんどはjdk.randomパッケージに含まれています。

RandomGeneratorインタフェースには、nextLong()nextInt()nextDouble()nextBoolean()など、様々なプリミティブ・データ型の乱数を生成する多くのメソッドが含まれています。

次の例は、RandomGeneratorFactoryクラスを使用してPRNGを作成する方法を示しています:

        RandomGeneratorFactory<RandomGenerator> factory2 =
            RandomGeneratorFactory.of("SecureRandom");
        RandomGenerator random2 = factory2.create();
        long value2 = random2.nextLong();
        System.out.println(value2); 

Java SEによって実装されたPRNGのリストを取得するには、RandomGeneratorFactory.all()メソッドを呼び出します:

        RandomGeneratorFactory.all()
            .map(f -> f.name())
            .sorted()
            .forEach(n -> System.out.println(n));

このメソッドは、使用可能なすべてのRandomGeneratorFactoryインスタンスのストリームを返します。

RandomGeneratorFactoryクラスを使用して、アルゴリズム名以外の特性に基づいてPRNGを作成できます。次の例では、最長周期のPRNGを検索し、この特性に基づいてRandomGeneratorFactoryを作成します:

        RandomGeneratorFactory<RandomGenerator> greatest =
            RandomGeneratorFactory
                .all()
                .sorted((f, g) -> g.period().compareTo(f.period()))
                .findFirst()
                .orElse(RandomGeneratorFactory.of("Random"));
        System.out.println(greatest.name());
        System.out.println(greatest.group());
        System.out.println(greatest.create().nextLong());

マルチスレッド・アプリケーションでの擬似乱数の生成

アプリケーション内の複数のスレッドがPRNGを使用して値のシーケンスを生成している場合、特に同じPRNGアルゴリズムを使用している場合は、これらのシーケンスに互いに一致する値が含まれている可能性がないことを確認します。(同じPRNGアルゴリズムを使用して、すべてのアプリケーションの擬似乱数シーケンスが同じ統計プロパティを持つようにします。)このためには、分割可能、ジャンプ可能およびリープ可能なPRNGが最適です。同じ統計プロパティを持ち、統計的に独立しているジェネレータのストリームを作成できます。

PRNGをアプリケーションに組み込むために使用できる手法は2つあります。アプリケーションが新しいスレッドをフォークする必要がある場合、新しいジェネレータを動的に作成できます。または、最初のRandomGeneratorに基づいてRandomGeneratorオブジェクトのストリームを作成し、各RandomGeneratorオブジェクトをストリームから独自のスレッドにマップすることもできます。

新しいジェネレータの動的な作成

RandomGenerator.SplittableGeneratorインタフェースを実装するPRNGを使用している場合、アプリケーションで実行されているスレッドが新しいスレッドをフォークする必要があるときに、split()メソッドを呼び出します。元のジェネレータと同じプロパティを持つ新しいジェネレータが作成されます。これは、元のジェネレータの周期を2つにパーティション化することによって行われます。各パーティションは、元のジェネレータまたは新しいジェネレータのいずれかが排他的に使用します。

次の例では、RandomGenerator.SplittableGeneratorインタフェースを実装するL128X1024MixRandom PRNGを使用します。IntStream processesストリームは、異なるスレッドで実行することを目的としたタスクを表します。

        int NUM_PROCESSES = 100;
        
        RandomGeneratorFactory<SplittableGenerator> factory =
            RandomGeneratorFactory.of("L128X1024MixRandom");
        SplittableGenerator random = factory.create();
       
        IntStream processes = IntStream.rangeClosed(1, NUM_PROCESSES);
        
        processes.parallel().forEach(p -> {
            RandomGenerator r = random.split();
            System.out.println(p + ": " + r.nextLong());
        });

一般に、分割可能なPRNGには、分割の結果として生じる新しいオブジェクトが異なる状態サイクルを使用できるように、長い周期があります。ただし、2つのインスタンスが誤って同じ状態サイクルを使用する場合でも、その共有状態サイクルの異なるリージョンを通過する可能性が高くなります。

ジェネレータのストリームの作成

最初のジェネレータでインタフェースRandomGenerator.StreamableGeneratorが実装されている場合は、メソッドrngs()jumps() (ジャンプ可能なジェネレータの場合)、またはleaps() (リープ可能なジェネレータの場合)を呼び出して、ジェネレータのストリームを作成します。ストリームでmap()メソッドを呼び出して、各ジェネレータを独自のスレッドに割り当てます。

jumps()メソッドを呼び出すと、ジェネレータはその状態サイクル内で一定の大きな距離をジャンプして状態を変更し、ジェネレータの新しい状態に基づいて新しいジェネレータを作成します。ジェネレータは、ジャンプとジェネレータの作成を繰り返し、ジェネレータのストリームを作成します。leaps()メソッドは似ていますが、ジャンプのサイズは非常に大きくなります。

次の例では、ジャンプ可能なジェネレータを作成し、jumps()メソッドを呼び出して、この最初のジェネレータに基づいてジェネレータのストリームを作成します。ストリームの最初のいくつかのジェネレータ(NUM_TASKSで定義)がTaskインスタンスにラップされ、各Taskが独自のスレッドで実行されます。

       int NUM_TASKS = 10;
       
       RandomGeneratorFactory<JumpableGenerator> factory =
            RandomGeneratorFactory.of("Xoshiro256PlusPlus");
       JumpableGenerator random = factory.create();
       
       class Task implements Runnable {
            private int p;
            private RandomGenerator r;
            public Task(RandomGenerator prng) {
                r = prng;
            }
            public void run() {
                System.out.println(r.nextLong());
            }
        }
        
        List<Thread> taskList = random
            .jumps()
            .limit(NUM_TASKS)
            .map(prng -> new Thread(new Task(prng)))
            .collect(Collectors.toList());
        taskList.stream().forEach(t -> t.start());

PRNGアルゴリズムの選択

暗号で保護されたアルゴリズムを必要としないアプリケーション(物理的なシミュレーション、機械学習、ゲームなど)のために、java.util.randomパッケージでは、速度、スペース、周期、偶発的な相関および等分布を含む1つ以上のPRNGプロパティに焦点を当てたインタフェースRandomGeneratorの複数の実装を提供しています。

ノート:

PRNGアルゴリズムの進化に伴い、Java SEは新しいPNGアルゴリズムを追加し、古いアルゴリズムを非推奨にする場合があります。非推奨のアルゴリズムは使用しないことをお薦めします。将来のJava SEリリースから削除される可能性があります。アルゴリズムが非推奨になっているかどうかをチェックするには、RandomGenerator.isDeprecated()またはRandomGeneratorFactory.isDeprecated()メソッドを呼び出します。

暗号で保護

暗号で保護された乱数ジェネレータ・アルゴリズムを必要とするアプリケーションの場合、java.securityパッケージのSecureRandomクラスを使用します。

詳細は、『Java Platform, Standard Editionセキュリティ開発者ガイド』「SecureRandomクラス」を参照してください。

汎用

特別な要件のないアプリケーションでは、L64X128MixRandomは、速度、スペースおよび周期のバランスをとります。適切に使用すると、シングル・スレッドとマルチ・スレッドの両方のアプリケーションに適しています(スレッドごとに個別のインスタンス)。

シングル・スレッド、高パフォーマンス

シングル・スレッド・アプリケーションの場合、Xoroshiro128PlusPlusは、小規模および高速で、十分な長さの周期を持っています。

32ビット・アプリケーション

32ビット環境で動作し、1つまたは少数のスレッドのみを使用するアプリケーションの場合、L32X64StarStarRandomまたはL32X64MixRandomを選択することをお薦めします。

静的スレッドを持つマルチスレッド・アプリケーション

計算の開始時に1つのバッチで割り当てられる多数のスレッドを使用するアプリケーションの場合、Xoroshiro128PlusPlusやXoshiro256PlusPlusなどのジャンプ可能なジェネレータ、またはL64X128MixRandomやL64X256MixRandomなどの分割可能なジェネレータを検討してください。アプリケーションが、32ビット未満の浮動小数点精度が必要で、正確な等分布が不要な均一分布の浮動小数点値のみを使用する場合、クラシックでよく研究されたアルゴリズムであるMRG32k3aが適切である可能性があります。

動的スレッドを持つマルチスレッド・アプリケーション

スプリッタなどの使用によって多数のスレッドを動的に作成するアプリケーションの場合、L64X128MixRandomやL64X256MixRandomなどの分割可能なジェネレータをお薦めします。

動的に作成されるジェネレータの数が非常に多い(数百万以上)場合、L128X128MixRandomやL128X256MixRandomなどのジェネレータを使用すると、2つのインスタンスが同じ状態サイクルを使用する可能性が大幅に低くなります。

連続して生成された値のタプル

連続して生成された値のタプルを使用するアプリケーションの場合、kが生成されるタプルの長さ以上になるように、kが等分布されたジェネレータを検討してください。たとえば、ジェネレータ L64X256MixRandomは4等分布であることが示されています。つまり、4つの値を含むタプルのシーケンスを持つことができ、これらのタプルは均一に分布されます(4つのタプルがシーケンスに表示される可能性は同等です)。また、L64X1024MixRandomが16等分布であることも示されています。

大きな順列

大きな順列を生成するアプリケーションの場合、可能な順列の総数よりも周期がはるかに大きいジェネレータを検討してください。そうしないと、意図した順列の一部を生成できなくなります。たとえば、1組52枚のカードをシャッフルする目的の場合、可能な順列の数は52! (52階乗)です。これは約2225.58であるため、L64X256MixRandom、L64X1024MixRandom、L128X256MixRandom、L128X1024MixRandomなど、周期が約2256以上のジェネレータの使用が最適です。