一聲嘆息,jdk居然有4個random

更多精彩文章。java

《微服務不是所有,只是特定領域的子集》linux

《「分庫分表" ?選型和流程要慎重,不然會失控》程序員

這麼多監控組件,總有一款適合你算法

《使用Netty,咱們到底在開發些什麼?》vim

《這多是最中肯的Redis規範了》api

《程序員畫像,十年沉浮》安全

最有用系列:bash

《Linux生產環境上,最經常使用的一套「vim「技巧》多線程

《Linux生產環境上,最經常使用的一套「Sed「技巧》併發

《Linux生產環境上,最經常使用的一套「AWK「技巧》


咱們從jdk8提及。主要是四個隨機數生成器。神馬?有四個?
接下來咱們簡單說下這幾個類的使用場景,來了解其中的細微差異,和api設計者的良苦用心。

java.util.Random
java.util.concurrent.ThreadLocalRandom
java.security.SecureRandom
java.util.SplittableRandom
複製代碼

Random

最經常使用的就是Random。

用來生成僞隨機數,默認使用48位種子、線性同餘公式進行修改。咱們能夠經過構造器傳入初始seed,或者經過setSeed重置(同步)。默認seed爲系統時間的納秒數,真大!

若是兩個(多個)不一樣的Random實例,使用相同的seed,按照相同的順序調用相同方法,那麼它們獲得的數字序列也是相同的。這看起來不太隨機。 這種設計策略,既有優勢也有缺點,優勢是「相同seed」生成的序列是一致的,使過程具備可回溯和校驗性(平臺無關、運行時機無關);缺點就是,這種一致性,潛在引入其「可被預測」的風險。

Random的實例是線程安全的。 可是,跨線程併發使用相同的java.util.Random實例可能會遇到爭用,從而致使性能稍欠佳(nextX方法中,在對seed賦值時使用了CAS,測試結果顯示,其實性能損耗很小)。 請考慮在多線程設計中使用ThreadLocalRandom。同時,咱們在併發環境下,也沒有必要刻意使用多個Random實例。

Random實例不具備加密安全性。 相反,請考慮使用SecureRandom來獲取加密安全的僞隨機數生成器,以供安全敏感應用程序使用。

Random是最經常使用的隨機數生成類,適用於絕大部分場景。

Random random = new Random(100);  
System.out.println(random.nextInt(10) + "," + random.nextInt(30) + "," + random.nextInt(50));  
 
random = new Random(100);  
System.out.println(random.nextInt(10) + "," + random.nextInt(30) + "," + random.nextInt(50));  
 
random = new Random(100);  
System.out.println(random.nextInt(10) + "," + random.nextInt(30) + "," + random.nextInt(50));  
複製代碼

上述三個不一樣的random實例,使用了相同的seed。調用過程同樣,其中產生的隨機數序列也是徹底同樣的。屢次執行結果也徹底一致,簡單而言,只要初始seed同樣,即便實例不一樣,屢次運行它們的結果都是一致的。這個現象與上面所說的一致。

若是Random構造器中不指定seed,而是使用默認的系統時間納秒數做爲主導變量,三個random實例執行的結果是不一樣的。屢次執行結果也不同。因而可知,seed是否具備隨機性,在必定程度上,也決定了Random產生結果的隨機性。

因此,在分佈式或者多線程環境下,若是Random實例處於代碼一致的tasks線程中,可能這些分佈式進程或者線程,產出的序列值是同樣的。這也是在JDK 7引入ForkJoin的同時,也引入了ThreadLocalRandom類。

ThreadLocalRandom

這個類的做用,使得隨機數的生成器隔離到當前線程。此類繼承自java.util.Random,與Math類使用的全局Random生成器同樣,ThreadLocalRandom使用內部生成的種子進行初始化,不然可能沒法修改。

在併發程序中使用ThreadLocalRandom,一般會有更少的開銷和競爭。 當多個任務(例如,每一個ForkJoinTask)在線程池中並行使用隨機數時,ThreadLocalRandom是特別合適的。

切記,在多個線程中不該該共享ThreadLocalRandom實例。

ThreadLocalRandom初始化是private的,因此沒法經過構造器設定seed,此外其setSeed方法也被重寫而不支持(拋出異常)。默認狀況下,每一個ThreadLocalRandom實例的seed主導變量值爲系統時間(納秒):

private static long initialSeed() {  
    String sec = VM.getSavedProperty("java.util.secureRandomSeed");  
    if (Boolean.parseBoolean(sec)) {  
        byte[] seedBytes = java.security.SecureRandom.getSeed(8);  
        long s = (long)(seedBytes[0]) & 0xffL;  
        for (int i = 1; i < 8; ++i)  
            s = (s << 8) | ((long)(seedBytes[i]) & 0xffL);  
        return s;  
    }  
    return (mix64(System.currentTimeMillis()) ^  
            mix64(System.nanoTime()));  
}  
複製代碼

根據其初始化seed的實現,咱們也能夠經過JVM啓動參數增長「-Djava.util.secureRandomSeed=true」,此時初始seed變量將再也不是系統時間,而是由SecureRandom類生成一個隨機因子,以此做爲ThreadLoalRandom的初始seed。

真是夠繞的。

從源碼中,我並無看到Thread-ID做爲變量生成seed,並且nextX方法中隨機數生成算法也具備一致性。這意味着,若是多個線程初始ThreadLocalRandom的時間徹底一致,在調用方法和過程相同的狀況下,產生的隨機序列也是相同的;在必定程度上「-Djava.util.secureRandom=true」能夠規避此問題。

ThreadLocalRandom並無使用ThreadLocal來支持內部數據存儲等,而是直接使用UnSafe操做當前Thread對象引用中seed屬性的內存地址並進行數據操做,我比較佩服SUN的這種巧妙的作法。

SecureRandom

它也繼承自Random,該類提供加密強隨機數生成器(RNG),加密強隨機數最低限度符合FIPS 140-2「加密模塊的安全要求」。 此外,SecureRandom必須產生非肯定性輸出。 所以,傳遞給SecureRandom對象的任何種子材料必須是不可預測的,而且全部SecureRandom輸出序列必須具備加密強度。(官文,其實我也只知其一;不知其二)

SecureRandom默認支持兩種RNG加密算法實現:
1)"SHA1PRNG"算法提供者sun.security.provider.SecureRandom
2)"NativePRNG"提供者sun.security.provider.NativePRNG

默認狀況下,是「SHA1PRNG」,即SUN提供的實現。此外能夠經過「-Djava.security=file:/dev/urandom」(推薦)或者「-Djava.security=file:/dev/random」指定使用linux本地的隨機算法,即NativePRNG;其中「/dev/random」與「/dev/urandom」在不一樣unix-*平臺中實現有所不一樣,性能也有所差別,建議使用「/dev/urandom」。

/dev/random的一個副本是/dev/urandom (」unlocked」,非阻塞的隨機數發生器),它會重複使用熵池中的數據以產生僞隨機數據。這表示對/dev/urandom的讀取操做不會產生阻塞,但其輸出的熵可能小於/dev/random的。它能夠做爲生成較低強度密碼的僞隨機數生成器,不建議用於生成高強度長期密碼。

算法的內部實現,比較複雜;本人測試,其實性能差不不太大(JDK 8環境)。SecureRandom也是線程安全的。

從輸出結果上分析,不管是否指定SecureRandom的初始seed,單個實例屢次運行的結果也徹底不一樣 ;多個不一樣的SecureRandom實例不管是否指定seed,即便指定同樣的初始seed,同時運行的結果也徹底不一樣。

SecureRandom繼承自Random,可是對nextX方法中的底層方法進行的重寫覆蓋,不過仍然基於Random的CAS且SecureRandom的底層方法還使用的同步,因此在併發環境下,性能比Random差了一些。

SplittableRandom

JDK 8 新增的API,主要適用於Fork/join形式的跨線程操做中。它並無繼承java.util.Random類。

具備相同seed的不一樣SplittableRandom實例或者同一個SplittableRandom,屢次運行結果是一致的。這和Random是一致的。

非線程安全,不能被併發使用。 (不會報錯,可是併發時可能多個線程同時獲得相同的隨機數)

同ThreadLocalRandom,對「-Djava.util.secureRandom=true」參數支持,可是隻有使用默認構造器的時候,纔會使用SecureRandom輔助生成初始seed。即不指定初始seed時,同一個SplittableRandom實例屢次運行,或者不一樣的實例運行,結果是不一樣的。

其中有一個split()方法,用來構造並返回與新的實例,這個實例共享了一些不可變狀態。須要注意,split產生的新SplittableRandom實例,與原實例並不存在內部數據的併發競爭,也不會交替或者延續原實例的隨機數生成序列(即兩個實例產出隨機序列的一致性,與原實例沒有關係,只是在統計值層面更加接近);可是代碼一致性的狀況下,屢次運行,其隨機數序列的結果老是一致的(假如初始seed是指定的,而非默認),這一點與Random、ThreadLocalRandom表現相同。

public SplittableRandom split() {  
    return new SplittableRandom(nextLong(), mixGamma(nextSeed()));  
}  
複製代碼

樣例代碼。

System.out.println("第一段");  
SplittableRandom random = new SplittableRandom(100);  
Thread thread = new Thread(new Runnable() {  
    @Override  
    public void run() {  
        SplittableRandom _random = random.split();  
        for (int i=0; i < 5; i++) {  
            System.out.println("---" + _random.nextInt(100));  
        }  
    }  
});  
thread.start();  
thread.join();  
for (int i=0; i < 5; i++) {  
    System.out.println("+++" + random.nextInt(100));  
}  
  
System.out.println("第二段");  
SplittableRandom _random = new SplittableRandom(100);  
for (int i=0; i < 10; i++) {  
    System.out.println("..." + _random.nextInt(100));  
}  
複製代碼

執行結果。

第一段
---71
---85
---10
---60
---98
+++44
+++87
+++77
+++67
+++72

第二段
...92
...30
...44
...87
...77
...67
...72
...23
...9
...64
複製代碼

從執行結果上看,split產生的random實例與原實例執行結果上沒有類似之處;可是不一樣SplittableRandom實例(不管是否執行過split),其產出隨機數序列是一致的。

性能檢測

簡析,基準:100000隨機數,單線程

一、 Random :2毫秒
二、 ThreadLocalRandom :1毫秒
三、 SecureRandom
1)默認算法,即SHAR1PRNG:80毫秒左右。
2)NativePRNG:90毫秒左右。
四、 SplittableRandom :1毫秒

End

日常使用Random,或者大多數時候使用,都是沒有問題的,它也是線程安全的。SplittableRandom是內部使用的類,應用較少,即便它也是public的也掩飾不了偏門。ThreadLocalRandom是爲了在高併發環境下節省一點細微的時間,追求性能的應用推薦使用。而對於有安全需求的,又但願更隨機一些的,用SecureRandom再好不過了。

jdk居然有這麼多隨機數生成器。有沒有大吃一精?我反正是跪了。


更多精彩文章。

《微服務不是所有,只是特定領域的子集》

《「分庫分表" ?選型和流程要慎重,不然會失控》

這麼多監控組件,總有一款適合你

《Linux生產環境上,最經常使用的一套「vim「技巧》

《使用Netty,咱們到底在開發些什麼?》

相關文章
相關標籤/搜索