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