淺談Java的偽隨機(jī)數(shù)發(fā)生器和線性同余法

前言

生成偽隨機(jī)數(shù)是用Java編程時(shí)的常見(jiàn)需求,本文簡(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),那么通過(guò)initialScramble()方法產(chǎn)生的實(shí)際使用的種子為:
(seed XOR 0x5DEECE66DL) AND ((1L << 48) - 1)

可見(jiàn)慢显,種子的長(zhǎng)度是48比特爪模。

如果調(diào)用的是無(wú)參構(gòu)造方法,那么會(huì)將seedUniquifier()方法返回的值與當(dāng)前JVM運(yùn)行的納秒時(shí)間戳做異或運(yùn)算荚藻,并作為seed值呻右,再經(jīng)過(guò)上式的運(yùn)算得出實(shí)際使用的種子。而seedUniquifier()是個(gè)自旋的方法鞋喇,它以8682522807148012L作為初始值声滥,不斷與181783497276652981L相乘,直到某次相乘前后的結(jié)果在long范圍內(nèi)相同時(shí)才返回侦香。這兩個(gè)大數(shù)的來(lái)歷請(qǐng)參見(jiàn)StackOverflow上的這個(gè)問(wèn)答落塑。

下面看看生成偽隨機(jī)數(shù)的核心方法next(),其參數(shù)bits是隨機(jī)數(shù)的字長(zhǎng)罐韩,比如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

可見(jiàn)熄云,這三個(gè)值是滿足Hull-Dobell定理的膨更,故Random的周期為248,足夠大了缴允。

為了方便理解荚守,下圖示出不同參數(shù)取值下的LCG周期×钒悖可見(jiàn)矗漾,當(dāng)不滿足上述定理時(shí)(第一行和第二行),X的周期是一定比m短的薄料。

特別地敞贡,c=0的特殊情況叫做Lehmer生成器(Lehmer RNG),又叫乘同余法(multiplicative congruential generator, MCG)摄职。它的出現(xiàn)比LCG更早(1951年)誊役,并且參數(shù)的取值有更多的限制获列。本文不再展開(kāi)講細(xì)節(jié),看官可以參見(jiàn)LCGMCG的英文維基相關(guān)頁(yè)面蛔垢。

上面所敘述的過(guò)程產(chǎn)生的是整數(shù)击孩,如果要產(chǎn)生(0, 1)區(qū)間內(nèi)的小數(shù)怎么辦呢?很簡(jiǎn)單啦桌,結(jié)果再除以m就行了,因?yàn)閄中的原始值肯定比m小及皂。

ThreadLocalRandom

阿里Java開(kāi)發(fā)手冊(cè)中如是說(shuō):

【推薦】避免Random實(shí)例被多線程使用甫男,雖然共享該實(shí)例是線程安全的,但會(huì)因競(jìng)爭(zhēng)同一seed導(dǎo)致性能下降验烧。
說(shuō)明: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è)問(wèn)題庭呜,產(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));
    }

可見(jiàn)数冬,ThreadLocalRandom產(chǎn)生隨機(jī)數(shù)的思路與Random完全相同节槐,不過(guò)是利用ThreadLocal為每個(gè)線程維護(hù)了不同的ThreadLocalRandom實(shí)例。在ThreadLocalRandom初始化時(shí)拐纱,會(huì)調(diào)用Random的無(wú)參構(gòu)造方法疯淫,并通過(guò)重寫過(guò)的setSeed()方法將計(jì)算出的種子存入rnd變量,故每個(gè)線程都擁有了不同的種子戳玫,通過(guò)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));
    }

這些源碼中并沒(méi)有出現(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是指示種子是否被初始化的探針變量《埃可見(jiàn)寞蚌,JDK8雖然沒(méi)有顯式使用ThreadLocal,但是基本的思想是相通的——即每個(gè)線程都持有獨(dú)享的種子值钠糊。ThreadLocalRandom代碼中的SEED和PROBE兩個(gè)量挟秤,實(shí)際上是通過(guò)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í)例抄伍,通過(guò)current()方法取得艘刚。當(dāng)某個(gè)Thread實(shí)例的探針threadLocalRandomProbe為0時(shí),表示是首次執(zhí)行current()方法截珍,進(jìn)而會(huì)調(diào)用localInit()方法初始化種子和探針的值攀甚,并將它們寫入對(duì)應(yīng)的Thread實(shí)例中。通過(guò)nextSeed()方法就可以分別更新對(duì)應(yīng)線程的種子了岗喉。

可見(jiàn)秋度,與JDK7版本相比,JDK8版本的ThreadLocalRandom不再依賴于父類Random的構(gòu)造方法產(chǎn)生種子钱床,也就徹底消除了CAS自旋帶來(lái)的開(kāi)銷静陈。關(guān)于這個(gè)改進(jìn)的細(xì)節(jié),還可以參考StackOverflow上的這個(gè)問(wèn)題诞丽。

最后一丟丟

上面講的LCG鲸拥、MCG是偽隨機(jī)數(shù)發(fā)生器,也就是說(shuō)如果知道了a僧免、c刑赶、m參數(shù)的值,隨機(jī)數(shù)就被破解了懂衩,無(wú)法做到安全撞叨。要保證安全,可以采用SecureRandom類浊洞。它利用了Linux的特殊設(shè)備/dev/random和/dev/urandom牵敷,基于系統(tǒng)的熵(進(jìn)程數(shù)、內(nèi)存用量法希、設(shè)備輸入等無(wú)法預(yù)測(cè)的指標(biāo))產(chǎn)生隨機(jī)數(shù)枷餐,這樣也就沒(méi)辦法預(yù)測(cè)隨機(jī)序列的值了。

晚安晚安苫亦。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末毛肋,一起剝皮案震驚了整個(gè)濱河市怨咪,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌润匙,老刑警劉巖诗眨,帶你破解...
    沈念sama閱讀 217,542評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異孕讳,居然都是意外死亡匠楚,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門厂财,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)芋簿,“玉大人,你說(shuō)我怎么就攤上這事蟀苛∫嬉В” “怎么了逮诲?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,912評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵帜平,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我梅鹦,道長(zhǎng)裆甩,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,449評(píng)論 1 293
  • 正文 為了忘掉前任齐唆,我火速辦了婚禮嗤栓,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘箍邮。我一直安慰自己茉帅,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布锭弊。 她就那樣靜靜地躺著堪澎,像睡著了一般。 火紅的嫁衣襯著肌膚如雪味滞。 梳的紋絲不亂的頭發(fā)上樱蛤,一...
    開(kāi)封第一講書(shū)人閱讀 51,370評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音剑鞍,去河邊找鬼昨凡。 笑死,一個(gè)胖子當(dāng)著我的面吹牛蚁署,可吹牛的內(nèi)容都是我干的便脊。 我是一名探鬼主播,決...
    沈念sama閱讀 40,193評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼光戈,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼就轧!你這毒婦竟也來(lái)了证杭?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,074評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤妒御,失蹤者是張志新(化名)和其女友劉穎解愤,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體乎莉,經(jīng)...
    沈念sama閱讀 45,505評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡送讲,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評(píng)論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了惋啃。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片哼鬓。...
    茶點(diǎn)故事閱讀 39,841評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖边灭,靈堂內(nèi)的尸體忽然破棺而出异希,到底是詐尸還是另有隱情,我是刑警寧澤绒瘦,帶...
    沈念sama閱讀 35,569評(píng)論 5 345
  • 正文 年R本政府宣布称簿,位于F島的核電站,受9級(jí)特大地震影響惰帽,放射性物質(zhì)發(fā)生泄漏憨降。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評(píng)論 3 328
  • 文/蒙蒙 一该酗、第九天 我趴在偏房一處隱蔽的房頂上張望授药。 院中可真熱鬧,春花似錦呜魄、人聲如沸悔叽。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,783評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)娇澎。三九已至,卻和暖如春操骡,著一層夾襖步出監(jiān)牢的瞬間九火,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,918評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工册招, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留岔激,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,962評(píng)論 2 370
  • 正文 我出身青樓是掰,卻偏偏與公主長(zhǎng)得像虑鼎,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評(píng)論 2 354