浅谈Java的伪随机数发生器和线性同余法
前言
生成偽隨機(jī)數(shù)是用Java編程時(shí)的常見需求,本文簡(jiǎn)單討論一下最常用的Random和ThreadLocalRandom這兩個(gè)隨機(jī)數(shù)類,順便介紹線性同余法。
Random
話休絮煩,直接上源碼。
private final AtomicLong seed;private static final long multiplier = 0x5DEECE66DL;private static final long addend = 0xBL;private static final long mask = (1L << 48) - 1;public Random() {this(seedUniquifier() ^ System.nanoTime());}private static long seedUniquifier() {for (;;) {long current = seedUniquifier.get();long next = current * 181783497276652981L;if (seedUniquifier.compareAndSet(current, next))return next;}}private static final AtomicLong seedUniquifier= new AtomicLong(8682522807148012L);public Random(long seed) {if (getClass() == Random.class)this.seed = new AtomicLong(initialScramble(seed));else {this.seed = new AtomicLong();setSeed(seed);}}private static long initialScramble(long seed) {return (seed ^ multiplier) & mask;}如果我們構(gòu)造Random實(shí)例時(shí),調(diào)用的是有參構(gòu)造方法(即傳入了自定義的隨機(jī)數(shù)種子seed),那么通過initialScramble()方法產(chǎn)生的實(shí)際使用的種子為:
(seed XOR 0x5DEECE66DL) AND ((1L << 48) - 1)
可見,種子的長度是48比特。
如果調(diào)用的是無參構(gòu)造方法,那么會(huì)將seedUniquifier()方法返回的值與當(dāng)前JVM運(yùn)行的納秒時(shí)間戳做異或運(yùn)算,并作為seed值,再經(jīng)過上式的運(yùn)算得出實(shí)際使用的種子。而seedUniquifier()是個(gè)自旋的方法,它以8682522807148012L作為初始值,不斷與181783497276652981L相乘,直到某次相乘前后的結(jié)果在long范圍內(nèi)相同時(shí)才返回。這兩個(gè)大數(shù)的來歷請(qǐng)參見StackOverflow上的這個(gè)問答。
下面看看生成偽隨機(jī)數(shù)的核心方法next(),其參數(shù)bits是隨機(jī)數(shù)的字長,比如nextInt()方法調(diào)用next()方法時(shí),bits就是32。
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));}產(chǎn)生隨機(jī)數(shù)時(shí),會(huì)首先根據(jù)當(dāng)前種子seed更新種子的值為:
(seed * 0x5DEECE66DL + 0xBL) & ((1L << 48) - 1)
然后將新的種子值右移(48 - bits)位,得到隨機(jī)數(shù)的真值。上式就是所謂線性同余法的實(shí)現(xiàn),下面簡(jiǎn)單分析一下。
線性同余法
線性同余法(linear congruential generator, LCG)是非常古老的偽隨機(jī)數(shù)生成方法,在1958年提出,但時(shí)至如今,它仍然是使用最廣泛的方法之一。它的思想很簡(jiǎn)單,用一個(gè)遞推式即可概括:
X[n+1] = (a·X[n] + c) mod m
其中:
- m > 0,稱為模(modulus);
- 0 < a < m,稱為乘數(shù)(multiplier);
- 0 <= c < m,稱為增量(increment);
- X[0],即遞推序列的初始值,稱為種子(seed)。X就是生成的隨機(jī)數(shù)序列。
LCG有如下的重要性質(zhì)(Hull-Dobell定理):
當(dāng)且僅當(dāng)
(1) c與m互素
(2) a - 1可以被所有m的質(zhì)因數(shù)整除
(3) 如果m能被4整除,那么a - 1也能被4整除
以上三個(gè)條件同時(shí)滿足時(shí),X是一個(gè)周期為m的序列。
既然LCG產(chǎn)生的是周期性的偽隨機(jī)數(shù)序列,為了使它難以預(yù)測(cè),m的值一般都會(huì)選得非常大。Random類中a、c、m這三個(gè)參數(shù)的取值分別為:
a = 0x5DEECE66DL = 25214903917 // multiplier c = 0xBL = 11 //addend m = 1L << 48 = 281474976710656 // mask可見,這三個(gè)值是滿足Hull-Dobell定理的,故Random的周期為248,足夠大了。
為了方便理解,下圖示出不同參數(shù)取值下的LCG周期。可見,當(dāng)不滿足上述定理時(shí)(第一行和第二行),X的周期是一定比m短的。
特別地,c=0的特殊情況叫做Lehmer生成器(Lehmer RNG),又叫乘同余法(multiplicative congruential generator, MCG)。它的出現(xiàn)比LCG更早(1951年),并且參數(shù)的取值有更多的限制。本文不再展開講細(xì)節(jié),看官可以參見LCG與MCG的英文維基相關(guān)頁面。
上面所敘述的過程產(chǎn)生的是整數(shù),如果要產(chǎn)生(0, 1)區(qū)間內(nèi)的小數(shù)怎么辦呢?很簡(jiǎn)單,結(jié)果再除以m就行了,因?yàn)閄中的原始值肯定比m小。
ThreadLocalRandom
阿里Java開發(fā)手冊(cè)中如是說:
【推薦】避免Random實(shí)例被多線程使用,雖然共享該實(shí)例是線程安全的,但會(huì)因競(jìng)爭(zhēng)同一seed導(dǎo)致性能下降。
說明:Random實(shí)例包括java.util.Random的實(shí)例或者M(jìn)ath.random()的方式。
正例:在JDK7之后,可以直接使用API ThreadLocalRandom,而在JDK7之前,需要編碼保證每個(gè)線程持有一個(gè)實(shí)例。
從代碼可知,由于Random類中的種子是AtomicLong類型,在多線程進(jìn)入next()方法時(shí),只有一個(gè)線程能CAS成功更新種子,其他線程都在不斷自旋,性能降低。為了解決這個(gè)問題,產(chǎn)生了ThreadLocalRandom。
ThreadLocalRandom是Random的子類,但在JDK7與JDK8中的實(shí)現(xiàn)方法頗有不同。下面是JDK7版本的主要源碼。
private static final long multiplier = 0x5DEECE66DL;private static final long addend = 0xBL;private static final long mask = (1L << 48) - 1;private long rnd;boolean initialized;// 填充字節(jié),避免不同線程的ThreadLocalRandom競(jìng)爭(zhēng)CPU緩存行造成虛共享private long pad0, pad1, pad2, pad3, pad4, pad5, pad6, pad7;ThreadLocalRandom() { super(); initialized = true; } private static final ThreadLocal<ThreadLocalRandom> localRandom =new ThreadLocal<ThreadLocalRandom>() {protected ThreadLocalRandom initialValue() {return new ThreadLocalRandom();}};public static ThreadLocalRandom current() {return localRandom.get();}public void setSeed(long seed) {if (initialized)throw new UnsupportedOperationException();rnd = (seed ^ multiplier) & mask;}protected int next(int bits) {rnd = (rnd * multiplier + addend) & mask;return (int) (rnd >>> (48-bits));}可見,ThreadLocalRandom產(chǎn)生隨機(jī)數(shù)的思路與Random完全相同,不過是利用ThreadLocal為每個(gè)線程維護(hù)了不同的ThreadLocalRandom實(shí)例。在ThreadLocalRandom初始化時(shí),會(huì)調(diào)用Random的無參構(gòu)造方法,并通過重寫過的setSeed()方法將計(jì)算出的種子存入rnd變量,故每個(gè)線程都擁有了不同的種子,通過current()方法即可取得自己的那一份,不會(huì)再產(chǎn)生沖突。
下面是JDK8版本的主要源碼。
private static final AtomicInteger probeGenerator =new AtomicInteger();private static final AtomicLong seeder = new AtomicLong(initialSeed());boolean initialized;private ThreadLocalRandom() {initialized = true;}static final ThreadLocalRandom instance = new ThreadLocalRandom();static final void localInit() {int p = probeGenerator.addAndGet(PROBE_INCREMENT);int probe = (p == 0) ? 1 : p;long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));Thread t = Thread.currentThread();UNSAFE.putLong(t, SEED, seed);UNSAFE.putInt(t, PROBE, probe);}public static ThreadLocalRandom current() {if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)localInit();return instance;}public void setSeed(long seed) {if (initialized)throw new UnsupportedOperationException();}final long nextSeed() {Thread t; long r;UNSAFE.putLong(t = Thread.currentThread(), SEED,r = UNSAFE.getLong(t, SEED) + GAMMA);return r;}protected int next(int bits) {return (int)(mix64(nextSeed()) >>> (64 - bits));}這些源碼中并沒有出現(xiàn)ThreadLocal,那么每個(gè)線程的種子去哪里了?答案是加進(jìn)了Thread類里,作為獨(dú)立的字段存在。
// sun.misc.Contended注解用于填充緩存行 @sun.misc.Contended("tlr") long threadLocalRandomSeed; @sun.misc.Contended("tlr") int threadLocalRandomProbe;threadLocalRandomSeed就是隨機(jī)數(shù)種子,threadLocalRandomProbe是指示種子是否被初始化的探針變量。可見,JDK8雖然沒有顯式使用ThreadLocal,但是基本的思想是相通的——即每個(gè)線程都持有獨(dú)享的種子值。ThreadLocalRandom代碼中的SEED和PROBE兩個(gè)量,實(shí)際上是通過Unsafe API取得的字段偏移地址。
private static final sun.misc.Unsafe UNSAFE;private static final long SEED;private static final long PROBE;static {try {UNSAFE = sun.misc.Unsafe.getUnsafe();Class<?> tk = Thread.class;SEED = UNSAFE.objectFieldOffset(tk.getDeclaredField("threadLocalRandomSeed"));PROBE = UNSAFE.objectFieldOffset(tk.getDeclaredField("threadLocalRandomProbe"));} catch (Exception e) {throw new Error(e);}}JDK8版ThreadLocalRandom只有一個(gè)全局的實(shí)例,通過current()方法取得。當(dāng)某個(gè)Thread實(shí)例的探針threadLocalRandomProbe為0時(shí),表示是首次執(zhí)行current()方法,進(jìn)而會(huì)調(diào)用localInit()方法初始化種子和探針的值,并將它們寫入對(duì)應(yīng)的Thread實(shí)例中。通過nextSeed()方法就可以分別更新對(duì)應(yīng)線程的種子了。
可見,與JDK7版本相比,JDK8版本的ThreadLocalRandom不再依賴于父類Random的構(gòu)造方法產(chǎn)生種子,也就徹底消除了CAS自旋帶來的開銷。關(guān)于這個(gè)改進(jìn)的細(xì)節(jié),還可以參考StackOverflow上的這個(gè)問題。
最后一丟丟
上面講的LCG、MCG是偽隨機(jī)數(shù)發(fā)生器,也就是說如果知道了a、c、m參數(shù)的值,隨機(jī)數(shù)就被破解了,無法做到安全。要保證安全,可以采用SecureRandom類。它利用了Linux的特殊設(shè)備/dev/random和/dev/urandom,基于系統(tǒng)的熵(進(jìn)程數(shù)、內(nèi)存用量、設(shè)備輸入等無法預(yù)測(cè)的指標(biāo))產(chǎn)生隨機(jī)數(shù),這樣也就沒辦法預(yù)測(cè)隨機(jī)序列的值了。
晚安晚安。
總結(jié)
以上是生活随笔為你收集整理的浅谈Java的伪随机数发生器和线性同余法的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ffmpeg推流和拉流rtsp
- 下一篇: MFC定时器SetTimer函数