Javaで乱数を扱う必要があり、少し調べた内容を整理したいと思います。用途によってはRandom、ThreadLocalRandomやSecureRandomがあり、今回は、これら3つのクラスの特徴、使い分け、そして実践で役立つ注意点について詳しく解説します。
3つの乱数生成クラスの基本概要
Random:基本的な擬似乱数生成器
java.util.Randomは最もシンプルで広く使われている乱数生成クラスです。
Random random = new Random();
int value = random.nextInt(100); // 0-99の範囲の整数
double doubleValue = random.nextDouble(); // 0.0-1.0の実数
特徴
- 線形合同法を使用した擬似乱数生成
- スレッドセーフだが、マルチスレッドでは同期化のオーバーヘッドが発生
- 暗号学的に安全ではない(予測可能)
- シンプルで使いやすい
ThreadLocalRandom:マルチスレッド最適化版
Java 7で導入されたjava.util.concurrent.ThreadLocalRandomは、マルチスレッド環境での性能を大幅に改善したクラスです。
ThreadLocalRandom random = ThreadLocalRandom.current();
int value = random.nextInt(0, 100); // 0-99の範囲
long longValue = random.nextLong(1, 1000); // 1-999の範囲
特徴
- 各スレッドが独自の乱数生成器インスタンスを保持
- 同期化が不要で、マルチスレッド環境で高性能
- Randomと同じアルゴリズムだが、スレッドローカルストレージを活用
- 暗号学的に安全ではない
SecureRandom:暗号学的に安全な乱数生成器
java.security.SecureRandomは、セキュリティが重要な用途向けの乱数生成クラスです。
SecureRandom secureRandom = new SecureRandom();
byte[] randomBytes = new byte[16];
secureRandom.nextBytes(randomBytes);
int secureInt = secureRandom.nextInt(100);
特徴
- 暗号学的に安全(CSPRNG: Cryptographically Secure Pseudo-Random Number Generator)
- 予測が困難で、セキュリティ用途に適している
- OSの乱数源やハードウェア乱数生成器を使用
- 処理速度は他の2つより遅い
使い分けのガイドライン
用途別推奨クラス
ThreadLocalRandom を選ぶべき場面
- マルチスレッドアプリケーションでの一般的な乱数生成
- シミュレーション、ゲーム、ランダムサンプリング
- 高いパフォーマンスが求められる並行処理
Random を選ぶべき場面
- シングルスレッドアプリケーション
- 簡単なプロトタイプや学習目的
- レガシーコードとの互換性が必要
SecureRandom を選ぶべき場面
- パスワード生成
- 暗号化キーの生成
- セッションIDやトークンの生成
- その他セキュリティが重要な用途
重要な注意点とベストプラクティス
Random クラスの注意点
シード値による再現性
同じシード値を使用すると、全く同じ乱数列が生成されます。これはテスト時には有用ですが、本番環境では注意が必要です。
// 同じシードだと同じ乱数列が生成される
Random r1 = new Random(12345);
Random r2 = new Random(12345);
System.out.println(r1.nextInt()); // 例:-1155869325
System.out.println(r2.nextInt()); // 同じ値:-1155869325
マルチスレッドでの性能問題
複数のスレッドが同じRandomインスタンスを共有すると、内部の同期化処理により性能が大幅に低下します。
ThreadLocalRandom クラスの注意点
正しいインスタンス取得方法
コンストラクタは使用せず、必ずcurrent()メソッドを使用してください。
// ❌ 間違い - コンストラクタは使用禁止
ThreadLocalRandom wrong = new ThreadLocalRandom();
// ✅ 正しい - current()メソッドを使用
ThreadLocalRandom correct = ThreadLocalRandom.current();
スレッド間での共有禁止
ThreadLocalRandomのインスタンスを他のスレッドで使用すると、予期しない動作を引き起こす可能性があります。
// ❌ 危険 - 別スレッドでの使用は避ける
ThreadLocalRandom tlr = ThreadLocalRandom.current();
CompletableFuture.runAsync(() -> {
tlr.nextInt(); // このような使い方は避ける
});
// ✅ 正しい - 各スレッドでcurrent()を呼び出す
CompletableFuture.runAsync(() -> {
ThreadLocalRandom.current().nextInt(); // これが正しい
});
SecureRandom クラスの注意点
初期化の遅延
SecureRandomは初回呼び出し時に大幅な遅延が発生する可能性があります。
SecureRandom sr = new SecureRandom();
// 初回呼び出し時に数秒かかることもある
sr.nextBytes(new byte[16]);
エントロピー不足の問題
Linux環境では/dev/randomを使用した場合、エントロピーが不足して処理がブロックされることがあります。必要に応じて/dev/urandomの使用を検討してください。
アルゴリズムの指定
try {
// 特定のアルゴリズムを指定可能
SecureRandom sr = SecureRandom.getInstance("SHA1PRNG");
} catch (NoSuchAlgorithmException e) {
// 環境によっては利用できないアルゴリズムもある
e.printStackTrace();
}
共通の注意点
範囲指定時のバイアス
剰余演算を使った範囲指定はバイアスを生む可能性があります。
// ❌ バイアスが発生する可能性
int biased = Math.abs(random.nextInt()) % 10;
// ✅ 適切な範囲指定
int unbiased = random.nextInt(10);
long値の範囲指定
Java 17以前では、RandomとSecureRandomのnextLong()メソッドには範囲指定版がありません。ThreadLocalRandomは対応済みです。
// ThreadLocalRandomは範囲指定に対応
ThreadLocalRandom tlr = ThreadLocalRandom.current();
long rangedLong = tlr.nextLong(100, 1000); // 100-999の範囲
// Random/SecureRandomでは自分で実装が必要(Java 17以前)
Random r = new Random();
long min = 100, max = 1000;
long rangedLong2 = min + (long)(r.nextDouble() * (max - min));
まとめ
乱数生成クラスの選択は、アプリケーションの要件によって決まります。
- 性能重視のマルチスレッドアプリケーション: ThreadLocalRandom
- シンプルな用途やシングルスレッド: Random
- セキュリティが重要: SecureRandom
それぞれの特徴と注意点を理解し、適切に使い分けることで、より安全で効率的なJavaアプリケーションを開発できるでしょう。特にマルチスレッド環境では、ThreadLocalRandomの使用を強く推奨します。セキュリティが関わる場面では、性能を犠牲にしてでもSecureRandomを選択することが重要です。
