Javaにおける擬似乱数

Javaにおける擬似乱数の概要。「擬似乱数とは」「シードの設定」「標準ライブラリでの擬似乱数クラス」など。

執筆時バージョン
Java

Java SE 8

1. 擬似乱数の基礎

そもそも擬似乱数とは。

  • プログラム(ソフトウェア)の乱数は擬似乱数(pseudorandom numbers)

  • (通常の)擬似乱数と暗号論的擬似乱数

    • 擬似乱数のなかでも暗号用途に利用に耐えられるのが暗号論的擬似乱数

    • 擬似乱数は再現可能であるが、暗号用途では簡単に再現されては当然困る。
      なので暗号用の乱数は、リバースエンジニアリングに関する耐性や擬似乱数生成の内部状態が攻撃者に漏洩してもそれ以前の出力は推測できないことを意識した作りにする必要がある。そのような対策をしている擬似乱数が暗号論的擬似乱数と呼ばれる。

    • 通常の擬似乱数はここまでは満たすようにはしていない。なので暗号論的擬似乱数であるか否かが大きな分類となる。

1.1. 擬似乱数生成アルゴリズム

擬似乱数列を作るアルゴリズムはいくつも提案されていて、生成が高速・実装が容易・適度にばらつく高品質な乱数生成アルゴリズムは今も研究され続けている。これでもう決まりというアルゴリズムはまだない。アルゴリズムによっては欠点がある場合があるので、乱数が重要な場合はどんなアルゴリズムで実装されているのかをしっかり検討する必要がある。

比較的古いものでいうと線形合同法が有名。ただ線形合同法はばらつきに問題があるという指摘が多いため最近は利用を敬遠されている。なので比較的新しいも注目されていて、メルセンヌツイスターなどが人気のようで評価も高い。

線形合同法
  • 生成が高速で、実装が容易

  • ばらつきに問題ありという指摘が多数されている

  • 比較的古い

メルセンヌツイスター
  • 既存の疑似乱数列生成手法にある多くの欠点が無い

  • 高品質で高速に生成できる

  • 比較的新しい

2. 擬似乱数の利用

擬似乱数を利用する場合にまず気をつけたいのはシードの設定。

2.1. シードの設定

擬似乱数では前回の値を元に次の乱数値を算出することが多いので最初はシードと呼ばれる初期状態みたいなものを設定する。これをシードという。同じシードを設定すれば乱数を再現することになる。(ただし、そもそもシードを要求しない擬似乱数も存在する)

再現できていいのか悪いのかについては乱数を利用する側の仕様による(乱数を使うからといって必ず再現できてはいけないと言うこともない)。なので、そもそも再現させるのか否かが決まっていないと、シードの設定が妥当なのかがわからないことになる。

そのうえで、再現してはいけないのに再現できるようなシードの設定方法だったりすればバグと言われてしまう。

2.1.1. シードを毎回変える?システム時間を利用する?

シードの設定についてこれらをやってしまいがちだが、少し考える必要がある。

偏りがない乱数を生成する場合に、シードを毎回変えるという手法をたまにみかけます。
こちらは間違っています。
システム時間などをシードとして与える事がよくありますが、アルゴリズムによっては与えられたシードの数値をすべて使うとは限りません。

— @isonami
サイコロを振るのは簡単である、しかし、ゲームでサイコロを実装するにはサイコロを知らなければならない - Qiita

乱数を再現しないようにシードを設定しようとして、シードを毎回変えたりシステム時間を利用したりするということをやりがちだが、このようなこともあるので不十分なこともある。

またシードを毎回変えることはその行為自体が再現してしまってバグになるようなことがあるのでやめたほうがよさそう。

ちなみにJava SE 7から登場するThreadLocalRandomの初期シードの設定を参考にすると、システム時間のミリ秒とナノ秒をそれぞれ加工したものを排他的論理和をとって初期シードにしている。またVMのプロパティの設定を変更すればjava.security.SecureRandomから初期シードを設定することもできるようになっている。

3. Javaにおける擬似乱数のクラス・ライブラリ

java.util.Randomが昔から存在するがJava SE 8 以降の標準ライブラリではThreadLocalRandomやSplittableRandomも追加され改善されている。ただ標準ライブラリでさまざまなアルゴリズムに対して潤沢に用意されている訳でもないので、それらが重要で必要な場合はサードパーティーライブラリを探す。

3.1. 標準ライブラリ

標準ライブラリの擬似乱数クラスを登場したバージョン順に並べると次のとおり。

Table 1. 擬似乱数クラス一覧
擬似乱数クラス バージョン 暗号論的擬似乱数

Random

1.0

SecureRandom

1.1

ThreadLocalRandom

7

SplittableRandom

8

「ThreadLocalRandom」と「SplittableRandom」については利用場面が異なる。並列のタスクではSplittableRandomの方が良い。Effective Java 第3版の説明がわかりやすいので引用させてもらう。

乱数のストリームを並列化するなら、ThreadLocalRandom(あるいは実質的に使われなくなっているRandom)ではなくSplittableRandomで始めてください。SplittableRandomクラスはまさにこのために設計されており、線形のパフォーマンス向上が見込まれます。ThreadLocalRandomインスタンスは単一スレッドから使われるように設計されており、並列ストリームのソースとして機能しますが、SplittableRandomほど速くありません。

— ジョシュア・ブロック
Effective Java 第3版 柴田 芳樹訳 p.225

3.1.1. 各クラスの詳細

java.util.Random
  • 線形合同法の擬似乱数生成クラス

  • 今となっては後発のThreadLocalRandom or SplittableRandomを使うべき。Javadocにも次のように記載されている

    java.util.Random のインスタンスはスレッドセーフです。ただし、複数のスレッドで同じ java.util.Random インスタンスを並行して使用すると、競合が発生してパフォーマンスが低下する可能性があります。マルチスレッド設計では、代わりに ThreadLocalRandom を使用することを検討してください。

    — Javadoc
    https://docs.oracle.com/javase/jp/8/docs/api/index.html?java/util/concurrent/Random.html
  • https://docs.oracle.com/javase/jp/8/docs/api/index.html?java/util/Random.html

java.security.SecureRandom
java.util.concurrent.ThreadLocalRandom
  • 前述のjava.util.Randomの問題点を改善している

    • 競合が発生しないようにsynchronizedを除去していて、そのためThreadLocalに一部値を保持している。(そのためThreadLocalRandomという名前だと思う…​)

  • シングルトンであるがスレッド毎に設定が行われる方式

    • なのでスレッド間で共有されない(独立したように振る舞う)

    • ThreadLocalRandom.current()でインスタンスを取得する

  • シードの設定はできない。

    • setSeed()するとUnsupportedOperationExceptionがスローされる

  • java.util.Randomを継承しているが、アルゴリズムも異なる。ThreadLocalRandomクラスのコメントには次のように書かれている。

    Even though this class subclasses java.util.Random, it uses the
    same basic algorithm as java.util.SplittableRandom. (See its
    internal documentation for explanations, which are not repeated
    here.) Because ThreadLocalRandoms are not splittable
    though, we use only a single 64bit gamma.

    — ThreadLocalRandomクラスのコメント
    • 基本的にはSplittableRandomと同じということなのでSplittableRandomを参照

  • https://docs.oracle.com/javase/jp/8/docs/api/index.html?java/util/concurrent/ThreadLocalRandom.html

java.util.SplittableRandom
  • 前述のように並列のタスクなどでの利用であればこちらを利用する

    • その想定のためそもそもスレッドセーフになっていない

  • java.util.Randomは継承していない

  • アルゴリズムはDotMixからアイデアを得たものらしい。SplittableRandomクラスのコメントには次のように書かれている。

    This algorithm was inspired by the "DotMix" algorithm by
    Leiserson, Schardl, and Sukha "Deterministic Parallel
    Random-Number Generation for Dynamic-Multithreading Platforms",
    PPoPP 2012, as well as those in "Parallel random numbers: as
    easy as 1, 2, 3" by Salmon, Morae, Dror, and Shaw, SC 2011. It
    differs mainly in simplifying and cheapening operations.

    — SplittableRandomクラスのコメントより
  • https://docs.oracle.com/javase/jp/8/docs/api/index.html?java/util/SplittableRandom.html

3.2. サードパーティーのライブラリ

乱数が重要な場合は標準ライブラリでは物足りない。乱数のライブラリや実装例はいろいろと存在するのでサードパーティーのライブラリなどを探してみると良い。とりあえずCommons Mathあたりが乱数関連クラス(メルセンヌツイスターなど)が豊富に用意されているので、検討に値する。

Commons Math

Appendix A: 改訂履歴

  • v1.02019-01-24: 初稿