如何用 Java 高效的生成隨機數?Random 的原理是什么?
在 JDK的java.util包里提供了一個用于生成隨機數的Random類,它是如何生成隨機數的?為什么它生成的隨機數是均勻的?今天我們一起來聊聊其背后的原理。
本文基于Java語言,jdk 11。
1. java.util.Random
Random是 java.util 包提供的一個用于生成隨機數的類,首先,我們看看官方對它的描述:
通過源碼,我們總結出幾個核心點:
- Random類的實例是用來生成一系列的偽隨機數;
- Random類使用一個 48位的種子(seed),通過線性同余算法進行修改;
- Random類的特定算法被指定,所以,兩個Random類的實例使用相同的種子創建,并且對于每個實例都調用相同順序的方法,它們將生成并返回相同的數字序列
- Random類是線程安全的,但是,跨線程同時使用同一個java.util.Random實例可能會遇到競爭和相應的性能問題;
- 在多線程設計中,考慮使用java.util.concurrent.ThreadLocalRandom;
- Random類的實例不是密碼安全的,對于安全敏感的應用程序,考慮使用java.security.SecureRandom;
2. 什么是偽隨機數?
偽隨機數指的是一種看起來像隨機數的序列,但實際上是由確定性算法生成的。這種算法稱為偽隨機數生成器(PRNG,Pseudo-Random Number Generator)。
PRNG使用一個稱為”種子”的初始值,然后通過一系列的數學運算來生成一個序列,這個序列看起來具有隨機性的特征,比如均勻分布、無序性等。
3. 什么是種子(seed)?
在隨機數生成器中,種子(seed)其實就是一個起始值,它用于初始化隨機數生成器的狀態。隨機數生成器使用這個種子來確定生成隨機數的序列。種子決定了隨機數生成器的初始狀態,因此給定相同的種子,將會生成相同的隨機數序列。
4. 線性同余算法
線性同余算法(LCG,Linear Congruential Generator)是最基本的偽隨機數生成算法之一,該算法通常使用如下方程表示:
????+1 = (a * ???? + c) mod m
其中:
- ???? 是當前的隨機數
- ????+1 是下一個隨機數
- a、c 和 m 是事先選定的常數
- a、c 和 m 是正整數
為了更好地理解這個方程,我們通過一個具體的例子來進行說明:
假設:a = 4, c = 1, m = 7,X? = 3,即種子 seed = 3
則:
X? = (4 * X? + 1) mod 5 = (4 * 3 + 1) mod 7 = 6
X? = (4 * X? + 1) mod 5 = (4 * 6 + 1) mod 7 = 4
X? = (4 * X? + 1) mod 5 = (4 * 4 + 1) mod 7 = 3
X? = (4 * X? + 1) mod 5 = (4 * 3 + 1) mod 7 = 6 (6,4,3)循環開始
X? = (4 * X? + 1) mod 5 = (4 * 6 + 1) mod 7 = 4
X? = (4 * X? + 1) mod 5 = (4 * 4 + 1) mod 7 = 3
...
說明:mod是取余操作,等同于 %
通過上面的示例可以看出:如果我們設定一個種子seed = 3,后面每一次獲取隨機數都可以通過該方程計算出來,而且按照(6,4,3)這個周期進行循環,整個過程獲取的數字看起來是隨機的,實際上又是通過固定的方法計算而來,因此叫做偽隨機數。
對于線性同余算法,需要重點考慮以下 5個因素:
- 種子(Seed): 線性同余算法的運行依賴于一個種子,改變該種子會產生不同的隨機數序列,但給定相同的種子和參數,將會生成相同的序列。
- 數選擇參: a、c 和 m 的選擇是至關重要的,不同的參數會導致不同質量的隨機數序列,包括周期長度、統計特性等。
- 周期性: 線性同余算法生成隨機數序列是周期性的,通過上面的例子也可以看出。
- 統計特性: 線性同余算法的生成的隨機數序列可能不滿足一些統計特性,如均勻分布、獨立性等。
- 效率: 線性同余算法是一種非常高效的隨機數生成算法,因為它只涉及簡單的數學運算。這使得它在許多情況下都是一個合適的選擇,尤其是對于需要大量隨機數的應用。
好了,理解了線性同余算法的實現原理,接下來我們來分析 Random是如何計算隨機數。
5. Random如何生成隨機數?
Random類包含兩個構造方法,如下:
// 無參構造器
public Random() {
this(seedUniquifier() ^ System.nanoTime());
}
// 接收一個 seed參數的構造器
public Random(long seed) {
if (getClass() == Random.class)
this.seed = new AtomicLong(initialScramble(seed));
else {
// subclass might have overriden setSeed
this.seed = new AtomicLong();
setSeed(seed);
}
}
// initialScramble方法是用于對種子進行初始的混淆處理,以增加生成的隨機數的隨機性
private static long initialScramble(long seed) {
return (seed ^ multiplier) & mask;
}
...
當使用Random的無參構造器時,Random內部會生成一個seed,生成方式如下:
seed = current * 1181783497276652981L ^ System.nanoTime()
如果 current沒有設置,默認的初始值是:1181783497276652981L。然后,用新生成的seed再調用帶參構造方法,構造器內部有initialScramble(long seed)方法,用于對種子進行初始的混淆處理,以增加生成的隨機數的隨機性,保證了其均勻性。
為了更好的說明java.util.Random是如何生成隨機數,這里以其nextDouble()為例進行講解,其源碼如下:
public double nextDouble() {
return (((long) (next(26)) << 27) + next(27)) * DOUBLE_UNIT;
}
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));
}
nextDouble()方法用于生成一個介于[0, 1.0)之間的隨機數,nextDouble()方法可以體現出Random對線性同余算法的具體實現如下:
線性同余算法:????+1 = (a * ???? + c) mod m
Random的具體實現:(seed ^ 0x5DEECE66DL) & ((1L << 48) - 1)
其中 a, c, m都是指定的值,分別為:
- a 是 0x5DEECE66DL(16進制),轉換成 10進制為25214903917,
- c 是 11(0xB),
- m 是 2??,即 281474976710656(10進制) 或 0x1000000000000L(16進制)。
另外,java.util.Random還包含另外一些常用的方法,如下:
public int nextInt() {
return next(32);
}
public boolean nextBoolean() {
return next(1) != 0;
}
public void nextBytes(byte[] bytes) {
for (int i = 0, len = bytes.length; i < len; )
for (int rnd = nextInt(),
n = Math.min(len - i, Integer.SIZE / Byte.SIZE);
n-- > 0; rnd >>= Byte.SIZE)
bytes[i++] = (byte) rnd;
}
6. Math.random()
Math.random()方法是日常開發中生成隨機數使用最多的方法,其本質是對Random類的包裝,下面為 Math.random()的源碼實現:
public static double random() {
return RandomNumberGeneratorHolder.randomNumberGenerator.nextDouble();
}
private static final class RandomNumberGeneratorHolder {
static final Random randomNumberGenerator = new Random();
}
通過Math.random()的源碼可以發現:Math.random() 的實現其實就是 (new Random()).nextDouble(),在這里就不贅述了,另外,日常開發中對Math.random()對真實使用方式,這里也以一副手稿來總結:
解釋:
- Math.random()生成一個介于[0, 1)的隨機數字,即 0~0.999…
- Math.random() * 8生成一個介于[0, 8)的隨機數字,即 0~7.999…
到此,Random生成隨機數講解完成,下面我們進行總結:
7. 總結
上述我們分析了幾種常見的隨機數生成方式,具體選用哪種可以根據自身業務:
- 偽隨機數指的是一種看起來像隨機數的序列,但實際上是由確定性算法生成的,這種算法稱為偽隨機數生成器。
- 線性同余算法是一種生成偽隨機數的常用方法,其實現方程為????+1 = (a * ???? + c) mod m;
- java.util.Random是 JDK提供的一種隨機數生成類,其核心算法就是線性同余算法;
- Math.random() 本質上是對 java.util.Random的包裝;